@sentry/junior 0.68.0 → 0.70.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.
Files changed (67) hide show
  1. package/dist/app.js +1779 -746
  2. package/dist/build/virtual-config.d.ts +2 -2
  3. package/dist/chat/agent-dispatch/heartbeat.d.ts +2 -2
  4. package/dist/chat/agent-dispatch/store.d.ts +4 -1
  5. package/dist/chat/agent-dispatch/types.d.ts +2 -4
  6. package/dist/chat/agent-dispatch/validation.d.ts +3 -2
  7. package/dist/chat/credentials/context.d.ts +49 -24
  8. package/dist/chat/credentials/user-token-store.d.ts +6 -0
  9. package/dist/chat/destination.d.ts +12 -0
  10. package/dist/chat/ingress/message-router.d.ts +1 -1
  11. package/dist/chat/mcp/auth-store.d.ts +2 -0
  12. package/dist/chat/mcp/oauth.d.ts +2 -0
  13. package/dist/chat/oauth-flow.d.ts +7 -0
  14. package/dist/chat/plugins/agent-hooks.d.ts +9 -9
  15. package/dist/chat/plugins/auth/auth-token-placeholder.d.ts +2 -2
  16. package/dist/chat/plugins/auth/oauth-request.d.ts +3 -1
  17. package/dist/chat/plugins/credential-hooks.d.ts +53 -0
  18. package/dist/chat/plugins/logging.d.ts +1 -1
  19. package/dist/chat/plugins/state.d.ts +1 -1
  20. package/dist/chat/plugins/types.d.ts +19 -23
  21. package/dist/chat/respond.d.ts +2 -0
  22. package/dist/chat/runtime/reply-executor.d.ts +3 -1
  23. package/dist/chat/runtime/slack-runtime.d.ts +8 -3
  24. package/dist/chat/sandbox/egress-credentials.d.ts +34 -0
  25. package/dist/chat/sandbox/egress-schemas.d.ts +105 -0
  26. package/dist/chat/sandbox/egress-session.d.ts +17 -17
  27. package/dist/chat/sandbox/sandbox.d.ts +3 -0
  28. package/dist/chat/sandbox/session.d.ts +1 -0
  29. package/dist/chat/services/mcp-auth-orchestration.d.ts +2 -0
  30. package/dist/chat/services/pending-auth.d.ts +2 -0
  31. package/dist/chat/services/plugin-auth-orchestration.d.ts +2 -0
  32. package/dist/chat/services/provider-retry.d.ts +13 -4
  33. package/dist/chat/services/timeout-resume.d.ts +2 -0
  34. package/dist/chat/services/turn-session-record.d.ts +6 -0
  35. package/dist/chat/slack/attachment-fetchers.d.ts +11 -0
  36. package/dist/chat/state/conversation-details.d.ts +46 -0
  37. package/dist/chat/state/conversation.d.ts +1 -0
  38. package/dist/chat/state/turn-session.d.ts +4 -3
  39. package/dist/chat/task-execution/queue.d.ts +2 -0
  40. package/dist/chat/task-execution/store.d.ts +5 -0
  41. package/dist/chat/task-execution/vercel-callback.d.ts +4 -0
  42. package/dist/chat/task-execution/vercel-queue.d.ts +2 -0
  43. package/dist/chat/task-execution/worker.d.ts +4 -2
  44. package/dist/chat/tools/slack/context.d.ts +3 -0
  45. package/dist/chat/tools/types.d.ts +21 -2
  46. package/dist/chunk-76YMBKW7.js +326 -0
  47. package/dist/{chunk-PIVOJIUD.js → chunk-B5HKWWQB.js} +9 -5
  48. package/dist/chunk-BBXYXOJW.js +1858 -0
  49. package/dist/{chunk-V47RLIO2.js → chunk-GT67ZWZQ.js} +4 -4
  50. package/dist/{chunk-UQQSW7QB.js → chunk-HOGQL2H6.js} +197 -343
  51. package/dist/{chunk-75UZ4JLC.js → chunk-IGLNC5H6.js} +21 -9
  52. package/dist/{chunk-EBVQXCD2.js → chunk-JS4HURDT.js} +362 -280
  53. package/dist/chunk-R62YWUNO.js +264 -0
  54. package/dist/{chunk-OIIXZOOC.js → chunk-UXG6TU2U.js} +311 -2015
  55. package/dist/cli/check.js +4 -4
  56. package/dist/cli/init.js +18 -1
  57. package/dist/cli/snapshot-warmup.js +5 -4
  58. package/dist/nitro.d.ts +1 -1
  59. package/dist/nitro.js +21 -19
  60. package/dist/plugins.d.ts +2 -2
  61. package/dist/reporting.d.ts +8 -4
  62. package/dist/reporting.js +72 -29
  63. package/package.json +6 -4
  64. package/dist/chat/plugins/auth/github-app-broker.d.ts +0 -4
  65. package/dist/chat/plugins/github-permissions.d.ts +0 -11
  66. package/dist/chat/queue/thread-message-dispatcher.d.ts +0 -33
  67. package/dist/chunk-KVZL5NZS.js +0 -519
package/dist/app.js CHANGED
@@ -1,8 +1,6 @@
1
1
  import {
2
2
  GET,
3
3
  JUNIOR_PERSONALITY,
4
- SlackActionError,
5
- TURN_CONTEXT_TAG,
6
4
  abandonAgentTurnSessionRecord,
7
5
  bindSlackDirectCredentialSubject,
8
6
  buildSentryConversationUrl,
@@ -13,7 +11,6 @@ import {
13
11
  createAgentPluginHookRunner,
14
12
  createAgentPluginLogger,
15
13
  createPluginState,
16
- downloadPrivateSlackFile,
17
14
  escapeXml,
18
15
  failAgentTurnSessionRecord,
19
16
  getAgentPluginRoutes,
@@ -21,39 +18,34 @@ import {
21
18
  getAgentPluginTools,
22
19
  getAgentPlugins,
23
20
  getAgentTurnSessionRecord,
24
- getFilePermalink,
25
- getHeaderString,
26
21
  getInterruptionMarker,
27
- getSlackClient,
28
- isConversationChannel,
29
- isConversationScopedChannel,
30
- isDmChannel,
22
+ initConversationContext,
31
23
  listAgentTurnSessionSummaries,
32
24
  listAgentTurnSessionSummariesForConversation,
33
25
  loadConnectedMcpProviders,
34
26
  loadProjection,
35
- normalizeSlackConversationId,
36
27
  normalizeSlackStatusText,
37
28
  recordAgentTurnSessionSummary,
38
29
  recordAuthorizationCompleted,
39
30
  recordAuthorizationRequested,
40
31
  recordMcpProviderConnected,
32
+ resolveChannelCapabilities,
41
33
  resolveSlackChannelTypeFromMessage,
42
34
  resolveSlackConversationContext,
43
35
  setAgentPlugins,
36
+ setConversationTitle,
44
37
  splitSlackReplyText,
45
38
  truncateStatusText,
46
39
  upsertAgentTurnSessionRecord,
47
40
  validateAgentPlugins,
48
- verifySlackDirectCredentialSubject,
49
- withSlackRetries
50
- } from "./chunk-UQQSW7QB.js";
41
+ verifySlackDirectCredentialSubject
42
+ } from "./chunk-HOGQL2H6.js";
51
43
  import {
52
44
  discoverSkills,
53
45
  findSkillByName,
54
46
  loadSkillsByName,
55
47
  parseSkillInvocation
56
- } from "./chunk-V47RLIO2.js";
48
+ } from "./chunk-GT67ZWZQ.js";
57
49
  import {
58
50
  buildNonInteractiveShellScript,
59
51
  createSandboxInstance,
@@ -62,104 +54,143 @@ import {
62
54
  isSnapshotMissingError,
63
55
  resolveRuntimeDependencySnapshot,
64
56
  runNonInteractiveCommand
65
- } from "./chunk-PIVOJIUD.js";
57
+ } from "./chunk-B5HKWWQB.js";
66
58
  import {
67
59
  ACTIVE_LOCK_TTL_MS,
60
+ SANDBOX_DATA_ROOT,
61
+ SANDBOX_SKILLS_ROOT,
62
+ SANDBOX_WORKSPACE_ROOT,
63
+ getStateAdapter,
64
+ sandboxSkillDir,
65
+ sandboxSkillFile
66
+ } from "./chunk-R62YWUNO.js";
67
+ import {
68
+ CredentialUnavailableError,
69
+ buildActorIdentity,
70
+ buildOAuthTokenRequest,
71
+ createPluginBroker,
72
+ credentialContextSchema,
73
+ getPluginCapabilityProviders,
74
+ getPluginCatalogSignature,
75
+ getPluginDefinition,
76
+ getPluginMcpProviders,
77
+ getPluginOAuthConfig,
78
+ getPluginProviders,
79
+ hasRequiredOAuthScope,
80
+ isActorUserId,
81
+ isPluginConfigKey,
82
+ isPluginProvider,
83
+ parseActorUserId,
84
+ parseOAuthTokenResponse,
85
+ resolveAuthTokenPlaceholder,
86
+ resolvePluginCommandEnv,
87
+ setPluginCatalogConfig,
88
+ slackActorIdentity
89
+ } from "./chunk-UXG6TU2U.js";
90
+ import {
91
+ defineJuniorPlugins,
92
+ getVercelConversationWorkQueue,
93
+ pluginCatalogConfigFromPluginSet,
94
+ pluginHookRegistrationsFromPluginSet,
95
+ resolveConversationWorkQueueTopic,
96
+ verifySignedConversationQueueMessage
97
+ } from "./chunk-IGLNC5H6.js";
98
+ import {
99
+ SlackActionError,
100
+ createSlackDestination,
101
+ destinationKey,
102
+ downloadPrivateSlackFile,
103
+ getFilePermalink,
104
+ getHeaderString,
105
+ getSlackClient,
106
+ isConversationScopedChannel,
107
+ isDmChannel,
108
+ normalizeSlackConversationId,
109
+ parseDestination,
110
+ sameDestination,
111
+ withSlackRetries
112
+ } from "./chunk-76YMBKW7.js";
113
+ import {
68
114
  FUNCTION_TIMEOUT_BUFFER_SECONDS,
69
115
  GEN_AI_PROVIDER_NAME,
70
116
  GEN_AI_SERVER_ADDRESS,
71
117
  GEN_AI_SERVER_PORT,
72
118
  MISSING_GATEWAY_CREDENTIALS_ERROR,
73
- SANDBOX_DATA_ROOT,
74
- SANDBOX_SKILLS_ROOT,
75
- SANDBOX_WORKSPACE_ROOT,
76
119
  botConfig,
120
+ buildUserTurnText,
77
121
  completeObject,
78
122
  completeText,
123
+ encodeNonImageAttachmentForPrompt,
124
+ extractAssistantText,
79
125
  getChatConfig,
80
126
  getGatewayApiKey,
81
127
  getPiGatewayApiKeyOverride,
128
+ getPiMessageRole,
82
129
  getRuntimeMetadata,
130
+ getSessionIdentifiers,
83
131
  getSlackBotToken,
84
132
  getSlackClientId,
85
133
  getSlackClientSecret,
86
134
  getSlackReactionConfig,
87
135
  getSlackSigningSecret,
88
- getStateAdapter,
136
+ getTerminalAssistantMessages,
137
+ hasRuntimeTurnContext,
138
+ isAssistantMessage,
139
+ isExecutionEscapeResponse,
140
+ isProviderRetryError,
141
+ isRawToolPayloadResponse,
142
+ isToolResultError,
143
+ isToolResultMessage,
144
+ nextProviderRetry,
89
145
  normalizeSlackEmojiName,
146
+ normalizeToolNameFromResult,
90
147
  parseSlackThreadId,
148
+ prependMissingRuntimeTurnContext,
91
149
  resolveConversationPrivacy,
92
150
  resolveGatewayModel,
93
151
  resolveSlackChannelIdFromMessage,
94
152
  resolveSlackChannelIdFromThreadId,
95
- sandboxSkillDir,
96
- sandboxSkillFile,
97
153
  setSlackReactionConfig,
154
+ stripRuntimeTurnContext,
155
+ summarizeMessageText,
98
156
  toGenAiMessageMetadata,
99
157
  toGenAiMessagesTraceAttributes,
100
158
  toGenAiPayloadMetadata,
101
159
  toGenAiPayloadTraceAttributes,
102
- toGenAiTextMetadata
103
- } from "./chunk-EBVQXCD2.js";
160
+ toGenAiTextMetadata,
161
+ toObservablePromptPart,
162
+ trimTrailingAssistantMessages,
163
+ upsertActiveSkill
164
+ } from "./chunk-JS4HURDT.js";
104
165
  import {
105
- CredentialUnavailableError,
106
- buildActorIdentity,
107
- buildOAuthTokenRequest,
108
166
  buildTurnFailureResponse,
109
167
  createChatSdkLogger,
110
- createPluginBroker,
111
168
  createRequestContext,
112
169
  extractGenAiUsageAttributes,
113
170
  extractGenAiUsageSummary,
114
171
  getActiveTraceId,
115
172
  getLogContextAttributes,
116
- getPluginCapabilityProviders,
117
- getPluginCatalogSignature,
118
- getPluginDefinition,
119
- getPluginMcpProviders,
120
- getPluginOAuthConfig,
121
- getPluginProviders,
122
- hasRequiredOAuthScope,
123
- isActorUserId,
124
- isPluginConfigKey,
125
- isPluginProvider,
173
+ homeDir,
126
174
  isRecord,
175
+ listReferenceFiles,
127
176
  logError,
128
177
  logException,
129
178
  logInfo,
130
179
  logWarn,
131
180
  normalizeGenAiFinishReason,
132
- parseActorUserId,
133
- parseCredentialContext,
134
- parseOAuthTokenResponse,
135
- resolveAuthTokenPlaceholder,
136
- resolvePluginCommandEnv,
137
181
  serializeGenAiAttribute,
138
- setPluginCatalogConfig,
139
182
  setSentryUser,
140
183
  setSpanAttributes,
141
184
  setSpanStatus,
142
185
  setTags,
143
- slackActorIdentity,
144
186
  toOptionalNumber,
145
187
  toOptionalString,
146
188
  withContext,
147
189
  withSpan
148
- } from "./chunk-OIIXZOOC.js";
190
+ } from "./chunk-BBXYXOJW.js";
149
191
  import {
150
192
  sentry_exports
151
193
  } from "./chunk-Z3YD6NHK.js";
152
- import {
153
- defineJuniorPlugins,
154
- getVercelConversationWorkQueue,
155
- pluginCatalogConfigFromPluginSet,
156
- trustedPluginRegistrationsFromPluginSet,
157
- verifySignedConversationQueueMessage
158
- } from "./chunk-75UZ4JLC.js";
159
- import {
160
- homeDir,
161
- listReferenceFiles
162
- } from "./chunk-KVZL5NZS.js";
163
194
  import "./chunk-2KG3PWR4.js";
164
195
 
165
196
  // src/app.ts
@@ -3240,6 +3271,11 @@ async function listThreadReplies(input) {
3240
3271
  return replies.slice(0, targetLimit);
3241
3272
  }
3242
3273
 
3274
+ // src/chat/tools/slack/context.ts
3275
+ function getSlackDeliveryChannelId(context) {
3276
+ return context.deliveryChannelId ?? context.channelId;
3277
+ }
3278
+
3243
3279
  // src/chat/tools/slack/channel-list-messages.ts
3244
3280
  function createSlackChannelListMessagesTool(context) {
3245
3281
  return tool({
@@ -3292,7 +3328,7 @@ function createSlackChannelListMessagesTool(context) {
3292
3328
  inclusive,
3293
3329
  max_pages
3294
3330
  }) => {
3295
- const targetChannelId = context.channelId;
3331
+ const targetChannelId = getSlackDeliveryChannelId(context);
3296
3332
  if (!targetChannelId) {
3297
3333
  return {
3298
3334
  ok: false,
@@ -3600,7 +3636,7 @@ function createSlackChannelPostMessageTool(context, state) {
3600
3636
  })
3601
3637
  }),
3602
3638
  execute: async ({ text }) => {
3603
- const targetChannelId = context.channelId;
3639
+ const targetChannelId = getSlackDeliveryChannelId(context);
3604
3640
  if (!targetChannelId) {
3605
3641
  return {
3606
3642
  ok: false,
@@ -3971,7 +4007,7 @@ function createSlackCanvasCreateTool(context, state) {
3971
4007
  })
3972
4008
  }),
3973
4009
  execute: async ({ title, markdown }) => {
3974
- const targetChannelId = context.channelId;
4010
+ const targetChannelId = getSlackDeliveryChannelId(context);
3975
4011
  if (!isConversationScopedChannel(targetChannelId)) {
3976
4012
  logError(
3977
4013
  "slack_canvas_create_invalid_context",
@@ -4860,7 +4896,10 @@ function createSlackThreadReadTool(context) {
4860
4896
  error: "Provide either a Slack message `url` or both `channel_id` and `ts`."
4861
4897
  };
4862
4898
  }
4863
- const access = checkChannelAccess(channelId, context.channelId);
4899
+ const access = checkChannelAccess(
4900
+ channelId,
4901
+ getSlackDeliveryChannelId(context)
4902
+ );
4864
4903
  if (!access.allowed) {
4865
4904
  return {
4866
4905
  ok: false,
@@ -5205,263 +5244,6 @@ import {
5205
5244
  } from "@earendil-works/pi-agent-core";
5206
5245
  import { Type as Type21 } from "@sinclair/typebox";
5207
5246
 
5208
- // src/chat/respond-helpers.ts
5209
- var MAX_INLINE_ATTACHMENT_BASE64_CHARS = 12e4;
5210
- var RUNTIME_TURN_CONTEXT_START = `<${TURN_CONTEXT_TAG}>`;
5211
- function getSessionIdentifiers(context) {
5212
- return {
5213
- conversationId: context.correlation?.conversationId ?? context.correlation?.threadId ?? context.correlation?.runId,
5214
- sessionId: context.correlation?.turnId
5215
- };
5216
- }
5217
- function isExecutionDeferralResponse(text) {
5218
- return /\b(want me to proceed|do you want me to proceed|shall i proceed|can i proceed|should i proceed|let me do that now|give me a moment|tag me again|fresh invocation)\b/i.test(
5219
- text
5220
- );
5221
- }
5222
- function isToolAccessDisclaimerResponse(text) {
5223
- return /\b(i (don't|do not) have access to (active )?tool|tool results came back empty|prior results .* empty|cannot access .*tool|need to (run|load) .*tool .* first)\b/i.test(
5224
- text
5225
- );
5226
- }
5227
- function isExecutionEscapeResponse(text) {
5228
- const trimmed = text.trim();
5229
- if (!trimmed) return false;
5230
- return isExecutionDeferralResponse(trimmed) || isToolAccessDisclaimerResponse(trimmed);
5231
- }
5232
- function parseJsonCandidate(text) {
5233
- const trimmed = text.trim();
5234
- if (!trimmed) return void 0;
5235
- try {
5236
- return JSON.parse(trimmed);
5237
- } catch {
5238
- const fenced = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
5239
- if (!fenced) return void 0;
5240
- try {
5241
- return JSON.parse(fenced[1]);
5242
- } catch {
5243
- return void 0;
5244
- }
5245
- }
5246
- }
5247
- function isToolPayloadShape(payload) {
5248
- if (!payload || typeof payload !== "object") return false;
5249
- const record = payload;
5250
- const type = typeof record.type === "string" ? record.type.toLowerCase() : "";
5251
- if (type.startsWith("tool-")) return true;
5252
- if (type === "tool_use" || type === "tool_call" || type === "tool_result" || type === "tool_error")
5253
- return true;
5254
- const hasToolName = typeof record.toolName === "string" || typeof record.name === "string";
5255
- const hasToolInput = Object.prototype.hasOwnProperty.call(record, "input") || Object.prototype.hasOwnProperty.call(record, "args");
5256
- if (hasToolName && hasToolInput) return true;
5257
- return false;
5258
- }
5259
- function isRawToolPayloadResponse(text) {
5260
- const parsed = parseJsonCandidate(text);
5261
- if (Array.isArray(parsed)) {
5262
- return parsed.some((entry) => isToolPayloadShape(entry));
5263
- }
5264
- if (isToolPayloadShape(parsed)) {
5265
- return true;
5266
- }
5267
- const compact = text.replace(/\s+/g, " ");
5268
- return /"type"\s*:\s*"tool[-_](use|call|result|error)"/i.test(compact);
5269
- }
5270
- function toObservablePromptPart(part) {
5271
- if (part.type === "text") {
5272
- return {
5273
- type: "text",
5274
- text: part.text
5275
- };
5276
- }
5277
- return {
5278
- type: "image",
5279
- mimeType: part.mimeType,
5280
- data: `[omitted:${part.data.length}]`
5281
- };
5282
- }
5283
- function summarizeMessageText(text) {
5284
- const normalized = text.trim().replace(/\s+/g, " ");
5285
- if (!normalized) {
5286
- return "[empty]";
5287
- }
5288
- return normalized.length > 1200 ? `${normalized.slice(0, 1200)}...` : normalized;
5289
- }
5290
- function isStructuredThreadContext(context) {
5291
- return /^<thread-(compactions|transcript)>/.test(context);
5292
- }
5293
- function renderThreadContextForPrompt(context) {
5294
- if (isStructuredThreadContext(context)) {
5295
- return context;
5296
- }
5297
- return ["<thread-background>", context, "</thread-background>"].join("\n");
5298
- }
5299
- function buildUserTurnText(userInput, conversationContext) {
5300
- const trimmedContext = conversationContext?.trim();
5301
- if (!trimmedContext) {
5302
- return userInput;
5303
- }
5304
- return [
5305
- renderThreadContextForPrompt(trimmedContext),
5306
- "",
5307
- "<current-instruction>",
5308
- userInput,
5309
- "</current-instruction>"
5310
- ].join("\n");
5311
- }
5312
- function encodeNonImageAttachmentForPrompt(attachment) {
5313
- const base64 = attachment.data.toString("base64");
5314
- const wasTruncated = base64.length > MAX_INLINE_ATTACHMENT_BASE64_CHARS;
5315
- const encodedPayload = wasTruncated ? `${base64.slice(0, MAX_INLINE_ATTACHMENT_BASE64_CHARS)}...` : base64;
5316
- return [
5317
- "<attachment>",
5318
- `filename: ${attachment.filename ?? "unnamed"}`,
5319
- `media_type: ${attachment.mediaType}`,
5320
- "encoding: base64",
5321
- `truncated: ${wasTruncated ? "true" : "false"}`,
5322
- "<data_base64>",
5323
- encodedPayload,
5324
- "</data_base64>",
5325
- "</attachment>"
5326
- ].join("\n");
5327
- }
5328
- function isToolResultMessage(value) {
5329
- return typeof value === "object" && value !== null && value.role === "toolResult";
5330
- }
5331
- function normalizeToolNameFromResult(result) {
5332
- if (!result || typeof result !== "object") return void 0;
5333
- const record = result;
5334
- if (typeof record.toolName === "string" && record.toolName.length > 0) {
5335
- return record.toolName;
5336
- }
5337
- if (typeof record.name === "string" && record.name.length > 0) {
5338
- return record.name;
5339
- }
5340
- return void 0;
5341
- }
5342
- function isToolResultError(result) {
5343
- if (!result || typeof result !== "object") return false;
5344
- return Boolean(result.isError);
5345
- }
5346
- function isAssistantMessage(value) {
5347
- return typeof value === "object" && value !== null && value.role === "assistant";
5348
- }
5349
- function getPiMessageRole(value) {
5350
- if (!value || typeof value !== "object") {
5351
- return void 0;
5352
- }
5353
- const role = value.role;
5354
- return typeof role === "string" ? role : void 0;
5355
- }
5356
- function getUserMessageContent(message) {
5357
- const record = message;
5358
- return record.role === "user" && Array.isArray(record.content) ? record.content : void 0;
5359
- }
5360
- function isRuntimeTurnContextPart(part, marker) {
5361
- return part !== null && typeof part === "object" && part.type === "text" && typeof part.text === "string" && part.text.startsWith(marker);
5362
- }
5363
- function prependRuntimeTurnContext(message, turnContextPrompt) {
5364
- const content = getUserMessageContent(message);
5365
- if (!content) {
5366
- return void 0;
5367
- }
5368
- const contextIndex = content.findIndex(
5369
- (part) => isRuntimeTurnContextPart(part, RUNTIME_TURN_CONTEXT_START)
5370
- );
5371
- if (contextIndex >= 0) {
5372
- return void 0;
5373
- }
5374
- return {
5375
- ...message,
5376
- content: [{ type: "text", text: turnContextPrompt }, ...content]
5377
- };
5378
- }
5379
- function prependMissingRuntimeTurnContext(messages, turnContextPrompt) {
5380
- if (hasRuntimeTurnContext(messages)) {
5381
- return messages;
5382
- }
5383
- for (let index = messages.length - 1; index >= 0; index -= 1) {
5384
- const updated = prependRuntimeTurnContext(
5385
- messages[index],
5386
- turnContextPrompt
5387
- );
5388
- if (!updated) {
5389
- continue;
5390
- }
5391
- const nextMessages = [...messages];
5392
- nextMessages[index] = updated;
5393
- return nextMessages;
5394
- }
5395
- return [
5396
- ...messages,
5397
- {
5398
- role: "user",
5399
- content: [{ type: "text", text: turnContextPrompt }],
5400
- timestamp: Date.now()
5401
- }
5402
- ];
5403
- }
5404
- function hasRuntimeTurnContext(messages) {
5405
- return messages.some(
5406
- (message) => getUserMessageContent(message)?.some(
5407
- (part) => isRuntimeTurnContextPart(part, RUNTIME_TURN_CONTEXT_START)
5408
- )
5409
- );
5410
- }
5411
- function stripRuntimeTurnContext(messages) {
5412
- return messages.flatMap((message) => {
5413
- const content = getUserMessageContent(message);
5414
- if (!content) {
5415
- return [message];
5416
- }
5417
- const nextContent = content.filter(
5418
- (part) => !isRuntimeTurnContextPart(part, RUNTIME_TURN_CONTEXT_START)
5419
- );
5420
- if (nextContent.length === content.length) {
5421
- return [message];
5422
- }
5423
- if (nextContent.length === 0) {
5424
- return [];
5425
- }
5426
- return [{ ...message, content: nextContent }];
5427
- });
5428
- }
5429
- function extractAssistantText(message) {
5430
- const content = message.content ?? [];
5431
- return content.filter(
5432
- (part) => part.type === "text" && typeof part.text === "string"
5433
- ).map((part) => part.text).join("\n");
5434
- }
5435
- function getTerminalAssistantMessages(messages) {
5436
- let lastToolResultIndex = -1;
5437
- for (let index = messages.length - 1; index >= 0; index -= 1) {
5438
- if (isToolResultMessage(messages[index])) {
5439
- lastToolResultIndex = index;
5440
- break;
5441
- }
5442
- }
5443
- return messages.slice(lastToolResultIndex + 1).filter(isAssistantMessage);
5444
- }
5445
- function upsertActiveSkill(activeSkills, next) {
5446
- const existing = activeSkills.find((skill) => skill.name === next.name);
5447
- if (existing) {
5448
- existing.body = next.body;
5449
- existing.description = next.description;
5450
- existing.skillPath = next.skillPath;
5451
- existing.allowedTools = next.allowedTools;
5452
- existing.pluginProvider = next.pluginProvider;
5453
- return;
5454
- }
5455
- activeSkills.push(next);
5456
- }
5457
- function trimTrailingAssistantMessages(messages) {
5458
- let end = messages.length;
5459
- while (end > 0 && getPiMessageRole(messages[end - 1]) === "assistant") {
5460
- end -= 1;
5461
- }
5462
- return end === messages.length ? [...messages] : messages.slice(0, end);
5463
- }
5464
-
5465
5247
  // src/chat/state/ttl.ts
5466
5248
  var JUNIOR_THREAD_STATE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
5467
5249
 
@@ -6431,18 +6213,20 @@ function createTools(availableSkills, hooks = {}, context) {
6431
6213
  tools.searchMcpTools = createSearchMcpToolsTool(context.mcpToolManager);
6432
6214
  tools.callMcpTool = createCallMcpToolTool(context.mcpToolManager);
6433
6215
  }
6434
- const { channelCapabilities } = context;
6435
- if (channelCapabilities.canCreateCanvas) {
6216
+ const outputChannelId = getSlackDeliveryChannelId(context);
6217
+ const outputCapabilities = resolveChannelCapabilities(outputChannelId);
6218
+ const rawChannelCapabilities = resolveChannelCapabilities(context.channelId);
6219
+ if (outputCapabilities.canCreateCanvas) {
6436
6220
  tools.slackCanvasCreate = createSlackCanvasCreateTool(context, state);
6437
6221
  }
6438
- if (channelCapabilities.canPostToChannel) {
6222
+ if (outputCapabilities.canPostToChannel) {
6439
6223
  tools.slackChannelPostMessage = createSlackChannelPostMessageTool(
6440
6224
  context,
6441
6225
  state
6442
6226
  );
6443
6227
  tools.slackChannelListMessages = createSlackChannelListMessagesTool(context);
6444
6228
  }
6445
- if (channelCapabilities.canAddReactions) {
6229
+ if (rawChannelCapabilities.canAddReactions) {
6446
6230
  tools.slackMessageAddReaction = createSlackMessageAddReactionTool(
6447
6231
  context,
6448
6232
  state
@@ -6452,24 +6236,13 @@ function createTools(availableSkills, hooks = {}, context) {
6452
6236
  getAgentPluginTools(context)
6453
6237
  )) {
6454
6238
  if (tools[name]) {
6455
- throw new Error(
6456
- `Trusted plugin tool "${name}" conflicts with a core tool`
6457
- );
6239
+ throw new Error(`Plugin tool "${name}" conflicts with a core tool`);
6458
6240
  }
6459
6241
  tools[name] = pluginTool;
6460
6242
  }
6461
6243
  return tools;
6462
6244
  }
6463
6245
 
6464
- // src/chat/tools/channel-capabilities.ts
6465
- function resolveChannelCapabilities(channelId) {
6466
- return {
6467
- canCreateCanvas: isConversationScopedChannel(channelId),
6468
- canPostToChannel: isConversationChannel(channelId),
6469
- canAddReactions: isConversationScopedChannel(channelId)
6470
- };
6471
- }
6472
-
6473
6246
  // src/chat/pi/traced-stream.ts
6474
6247
  import {
6475
6248
  streamSimple
@@ -6581,6 +6354,33 @@ import fs3 from "fs/promises";
6581
6354
  // src/chat/oauth-flow.ts
6582
6355
  import { randomBytes } from "crypto";
6583
6356
  var OAUTH_STATE_TTL_MS = 10 * 60 * 1e3;
6357
+ function optionalString(value) {
6358
+ return typeof value === "string" ? value : void 0;
6359
+ }
6360
+ function parseOAuthStatePayload(value) {
6361
+ if (!isRecord(value)) {
6362
+ return void 0;
6363
+ }
6364
+ if (typeof value.userId !== "string" || typeof value.provider !== "string") {
6365
+ return void 0;
6366
+ }
6367
+ const destination = parseDestination(value.destination);
6368
+ if (value.destination !== void 0 && !destination) {
6369
+ return void 0;
6370
+ }
6371
+ return {
6372
+ userId: value.userId,
6373
+ provider: value.provider,
6374
+ ...optionalString(value.channelId) ? { channelId: optionalString(value.channelId) } : {},
6375
+ ...destination ? { destination } : {},
6376
+ ...optionalString(value.threadTs) ? { threadTs: optionalString(value.threadTs) } : {},
6377
+ ...optionalString(value.pendingMessage) ? { pendingMessage: optionalString(value.pendingMessage) } : {},
6378
+ ...isRecord(value.configuration) ? { configuration: value.configuration } : {},
6379
+ ...optionalString(value.resumeConversationId) ? { resumeConversationId: optionalString(value.resumeConversationId) } : {},
6380
+ ...optionalString(value.resumeSessionId) ? { resumeSessionId: optionalString(value.resumeSessionId) } : {},
6381
+ ...optionalString(value.scope) ? { scope: optionalString(value.scope) } : {}
6382
+ };
6383
+ }
6584
6384
  function formatProviderLabel(provider) {
6585
6385
  return provider.charAt(0).toUpperCase() + provider.slice(1);
6586
6386
  }
@@ -6684,17 +6484,20 @@ async function startOAuthFlow(provider, input) {
6684
6484
  }
6685
6485
  const configuration = input.userMessage && input.channelConfiguration ? await input.channelConfiguration.resolveValues() : void 0;
6686
6486
  const state = randomBytes(32).toString("hex");
6487
+ const requestedScope = input.scope ?? providerConfig.scope;
6687
6488
  await getStateAdapter().set(
6688
6489
  `oauth-state:${state}`,
6689
6490
  {
6690
6491
  userId: input.requesterId,
6691
6492
  provider,
6692
6493
  ...input.channelId ? { channelId: input.channelId } : {},
6494
+ ...input.destination ? { destination: input.destination } : {},
6693
6495
  ...input.threadTs ? { threadTs: input.threadTs } : {},
6694
6496
  ...input.userMessage ? { pendingMessage: input.userMessage } : {},
6695
6497
  ...configuration && Object.keys(configuration).length > 0 ? { configuration } : {},
6696
6498
  ...input.resumeConversationId ? { resumeConversationId: input.resumeConversationId } : {},
6697
- ...input.resumeSessionId ? { resumeSessionId: input.resumeSessionId } : {}
6499
+ ...input.resumeSessionId ? { resumeSessionId: input.resumeSessionId } : {},
6500
+ ...requestedScope ? { scope: requestedScope } : {}
6698
6501
  },
6699
6502
  OAUTH_STATE_TTL_MS
6700
6503
  );
@@ -6704,8 +6507,8 @@ async function startOAuthFlow(provider, input) {
6704
6507
  redirect_uri: `${baseUrl}${providerConfig.callbackPath}`,
6705
6508
  response_type: "code"
6706
6509
  });
6707
- if (providerConfig.scope) {
6708
- authorizeParams.set("scope", providerConfig.scope);
6510
+ if (requestedScope) {
6511
+ authorizeParams.set("scope", requestedScope);
6709
6512
  }
6710
6513
  for (const [key, value] of Object.entries(
6711
6514
  providerConfig.authorizeParams ?? {}
@@ -6734,22 +6537,88 @@ async function startOAuthFlow(provider, input) {
6734
6537
 
6735
6538
  // src/chat/sandbox/egress-session.ts
6736
6539
  import { createHmac, randomUUID, timingSafeEqual } from "crypto";
6540
+
6541
+ // src/chat/sandbox/egress-schemas.ts
6542
+ import { z } from "zod";
6543
+ import {
6544
+ agentPluginAuthorizationSchema,
6545
+ agentPluginCredentialHeaderTransformSchema,
6546
+ agentPluginGrantSchema,
6547
+ agentPluginProviderAccountSchema
6548
+ } from "@sentry/junior-plugin-api";
6549
+ var finiteNumberSchema = z.number().refine(Number.isFinite);
6550
+ var httpStatusSchema = z.number().int().min(100).max(599);
6551
+ var providerNameSchema = z.string().regex(/^[a-z][a-z0-9-]*$/);
6552
+ var sandboxEgressGrantSchema = agentPluginGrantSchema;
6553
+ var sandboxEgressCredentialContextSchema = z.object({
6554
+ credentials: credentialContextSchema,
6555
+ egressId: z.string().min(1),
6556
+ expiresAtMs: finiteNumberSchema,
6557
+ contextId: z.string().min(1)
6558
+ }).strict();
6559
+ var sandboxEgressCredentialLeaseSchema = z.object({
6560
+ account: agentPluginProviderAccountSchema.optional(),
6561
+ authorization: agentPluginAuthorizationSchema.optional(),
6562
+ grant: sandboxEgressGrantSchema,
6563
+ provider: providerNameSchema,
6564
+ expiresAt: z.string().min(1),
6565
+ headerTransforms: z.array(agentPluginCredentialHeaderTransformSchema).min(1)
6566
+ }).strict();
6567
+ var sandboxEgressAuthRequiredSignalSchema = z.object({
6568
+ authorization: agentPluginAuthorizationSchema.optional(),
6569
+ grant: sandboxEgressGrantSchema,
6570
+ provider: providerNameSchema,
6571
+ message: z.string().optional(),
6572
+ createdAtMs: finiteNumberSchema
6573
+ }).strict().superRefine((signal, ctx) => {
6574
+ if (signal.authorization && signal.authorization.provider !== signal.provider) {
6575
+ ctx.addIssue({
6576
+ code: z.ZodIssueCode.custom,
6577
+ message: "Auth signal authorization provider must match provider",
6578
+ path: ["authorization", "provider"]
6579
+ });
6580
+ }
6581
+ });
6582
+ var sandboxEgressPermissionDeniedSignalSchema = z.object({
6583
+ account: agentPluginProviderAccountSchema.optional(),
6584
+ acceptedPermissions: z.string().optional(),
6585
+ grant: sandboxEgressGrantSchema,
6586
+ message: z.string().min(1),
6587
+ provider: providerNameSchema,
6588
+ source: z.literal("upstream"),
6589
+ sso: z.string().optional(),
6590
+ status: httpStatusSchema,
6591
+ upstreamHost: z.string().min(1),
6592
+ upstreamPath: z.string().min(1),
6593
+ createdAtMs: finiteNumberSchema
6594
+ }).strict();
6595
+ function parseSandboxEgressAuthRequiredSignal(value) {
6596
+ const result = sandboxEgressAuthRequiredSignalSchema.safeParse(value);
6597
+ return result.success ? result.data : void 0;
6598
+ }
6599
+ function parseSandboxEgressPermissionDeniedSignal(value) {
6600
+ const result = sandboxEgressPermissionDeniedSignalSchema.safeParse(value);
6601
+ return result.success ? result.data : void 0;
6602
+ }
6603
+
6604
+ // src/chat/sandbox/egress-session.ts
6737
6605
  var SANDBOX_EGRESS_PROXY_PATH = "/api/internal/sandbox-egress";
6738
6606
  var SANDBOX_EGRESS_TOKEN_VERSION = "v1";
6739
6607
  var SANDBOX_EGRESS_HMAC_CONTEXT = "junior.sandbox_egress.v1";
6608
+ var SANDBOX_EGRESS_AUTH_SIGNAL_PREFIX = "sandbox-egress-auth-required";
6609
+ var SANDBOX_EGRESS_PERMISSION_SIGNAL_PREFIX = "sandbox-egress-permission-denied";
6740
6610
  var SANDBOX_EGRESS_LEASE_PREFIX = "sandbox-egress-lease";
6741
6611
  var DEFAULT_SESSION_TTL_MS = 30 * 60 * 1e3;
6742
- function leaseKey(provider, context) {
6612
+ function leaseKey(provider, grantName, context) {
6743
6613
  const actor = context.credentials.actor;
6744
6614
  const actorKey = actor.type === "user" ? `user:${actor.userId}` : `system:${actor.id}`;
6745
- return `${SANDBOX_EGRESS_LEASE_PREFIX}:${provider}:${actorKey}:${context.egressId}:${context.contextId}`;
6615
+ return `${SANDBOX_EGRESS_LEASE_PREFIX}:${provider}:${grantName}:${actorKey}:${context.egressId}:${context.contextId}`;
6746
6616
  }
6747
- function getSandboxEgressSecret() {
6748
- const secret = process.env.JUNIOR_SECRET?.trim();
6749
- if (secret) {
6750
- return secret;
6751
- }
6752
- throw new Error("Cannot determine sandbox egress secret (set JUNIOR_SECRET)");
6617
+ function authSignalKey(egressId, access) {
6618
+ return `${SANDBOX_EGRESS_AUTH_SIGNAL_PREFIX}:${egressId}:${access}`;
6619
+ }
6620
+ function permissionSignalKey(egressId, access) {
6621
+ return `${SANDBOX_EGRESS_PERMISSION_SIGNAL_PREFIX}:${egressId}:${access}`;
6753
6622
  }
6754
6623
  function base64Url(input) {
6755
6624
  return Buffer.from(input, "utf8").toString("base64url");
@@ -6769,49 +6638,32 @@ function timingSafeMatch(expected, actual) {
6769
6638
  return timingSafeEqual(expectedBuffer, actualBuffer);
6770
6639
  }
6771
6640
  function parseSandboxEgressContext(value) {
6772
- if (!value || typeof value !== "object") {
6641
+ const result = sandboxEgressCredentialContextSchema.safeParse(value);
6642
+ if (!result.success) {
6773
6643
  return void 0;
6774
6644
  }
6775
- const record = value;
6776
- const credentials = parseCredentialContext(record.credentials);
6777
- if (!credentials || typeof record.egressId !== "string" || !record.egressId || typeof record.expiresAtMs !== "number" || !Number.isFinite(record.expiresAtMs) || typeof record.contextId !== "string" || !record.contextId) {
6778
- return void 0;
6779
- }
6780
- if (record.expiresAtMs <= Date.now()) {
6645
+ if (result.data.expiresAtMs <= Date.now()) {
6781
6646
  return void 0;
6782
6647
  }
6783
- return {
6784
- credentials,
6785
- egressId: record.egressId,
6786
- expiresAtMs: record.expiresAtMs,
6787
- contextId: record.contextId
6788
- };
6648
+ return result.data;
6789
6649
  }
6790
6650
  function parseLease(value) {
6791
- if (!value || typeof value !== "object") {
6792
- return void 0;
6793
- }
6794
- const record = value;
6795
- if (typeof record.provider !== "string" || typeof record.expiresAt !== "string" || !Array.isArray(record.headerTransforms)) {
6651
+ const result = sandboxEgressCredentialLeaseSchema.safeParse(value);
6652
+ if (!result.success) {
6796
6653
  return void 0;
6797
6654
  }
6798
- const expiresAtMs = Date.parse(record.expiresAt);
6655
+ const expiresAtMs = Date.parse(result.data.expiresAt);
6799
6656
  if (!Number.isFinite(expiresAtMs) || expiresAtMs <= Date.now()) {
6800
6657
  return void 0;
6801
6658
  }
6802
- const headerTransforms = record.headerTransforms.filter(
6803
- (transform) => Boolean(
6804
- transform && typeof transform.domain === "string" && transform.headers && typeof transform.headers === "object"
6805
- )
6806
- );
6807
- if (headerTransforms.length === 0) {
6808
- return void 0;
6659
+ return result.data;
6660
+ }
6661
+ function getSandboxEgressSecret() {
6662
+ const secret = process.env.JUNIOR_SECRET?.trim();
6663
+ if (secret) {
6664
+ return secret;
6809
6665
  }
6810
- return {
6811
- provider: record.provider,
6812
- expiresAt: record.expiresAt,
6813
- headerTransforms
6814
- };
6666
+ throw new Error("Cannot determine sandbox egress secret (set JUNIOR_SECRET)");
6815
6667
  }
6816
6668
  function createSandboxEgressCredentialToken(input) {
6817
6669
  const ttlMs = Math.max(1, input.ttlMs ?? DEFAULT_SESSION_TTL_MS);
@@ -6861,17 +6713,94 @@ async function setSandboxEgressCredentialLease(context, lease) {
6861
6713
  );
6862
6714
  const state = getStateAdapter();
6863
6715
  await state.connect();
6864
- await state.set(leaseKey(lease.provider, context), lease, ttlMs);
6865
- }
6866
- async function getSandboxEgressCredentialLease(provider, context) {
6867
- const state = getStateAdapter();
6716
+ await state.set(
6717
+ leaseKey(lease.provider, lease.grant.name, context),
6718
+ lease,
6719
+ ttlMs
6720
+ );
6721
+ }
6722
+ async function getSandboxEgressCredentialLease(provider, grantName, context) {
6723
+ const state = getStateAdapter();
6724
+ await state.connect();
6725
+ return parseLease(await state.get(leaseKey(provider, grantName, context)));
6726
+ }
6727
+ async function clearSandboxEgressCredentialLease(provider, grantName, context) {
6728
+ const state = getStateAdapter();
6729
+ await state.connect();
6730
+ await state.delete(leaseKey(provider, grantName, context));
6731
+ }
6732
+ async function setSandboxEgressAuthRequiredSignal(context, signal) {
6733
+ const ttlMs = Math.max(1, context.expiresAtMs - Date.now());
6734
+ const state = getStateAdapter();
6735
+ await state.connect();
6736
+ await state.set(
6737
+ authSignalKey(context.egressId, signal.grant.access),
6738
+ {
6739
+ ...signal,
6740
+ createdAtMs: Date.now()
6741
+ },
6742
+ ttlMs
6743
+ );
6744
+ }
6745
+ async function setSandboxEgressPermissionDeniedSignal(context, signal) {
6746
+ const ttlMs = Math.max(1, context.expiresAtMs - Date.now());
6747
+ const state = getStateAdapter();
6748
+ await state.connect();
6749
+ await state.set(
6750
+ permissionSignalKey(context.egressId, signal.grant.access),
6751
+ {
6752
+ ...signal,
6753
+ createdAtMs: Date.now()
6754
+ },
6755
+ ttlMs
6756
+ );
6757
+ }
6758
+ async function clearSandboxEgressSignals(egressId) {
6759
+ if (!egressId) {
6760
+ return;
6761
+ }
6762
+ const state = getStateAdapter();
6763
+ await state.connect();
6764
+ await Promise.all([
6765
+ state.delete(authSignalKey(egressId, "read")),
6766
+ state.delete(authSignalKey(egressId, "write")),
6767
+ state.delete(permissionSignalKey(egressId, "read")),
6768
+ state.delete(permissionSignalKey(egressId, "write"))
6769
+ ]);
6770
+ }
6771
+ async function consumeSandboxEgressAuthRequiredSignal(egressId) {
6772
+ if (!egressId) {
6773
+ return void 0;
6774
+ }
6775
+ const state = getStateAdapter();
6868
6776
  await state.connect();
6869
- return parseLease(await state.get(leaseKey(provider, context)));
6777
+ const [writeSignal, readSignal] = await Promise.all([
6778
+ state.get(authSignalKey(egressId, "write")),
6779
+ state.get(authSignalKey(egressId, "read"))
6780
+ ]);
6781
+ const signal = parseSandboxEgressAuthRequiredSignal(writeSignal) ?? parseSandboxEgressAuthRequiredSignal(readSignal);
6782
+ await Promise.all([
6783
+ state.delete(authSignalKey(egressId, "read")),
6784
+ state.delete(authSignalKey(egressId, "write"))
6785
+ ]);
6786
+ return signal;
6870
6787
  }
6871
- async function clearSandboxEgressCredentialLease(provider, context) {
6788
+ async function consumeSandboxEgressPermissionDeniedSignal(egressId) {
6789
+ if (!egressId) {
6790
+ return void 0;
6791
+ }
6872
6792
  const state = getStateAdapter();
6873
6793
  await state.connect();
6874
- await state.delete(leaseKey(provider, context));
6794
+ const [writeSignal, readSignal] = await Promise.all([
6795
+ state.get(permissionSignalKey(egressId, "write")),
6796
+ state.get(permissionSignalKey(egressId, "read"))
6797
+ ]);
6798
+ const signal = parseSandboxEgressPermissionDeniedSignal(writeSignal) ?? parseSandboxEgressPermissionDeniedSignal(readSignal);
6799
+ await Promise.all([
6800
+ state.delete(permissionSignalKey(egressId, "read")),
6801
+ state.delete(permissionSignalKey(egressId, "write"))
6802
+ ]);
6803
+ return signal;
6875
6804
  }
6876
6805
 
6877
6806
  // src/chat/sandbox/egress-policy.ts
@@ -6929,7 +6858,7 @@ async function resolveSandboxCommandEnvironment() {
6929
6858
  )) {
6930
6859
  Object.assign(env, resolvePluginCommandEnv(plugin.manifest));
6931
6860
  const credentials = plugin.manifest.credentials;
6932
- if (credentials) {
6861
+ if (credentials?.authTokenEnv) {
6933
6862
  env[credentials.authTokenEnv] = resolveAuthTokenPlaceholder(credentials);
6934
6863
  }
6935
6864
  }
@@ -7829,10 +7758,6 @@ function createSandboxSessionManager(options) {
7829
7758
  let timeoutId;
7830
7759
  let onAbort;
7831
7760
  try {
7832
- if (input.signal?.aborted) {
7833
- return getCommandAbortedResult();
7834
- }
7835
- await refreshNetworkPolicy(sandboxInstance);
7836
7761
  if (input.signal?.aborted) {
7837
7762
  return getCommandAbortedResult();
7838
7763
  }
@@ -7943,6 +7868,9 @@ function createSandboxSessionManager(options) {
7943
7868
  getSandboxId() {
7944
7869
  return sandbox ? sandbox.sandboxId : sandboxIdHint;
7945
7870
  },
7871
+ getSandboxEgressId() {
7872
+ return sandbox?.sandboxEgressId;
7873
+ },
7946
7874
  getDependencyProfileHash() {
7947
7875
  return dependencyProfileHash;
7948
7876
  },
@@ -8075,6 +8003,8 @@ function createSandboxExecutor(options) {
8075
8003
  "app.sandbox.command_length": command.length
8076
8004
  });
8077
8005
  const executeBash = (await sessionManager.ensureToolExecutors()).bash;
8006
+ const activeEgressId = sessionManager.getSandboxEgressId();
8007
+ await clearSandboxEgressSignals(activeEgressId);
8078
8008
  const result = await withSandboxSpan(
8079
8009
  "bash",
8080
8010
  "process.exec",
@@ -8112,6 +8042,8 @@ function createSandboxExecutor(options) {
8112
8042
  }
8113
8043
  }
8114
8044
  );
8045
+ const authRequired = result.exitCode !== 0 ? await consumeSandboxEgressAuthRequiredSignal(activeEgressId) : void 0;
8046
+ const permissionDenied = result.exitCode !== 0 ? await consumeSandboxEgressPermissionDeniedSignal(activeEgressId) : void 0;
8115
8047
  return {
8116
8048
  result: {
8117
8049
  ok: result.exitCode === 0,
@@ -8123,7 +8055,9 @@ function createSandboxExecutor(options) {
8123
8055
  stdout: result.stdout,
8124
8056
  stderr: result.stderr,
8125
8057
  stdout_truncated: result.stdoutTruncated,
8126
- stderr_truncated: result.stderrTruncated
8058
+ stderr_truncated: result.stderrTruncated,
8059
+ ...authRequired ? { auth_required: authRequired } : {},
8060
+ ...permissionDenied ? { permission_denied: permissionDenied } : {}
8127
8061
  }
8128
8062
  };
8129
8063
  };
@@ -8544,13 +8478,95 @@ function toToolContentText(value) {
8544
8478
  return String(value);
8545
8479
  }
8546
8480
  }
8481
+ function isRecord3(value) {
8482
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
8483
+ }
8484
+ function stringField(record, key) {
8485
+ const value = record[key];
8486
+ return typeof value === "string" ? value : "";
8487
+ }
8488
+ function numberField(record, key) {
8489
+ const value = record[key];
8490
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
8491
+ }
8492
+ function stringListField(record, key) {
8493
+ const value = record[key];
8494
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
8495
+ }
8496
+ function accountText(value) {
8497
+ if (!isRecord3(value)) {
8498
+ return void 0;
8499
+ }
8500
+ const label = stringField(value, "label") || stringField(value, "id");
8501
+ const id = stringField(value, "id");
8502
+ if (!label) {
8503
+ return void 0;
8504
+ }
8505
+ return id && id !== label ? `${label} (${id})` : label;
8506
+ }
8507
+ function upstreamPermissionDeniedText(value) {
8508
+ if (!isRecord3(value) || !isRecord3(value.permission_denied)) {
8509
+ return void 0;
8510
+ }
8511
+ const signal = value.permission_denied;
8512
+ if (signal.source !== "upstream") {
8513
+ return void 0;
8514
+ }
8515
+ const provider = stringField(signal, "provider");
8516
+ const message = stringField(signal, "message");
8517
+ const upstreamHost = stringField(signal, "upstreamHost");
8518
+ const upstreamPath2 = stringField(signal, "upstreamPath");
8519
+ const status = numberField(signal, "status");
8520
+ if (!provider || !message || !upstreamHost || !upstreamPath2 || !status) {
8521
+ return void 0;
8522
+ }
8523
+ const grant = isRecord3(signal.grant) ? signal.grant : {};
8524
+ const grantName = stringField(grant, "name");
8525
+ const grantAccess = stringField(grant, "access");
8526
+ const grantReason = stringField(grant, "reason");
8527
+ const grantRequirements = stringListField(grant, "requirements");
8528
+ const account = accountText(signal.account);
8529
+ const command = stringField(value, "command");
8530
+ const stderr = stringField(value, "stderr").trim();
8531
+ const stdout = stringField(value, "stdout").trim();
8532
+ const acceptedPermissions = stringField(signal, "acceptedPermissions");
8533
+ const sso = stringField(signal, "sso");
8534
+ return [
8535
+ "Upstream permission denied.",
8536
+ message,
8537
+ "",
8538
+ `Provider: ${provider}`,
8539
+ ...account ? [`Provider account: ${account}`] : [],
8540
+ `Grant: ${grantName || "unknown"}${grantAccess ? ` (${grantAccess}${grantReason ? `, ${grantReason}` : ""})` : ""}`,
8541
+ ...grantRequirements.length > 0 ? [
8542
+ "Provider requirements:",
8543
+ ...grantRequirements.map((item) => `- ${item}`)
8544
+ ] : [],
8545
+ `Upstream: ${upstreamHost}${upstreamPath2}`,
8546
+ `Status: ${status}`,
8547
+ ...acceptedPermissions ? [`Accepted provider permissions: ${acceptedPermissions}`] : [],
8548
+ ...sso ? [`Provider SSO: ${sso}`] : [],
8549
+ ...command ? [`Command: ${command}`] : [],
8550
+ "",
8551
+ "Junior had a credential lease for this grant and forwarded the request. Do not diagnose this as a missing user token or a local Junior runtime block; diagnose provider-side permissions, installation scope, SSO, or requester-provider account access.",
8552
+ ...stderr ? ["", `stderr:
8553
+ ${stderr}`] : [],
8554
+ ...stdout ? ["", `stdout:
8555
+ ${stdout}`] : []
8556
+ ].join("\n");
8557
+ }
8547
8558
  function normalizeToolResult(result, isSandboxResult) {
8548
8559
  const unwrapped = isSandboxResult && result && typeof result === "object" && "result" in result ? result.result : result;
8549
8560
  if (isStructuredToolExecutionResult(unwrapped)) {
8550
8561
  return unwrapped;
8551
8562
  }
8552
8563
  return {
8553
- content: [{ type: "text", text: toToolContentText(unwrapped) }],
8564
+ content: [
8565
+ {
8566
+ type: "text",
8567
+ text: upstreamPermissionDeniedText(unwrapped) ?? toToolContentText(unwrapped)
8568
+ }
8569
+ ],
8554
8570
  details: unwrapped
8555
8571
  };
8556
8572
  }
@@ -8609,11 +8625,16 @@ function parseMcpAuthSession(value) {
8609
8625
  if (typeof parsed.authSessionId !== "string" || typeof parsed.provider !== "string" || typeof parsed.userId !== "string" || typeof parsed.conversationId !== "string" || typeof parsed.sessionId !== "string" || typeof parsed.userMessage !== "string" || typeof parsed.createdAtMs !== "number" || typeof parsed.updatedAtMs !== "number") {
8610
8626
  return void 0;
8611
8627
  }
8628
+ const destination = parsed.destination === void 0 ? void 0 : parseDestination(parsed.destination);
8629
+ if (parsed.destination !== void 0 && !destination) {
8630
+ return void 0;
8631
+ }
8612
8632
  return {
8613
8633
  authSessionId: parsed.authSessionId,
8614
8634
  provider: parsed.provider,
8615
8635
  userId: parsed.userId,
8616
8636
  conversationId: parsed.conversationId,
8637
+ ...destination ? { destination } : {},
8617
8638
  sessionId: parsed.sessionId,
8618
8639
  userMessage: parsed.userMessage,
8619
8640
  createdAtMs: parsed.createdAtMs,
@@ -8709,6 +8730,7 @@ async function patchMcpAuthSession(authSessionId, patch) {
8709
8730
  provider: current.provider,
8710
8731
  userId: current.userId,
8711
8732
  conversationId: current.conversationId,
8733
+ ...current.destination ? { destination: current.destination } : {},
8712
8734
  sessionId: current.sessionId,
8713
8735
  userMessage: current.userMessage,
8714
8736
  createdAtMs: current.createdAtMs,
@@ -8830,14 +8852,14 @@ function canReusePendingAuthLink(args) {
8830
8852
  if (!pendingAuth) {
8831
8853
  return false;
8832
8854
  }
8833
- return pendingAuth.kind === args.kind && pendingAuth.provider === args.provider && pendingAuth.requesterId === args.requesterId && pendingAuth.linkSentAtMs + AUTH_LINK_REUSE_WINDOW_MS > (args.nowMs ?? Date.now());
8855
+ return pendingAuth.kind === args.kind && pendingAuth.provider === args.provider && pendingAuth.requesterId === args.requesterId && pendingAuth.scope === args.scope && pendingAuth.linkSentAtMs + AUTH_LINK_REUSE_WINDOW_MS > (args.nowMs ?? Date.now());
8834
8856
  }
8835
8857
  function getConversationPendingAuth(args) {
8836
8858
  const pendingAuth = args.conversation.processing.pendingAuth;
8837
8859
  if (!pendingAuth) {
8838
8860
  return void 0;
8839
8861
  }
8840
- if (pendingAuth.kind !== args.kind || pendingAuth.provider !== args.provider || pendingAuth.requesterId !== args.requesterId) {
8862
+ if (pendingAuth.kind !== args.kind || pendingAuth.provider !== args.provider || pendingAuth.requesterId !== args.requesterId || pendingAuth.scope !== args.scope) {
8841
8863
  return void 0;
8842
8864
  }
8843
8865
  return pendingAuth;
@@ -8873,6 +8895,177 @@ function isPendingAuthLatestRequest(conversation, pendingAuth) {
8873
8895
  return false;
8874
8896
  }
8875
8897
 
8898
+ // src/chat/plugins/credential-hooks.ts
8899
+ import {
8900
+ agentPluginAuthorizationSchema as agentPluginAuthorizationSchema2,
8901
+ agentPluginCredentialResultSchema,
8902
+ agentPluginGrantSchema as agentPluginGrantSchema2,
8903
+ agentPluginProviderAccountSchema as agentPluginProviderAccountSchema2
8904
+ } from "@sentry/junior-plugin-api";
8905
+ function parseSchema(schema, value, message) {
8906
+ const result = schema.safeParse(value);
8907
+ if (!result.success) {
8908
+ throw new Error(message);
8909
+ }
8910
+ return result.data;
8911
+ }
8912
+ function parseAuthorization(value, pluginName) {
8913
+ if (value === void 0) {
8914
+ return void 0;
8915
+ }
8916
+ const authorization = parseSchema(
8917
+ agentPluginAuthorizationSchema2,
8918
+ value,
8919
+ `Plugin "${pluginName}" grant authorization is invalid`
8920
+ );
8921
+ if (authorization.provider !== pluginName) {
8922
+ throw new Error(
8923
+ `Plugin "${pluginName}" grant authorization provider must match the issuing plugin`
8924
+ );
8925
+ }
8926
+ return authorization;
8927
+ }
8928
+ function parseGrant(value, pluginName) {
8929
+ return parseSchema(
8930
+ agentPluginGrantSchema2,
8931
+ value,
8932
+ `Plugin "${pluginName}" grantForEgress returned an invalid grant`
8933
+ );
8934
+ }
8935
+ function agentPluginFor(provider) {
8936
+ return getAgentPlugins().find((candidate) => candidate.name === provider);
8937
+ }
8938
+ function parseCredentialResult(value, pluginName) {
8939
+ const result = parseSchema(
8940
+ agentPluginCredentialResultSchema,
8941
+ value,
8942
+ `Plugin "${pluginName}" issueCredential result is invalid`
8943
+ );
8944
+ if (result.type === "lease") {
8945
+ parseAuthorization(result.lease.authorization, pluginName);
8946
+ return result;
8947
+ }
8948
+ if (result.type === "unavailable") {
8949
+ return result;
8950
+ }
8951
+ parseAuthorization(result.authorization, pluginName);
8952
+ return result;
8953
+ }
8954
+ async function selectPluginGrant(input) {
8955
+ const plugin = agentPluginFor(input.provider);
8956
+ const hook = plugin?.hooks?.grantForEgress;
8957
+ if (!plugin || !hook) {
8958
+ return void 0;
8959
+ }
8960
+ const result = await hook({
8961
+ plugin: { name: plugin.name },
8962
+ log: createAgentPluginLogger(plugin.name),
8963
+ request: {
8964
+ ...input.bodyText !== void 0 ? { bodyText: input.bodyText } : {},
8965
+ method: input.method,
8966
+ url: input.upstreamUrl.toString()
8967
+ }
8968
+ });
8969
+ return result === void 0 ? void 0 : parseGrant(result, plugin.name);
8970
+ }
8971
+ async function onPluginEgressResponse(input) {
8972
+ const plugin = agentPluginFor(input.provider);
8973
+ const hook = plugin?.hooks?.onEgressResponse;
8974
+ if (!plugin || !hook) {
8975
+ return {};
8976
+ }
8977
+ let permissionDenied;
8978
+ await hook({
8979
+ plugin: { name: plugin.name },
8980
+ log: createAgentPluginLogger(plugin.name),
8981
+ grant: input.grant,
8982
+ permissionDenied(message) {
8983
+ const trimmed = message.trim();
8984
+ if (!trimmed) {
8985
+ throw new Error(
8986
+ `Plugin "${plugin.name}" onEgressResponse permissionDenied message is empty`
8987
+ );
8988
+ }
8989
+ permissionDenied = { message: trimmed };
8990
+ },
8991
+ request: {
8992
+ method: input.method,
8993
+ url: input.upstreamUrl.toString()
8994
+ },
8995
+ response: input.response
8996
+ });
8997
+ return permissionDenied ? { permissionDenied } : {};
8998
+ }
8999
+ function hasEgressCredentialHooks(provider) {
9000
+ const hooks = agentPluginFor(provider)?.hooks;
9001
+ return Boolean(hooks?.grantForEgress || hooks?.issueCredential);
9002
+ }
9003
+ async function resolvePluginOAuthAccount(input) {
9004
+ const plugin = agentPluginFor(input.provider);
9005
+ const hook = plugin?.hooks?.resolveOAuthAccount;
9006
+ if (!plugin || !hook) {
9007
+ return void 0;
9008
+ }
9009
+ const account = await hook({
9010
+ plugin: { name: plugin.name },
9011
+ log: createAgentPluginLogger(plugin.name),
9012
+ tokens: input.tokens
9013
+ });
9014
+ return account === void 0 ? void 0 : parseSchema(
9015
+ agentPluginProviderAccountSchema2,
9016
+ account,
9017
+ `Plugin "${plugin.name}" resolveOAuthAccount returned an invalid account`
9018
+ );
9019
+ }
9020
+ async function issuePluginCredential(input) {
9021
+ const plugin = agentPluginFor(input.provider);
9022
+ const hook = plugin?.hooks?.issueCredential;
9023
+ if (!plugin || !hook) {
9024
+ throw new Error(`Plugin "${input.provider}" has no issueCredential hook`);
9025
+ }
9026
+ const currentUserId = input.actor.type === "user" ? input.actor.userId : void 0;
9027
+ const credentialSubjectUserId = input.credentialSubject?.userId;
9028
+ const result = await hook({
9029
+ plugin: { name: plugin.name },
9030
+ log: createAgentPluginLogger(plugin.name),
9031
+ actor: input.actor,
9032
+ grant: input.grant,
9033
+ ...input.credentialSubject ? { credentialSubject: input.credentialSubject } : {},
9034
+ tokens: {
9035
+ ...currentUserId ? {
9036
+ currentUser: {
9037
+ userId: currentUserId,
9038
+ get: async () => await input.userTokenStore.get(currentUserId, plugin.name),
9039
+ set: async (tokens) => {
9040
+ await input.userTokenStore.set(
9041
+ currentUserId,
9042
+ plugin.name,
9043
+ tokens
9044
+ );
9045
+ }
9046
+ }
9047
+ } : {},
9048
+ ...credentialSubjectUserId ? {
9049
+ credentialSubject: {
9050
+ userId: credentialSubjectUserId,
9051
+ get: async () => await input.userTokenStore.get(
9052
+ credentialSubjectUserId,
9053
+ plugin.name
9054
+ ),
9055
+ set: async (tokens) => {
9056
+ await input.userTokenStore.set(
9057
+ credentialSubjectUserId,
9058
+ plugin.name,
9059
+ tokens
9060
+ );
9061
+ }
9062
+ }
9063
+ } : {}
9064
+ }
9065
+ });
9066
+ return parseCredentialResult(result, plugin.name);
9067
+ }
9068
+
8876
9069
  // src/chat/services/plugin-auth-orchestration.ts
8877
9070
  var PluginAuthorizationPauseError = class extends AuthorizationPauseError {
8878
9071
  constructor(provider, disposition) {
@@ -8901,7 +9094,6 @@ ${typeof result.stderr === "string" ? result.stderr : ""}`.toLowerCase();
8901
9094
  return false;
8902
9095
  }
8903
9096
  return [
8904
- /\bjunior-auth-required\b/,
8905
9097
  /\b401\b/,
8906
9098
  /\bunauthorized\b/,
8907
9099
  /\bbad credentials\b/,
@@ -8923,18 +9115,20 @@ function commandText(details) {
8923
9115
  return `${typeof result.stdout === "string" ? result.stdout : ""}
8924
9116
  ${typeof result.stderr === "string" ? result.stderr : ""}`;
8925
9117
  }
8926
- function isGitHubSmartHttpAuthFailure(provider, command, details) {
8927
- if (provider !== "github" || !/^\s*(?:gh|git)\b/i.test(command)) {
8928
- return false;
9118
+ function pluginAuthRequiredSignal(details) {
9119
+ if (!details || typeof details !== "object") {
9120
+ return void 0;
8929
9121
  }
8930
- const text = commandText(details).toLowerCase();
8931
- return /\bgzip:\s*invalid header\b/.test(text);
8932
- }
8933
- function explicitAuthRequiredProvider(details) {
8934
- const match = /\bjunior-auth-required\s+provider=([a-z0-9-]+)\b/.exec(
8935
- commandText(details).toLowerCase()
8936
- );
8937
- return match?.[1];
9122
+ const signal = details.auth_required;
9123
+ const parsedSignal = parseSandboxEgressAuthRequiredSignal(signal);
9124
+ if (!parsedSignal) {
9125
+ return void 0;
9126
+ }
9127
+ return {
9128
+ provider: parsedSignal.provider,
9129
+ grant: parsedSignal.grant,
9130
+ ...parsedSignal.authorization ? { authorization: parsedSignal.authorization } : {}
9131
+ };
8938
9132
  }
8939
9133
  function registeredProviderNames() {
8940
9134
  const providers = /* @__PURE__ */ new Set();
@@ -8954,15 +9148,14 @@ function commandTargetsProvider(provider, command, details) {
8954
9148
  if (!normalizedCommand) {
8955
9149
  return false;
8956
9150
  }
8957
- if (provider === "github" && /^(gh|git)\b/.test(normalizedCommand)) {
8958
- return true;
8959
- }
8960
9151
  const plugin = getPluginDefinition(provider);
8961
9152
  const candidates = /* @__PURE__ */ new Set([provider.toLowerCase()]);
8962
9153
  const manifest = plugin?.manifest;
8963
9154
  const credentials = manifest?.credentials;
8964
9155
  if (credentials) {
8965
- candidates.add(credentials.authTokenEnv.toLowerCase());
9156
+ if (credentials.authTokenEnv) {
9157
+ candidates.add(credentials.authTokenEnv.toLowerCase());
9158
+ }
8966
9159
  for (const domain of credentials.domains) {
8967
9160
  candidates.add(domain.toLowerCase());
8968
9161
  }
@@ -8982,14 +9175,11 @@ function authorizationId(args) {
8982
9175
  return `${args.sessionId}:${args.kind}:${args.provider}`;
8983
9176
  }
8984
9177
  function buildCredentialFailureError(provider, command) {
8985
- const providerLabel = provider === "github" ? "GitHub" : formatProviderLabel(provider);
8986
- const plugin = getPluginDefinition(provider);
8987
- const credentialType = plugin?.manifest.credentials?.type;
9178
+ const providerLabel = formatProviderLabel(provider);
8988
9179
  const commandSummary = formatCommand(command);
8989
- const remediation = provider === "github" && credentialType === "github-app" ? "Verify the GitHub App installation covers the target repository and the host GitHub App environment variables are current." : `Verify the ${providerLabel} provider credentials before retrying.`;
8990
9180
  return new PluginCredentialFailureError(
8991
9181
  provider,
8992
- `${providerLabel} credentials were rejected while running \`${commandSummary}\`. ${remediation}`
9182
+ `${providerLabel} credentials were rejected while running \`${commandSummary}\`. Verify the ${providerLabel} provider credentials before retrying.`
8993
9183
  );
8994
9184
  }
8995
9185
  function createPluginAuthOrchestration(deps, abortAgent) {
@@ -9009,16 +9199,19 @@ function createPluginAuthOrchestration(deps, abortAgent) {
9009
9199
  pendingAuth: deps.currentPendingAuth,
9010
9200
  kind: "plugin",
9011
9201
  provider,
9012
- requesterId: deps.requesterId
9202
+ requesterId: deps.requesterId,
9203
+ ...options?.scope ? { scope: options.scope } : {}
9013
9204
  });
9014
9205
  if (!reusingPendingLink) {
9015
9206
  const oauthResult = await startOAuthFlow(provider, {
9016
9207
  requesterId: deps.requesterId,
9017
9208
  channelId: deps.channelId,
9209
+ destination: deps.destination,
9018
9210
  threadTs: deps.threadTs,
9019
9211
  userMessage: deps.userMessage,
9020
9212
  channelConfiguration: deps.channelConfiguration,
9021
9213
  activeSkillName: activeSkill?.name ?? void 0,
9214
+ ...options?.scope ? { scope: options.scope } : {},
9022
9215
  resumeConversationId: deps.conversationId,
9023
9216
  resumeSessionId: deps.sessionId
9024
9217
  });
@@ -9039,6 +9232,7 @@ function createPluginAuthOrchestration(deps, abortAgent) {
9039
9232
  kind: "plugin",
9040
9233
  provider,
9041
9234
  requesterId: deps.requesterId,
9235
+ ...options?.scope ? { scope: options.scope } : {},
9042
9236
  sessionId: deps.sessionId,
9043
9237
  linkSentAtMs: reusingPendingLink ? deps.currentPendingAuth.linkSentAtMs : Date.now()
9044
9238
  });
@@ -9068,8 +9262,9 @@ function createPluginAuthOrchestration(deps, abortAgent) {
9068
9262
  return {
9069
9263
  handleCommandFailure: async (input) => {
9070
9264
  const providers = registeredProviderNames();
9071
- const explicitProvider = explicitAuthRequiredProvider(input.details);
9072
- const provider = explicitProvider && providers.includes(explicitProvider) ? explicitProvider : providers.find(
9265
+ const parsedAuthSignal = pluginAuthRequiredSignal(input.details);
9266
+ const authSignal = parsedAuthSignal && providers.includes(parsedAuthSignal.provider) ? parsedAuthSignal : void 0;
9267
+ const provider = authSignal ? authSignal.provider : providers.find(
9073
9268
  (availableProvider) => commandTargetsProvider(
9074
9269
  availableProvider,
9075
9270
  input.command,
@@ -9079,20 +9274,30 @@ function createPluginAuthOrchestration(deps, abortAgent) {
9079
9274
  if (!provider) {
9080
9275
  return;
9081
9276
  }
9082
- const authFailure = isCommandAuthFailure(input.details) || isGitHubSmartHttpAuthFailure(provider, input.command, input.details);
9277
+ const authFailure = Boolean(authSignal) || isCommandAuthFailure(input.details);
9083
9278
  if (!authFailure) {
9084
9279
  return;
9085
9280
  }
9281
+ const providerOAuth = getPluginOAuthConfig(provider);
9282
+ const authorization = authSignal?.authorization ?? (!authSignal && !hasEgressCredentialHooks(provider) && providerOAuth ? {
9283
+ type: "oauth",
9284
+ provider,
9285
+ ...providerOAuth.scope ? { scope: providerOAuth.scope } : {}
9286
+ } : void 0);
9086
9287
  if (!deps.requesterId || !deps.userTokenStore) {
9087
9288
  if (deps.authorizationFlowMode === "disabled") {
9088
9289
  throw new AuthorizationFlowDisabledError("plugin", provider);
9089
9290
  }
9090
9291
  throw buildCredentialFailureError(provider, input.command);
9091
9292
  }
9092
- if (!getPluginOAuthConfig(provider)) {
9293
+ if (authorization?.type !== "oauth") {
9294
+ throw buildCredentialFailureError(provider, input.command);
9295
+ }
9296
+ if (!getPluginOAuthConfig(authorization.provider)) {
9093
9297
  throw buildCredentialFailureError(provider, input.command);
9094
9298
  }
9095
- await startAuthorizationPause(provider, input.activeSkill, {
9299
+ await startAuthorizationPause(authorization.provider, input.activeSkill, {
9300
+ ...authorization.scope ? { scope: authorization.scope } : {},
9096
9301
  unlinkExistingProvider: true
9097
9302
  });
9098
9303
  },
@@ -9579,6 +9784,7 @@ function coercePendingAuthState(value) {
9579
9784
  const kind = value.kind;
9580
9785
  const provider = toOptionalString(value.provider);
9581
9786
  const requesterId = toOptionalString(value.requesterId);
9787
+ const scope = toOptionalString(value.scope);
9582
9788
  const sessionId = toOptionalString(value.sessionId);
9583
9789
  const linkSentAtMs = toOptionalNumber(value.linkSentAtMs);
9584
9790
  if (kind !== "mcp" && kind !== "plugin" || !provider || !requesterId || !sessionId || typeof linkSentAtMs !== "number") {
@@ -9588,6 +9794,7 @@ function coercePendingAuthState(value) {
9588
9794
  kind,
9589
9795
  provider,
9590
9796
  requesterId,
9797
+ ...scope ? { scope } : {},
9591
9798
  sessionId,
9592
9799
  linkSentAtMs
9593
9800
  };
@@ -10170,29 +10377,8 @@ function buildTurnResult(input) {
10170
10377
  };
10171
10378
  }
10172
10379
 
10173
- // src/chat/services/provider-retry.ts
10174
- var RETRYABLE_PROVIDER_ERROR_PATTERN = /overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|stream ended before message_stop|http2 request did not get a response|timed? out|timeout|terminated|retry delay/i;
10175
- var NON_RETRYABLE_PROVIDER_ERROR_PATTERN = /invalid.?api.?key|authentication|authorization|permission|forbidden|context.?length|context.?window|content.?policy|validation|bad request|400|401|403/i;
10176
- function isRetryableProviderError(message) {
10177
- if (message?.stopReason !== "error" || !message.errorMessage) {
10178
- return false;
10179
- }
10180
- if (NON_RETRYABLE_PROVIDER_ERROR_PATTERN.test(message.errorMessage)) {
10181
- return false;
10182
- }
10183
- return RETRYABLE_PROVIDER_ERROR_PATTERN.test(message.errorMessage);
10184
- }
10185
- function trimRetryableProviderErrorTail(messages) {
10186
- const trimmed = trimTrailingAssistantMessages(messages);
10187
- if (trimmed.length === messages.length) {
10188
- return void 0;
10189
- }
10190
- const tailRole = getPiMessageRole(trimmed.at(-1));
10191
- return tailRole === "user" || tailRole === "toolResult" ? trimmed : void 0;
10192
- }
10193
-
10194
10380
  // src/chat/services/turn-thinking-level.ts
10195
- import { z } from "zod";
10381
+ import { z as z2 } from "zod";
10196
10382
  var CLASSIFIER_CONFIDENCE_THRESHOLD = 0.75;
10197
10383
  var MAX_ROUTER_CONTEXT_CHARS = 8e3;
10198
10384
  var ROUTER_CONTEXT_HEAD_CHARS = 3e3;
@@ -10224,13 +10410,13 @@ function coerceClassifierConfidence(value) {
10224
10410
  }
10225
10411
  return CONFIDENCE_LABELS[trimmed] ?? value;
10226
10412
  }
10227
- var turnExecutionProfileSchema = z.object({
10228
- thinking_level: z.enum(TURN_THINKING_LEVELS),
10229
- confidence: z.preprocess(
10413
+ var turnExecutionProfileSchema = z2.object({
10414
+ thinking_level: z2.enum(TURN_THINKING_LEVELS),
10415
+ confidence: z2.preprocess(
10230
10416
  coerceClassifierConfidence,
10231
- z.number().min(0).max(1)
10417
+ z2.number().min(0).max(1)
10232
10418
  ),
10233
- reason: z.string().min(1)
10419
+ reason: z2.string().min(1)
10234
10420
  });
10235
10421
  var DEFAULT_THINKING_LEVEL = "medium";
10236
10422
  var THINKING_LEVEL_RANK = {
@@ -10546,6 +10732,7 @@ async function persistRunningSessionRecord(args) {
10546
10732
  conversationId: args.conversationId,
10547
10733
  cumulativeDurationMs: latestSessionRecord?.cumulativeDurationMs,
10548
10734
  cumulativeUsage: latestSessionRecord?.cumulativeUsage,
10735
+ ...args.destination ?? latestSessionRecord?.destination ? { destination: args.destination ?? latestSessionRecord?.destination } : {},
10549
10736
  sessionId: args.sessionId,
10550
10737
  sliceId: args.sliceId,
10551
10738
  state: "running",
@@ -10586,6 +10773,7 @@ async function persistCompletedSessionRecord(args) {
10586
10773
  latestSessionRecord?.cumulativeUsage,
10587
10774
  args.currentUsage
10588
10775
  ),
10776
+ ...args.destination ?? latestSessionRecord?.destination ? { destination: args.destination ?? latestSessionRecord?.destination } : {},
10589
10777
  sessionId: args.sessionId,
10590
10778
  sliceId: args.sliceId,
10591
10779
  state: "completed",
@@ -10632,6 +10820,7 @@ async function persistAuthPauseSessionRecord(args) {
10632
10820
  latestSessionRecord?.cumulativeUsage,
10633
10821
  args.currentUsage
10634
10822
  ),
10823
+ ...args.destination ?? latestSessionRecord?.destination ? { destination: args.destination ?? latestSessionRecord?.destination } : {},
10635
10824
  sessionId: args.sessionId,
10636
10825
  sliceId: nextSliceId,
10637
10826
  state: "awaiting_resume",
@@ -10688,6 +10877,9 @@ async function persistTimeoutSessionRecord(args) {
10688
10877
  conversationId: args.conversationId,
10689
10878
  cumulativeDurationMs,
10690
10879
  cumulativeUsage,
10880
+ ...args.destination ?? latestSessionRecord?.destination ? {
10881
+ destination: args.destination ?? latestSessionRecord?.destination
10882
+ } : {},
10691
10883
  sessionId: args.sessionId,
10692
10884
  sliceId: args.currentSliceId,
10693
10885
  state: "failed",
@@ -10706,6 +10898,7 @@ async function persistTimeoutSessionRecord(args) {
10706
10898
  conversationId: args.conversationId,
10707
10899
  cumulativeDurationMs,
10708
10900
  cumulativeUsage,
10901
+ ...args.destination ?? latestSessionRecord?.destination ? { destination: args.destination ?? latestSessionRecord?.destination } : {},
10709
10902
  sessionId: args.sessionId,
10710
10903
  sliceId: nextSliceId,
10711
10904
  state: "awaiting_resume",
@@ -10756,6 +10949,7 @@ async function persistYieldSessionRecord(args) {
10756
10949
  latestSessionRecord?.cumulativeUsage,
10757
10950
  args.currentUsage
10758
10951
  ),
10952
+ ...args.destination ?? latestSessionRecord?.destination ? { destination: args.destination ?? latestSessionRecord?.destination } : {},
10759
10953
  sessionId: args.sessionId,
10760
10954
  sliceId: args.currentSliceId,
10761
10955
  state: "awaiting_resume",
@@ -10971,6 +11165,7 @@ async function createMcpOAuthClientProvider(input) {
10971
11165
  provider: input.provider,
10972
11166
  userId: input.userId,
10973
11167
  conversationId: input.conversationId,
11168
+ ...input.destination ? { destination: input.destination } : {},
10974
11169
  sessionId: input.sessionId,
10975
11170
  userMessage: input.userMessage,
10976
11171
  ...input.channelId ? { channelId: input.channelId } : {},
@@ -10990,6 +11185,7 @@ async function createMcpOAuthClientProvider(input) {
10990
11185
  provider: input.provider,
10991
11186
  userId: input.userId,
10992
11187
  conversationId: input.conversationId,
11188
+ ...input.destination ? { destination: input.destination } : {},
10993
11189
  sessionId: input.sessionId,
10994
11190
  userMessage: input.userMessage,
10995
11191
  ...input.channelId ? { channelId: input.channelId } : {},
@@ -11065,6 +11261,7 @@ function createMcpAuthOrchestration(deps, abortAgent) {
11065
11261
  const provider = await createMcpOAuthClientProvider({
11066
11262
  provider: plugin.manifest.name,
11067
11263
  conversationId: deps.conversationId,
11264
+ destination: deps.destination,
11068
11265
  sessionId: deps.sessionId,
11069
11266
  userId: deps.requesterId,
11070
11267
  userMessage: deps.userMessage,
@@ -11161,7 +11358,6 @@ function createMcpAuthOrchestration(deps, abortAgent) {
11161
11358
  }
11162
11359
 
11163
11360
  // src/chat/respond.ts
11164
- var PROVIDER_RETRY_DELAYS_MS = [1e3, 2e3];
11165
11361
  var AGENT_ABORT_SETTLE_GRACE_MS = 5e3;
11166
11362
  function sleep2(ms) {
11167
11363
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -11652,6 +11848,7 @@ async function generateAssistantReply(messageText2, context = {}) {
11652
11848
  sessionId,
11653
11849
  requesterId: authRequesterId,
11654
11850
  channelId: context.correlation?.channelId,
11851
+ destination: context.destination,
11655
11852
  threadTs: context.correlation?.threadTs,
11656
11853
  toolChannelId: context.toolChannelId,
11657
11854
  userMessage: userInput,
@@ -11670,6 +11867,7 @@ async function generateAssistantReply(messageText2, context = {}) {
11670
11867
  sessionId,
11671
11868
  requesterId: authRequesterId,
11672
11869
  channelId: context.correlation?.channelId,
11870
+ destination: context.destination,
11673
11871
  threadTs: context.correlation?.threadTs,
11674
11872
  userMessage: userInput,
11675
11873
  channelConfiguration: context.channelConfiguration,
@@ -11696,8 +11894,6 @@ async function generateAssistantReply(messageText2, context = {}) {
11696
11894
  assistantUserName: botConfig.userName,
11697
11895
  modelId: botConfig.modelId
11698
11896
  });
11699
- const toolChannelId = context.toolChannelId ?? context.correlation?.channelId;
11700
- const channelCapabilities = resolveChannelCapabilities(toolChannelId);
11701
11897
  const loadableSkills = availableSkills.filter(
11702
11898
  (skill) => skill.disableModelInvocation !== true || skill.name === invokedSkill?.name
11703
11899
  );
@@ -11748,8 +11944,10 @@ async function generateAssistantReply(messageText2, context = {}) {
11748
11944
  }
11749
11945
  },
11750
11946
  {
11751
- channelId: toolChannelId,
11752
- channelCapabilities,
11947
+ channelId: context.correlation?.channelId,
11948
+ conversationId: sessionConversationId,
11949
+ deliveryChannelId: context.toolChannelId,
11950
+ destination: context.destination,
11753
11951
  requester: actorRequester,
11754
11952
  teamId: context.correlation?.teamId,
11755
11953
  messageTs: context.correlation?.messageTs,
@@ -11889,6 +12087,7 @@ async function generateAssistantReply(messageText2, context = {}) {
11889
12087
  const persisted = await persistRunningSessionRecord({
11890
12088
  channelName: context.correlation?.channelName,
11891
12089
  conversationId: sessionConversationId,
12090
+ destination: context.destination,
11892
12091
  sessionId,
11893
12092
  sliceId: currentSliceId,
11894
12093
  messages,
@@ -12154,26 +12353,24 @@ async function generateAssistantReply(messageText2, context = {}) {
12154
12353
  throw getPendingAuthPause();
12155
12354
  }
12156
12355
  const lastAssistant = outputMessages.at(-1);
12157
- const retryDelayMs = PROVIDER_RETRY_DELAYS_MS[attempt];
12158
- if (retryDelayMs === void 0 || !isRetryableProviderError(lastAssistant)) {
12356
+ const providerRetry = nextProviderRetry({
12357
+ attempt,
12358
+ lastAssistant,
12359
+ messages: agent.state.messages
12360
+ });
12361
+ if (!providerRetry) {
12159
12362
  break;
12160
12363
  }
12161
12364
  retryUsage = turnUsage;
12162
- const retryMessages = trimRetryableProviderErrorTail(
12163
- agent.state.messages
12164
- );
12165
- if (!retryMessages) {
12166
- break;
12167
- }
12168
- agent.state.messages = retryMessages;
12169
- await persistSafeBoundary(retryMessages);
12365
+ agent.state.messages = providerRetry.messages;
12366
+ await persistSafeBoundary(providerRetry.messages);
12170
12367
  logWarn(
12171
12368
  "agent_turn_provider_retry",
12172
12369
  spanContext,
12173
12370
  {},
12174
12371
  "Retrying transient provider failure"
12175
12372
  );
12176
- await sleep2(retryDelayMs);
12373
+ await sleep2(providerRetry.delayMs);
12177
12374
  run = agent.continue();
12178
12375
  }
12179
12376
  },
@@ -12203,6 +12400,7 @@ async function generateAssistantReply(messageText2, context = {}) {
12203
12400
  conversationId: sessionConversationId,
12204
12401
  currentDurationMs: Date.now() - replyStartedAtMs,
12205
12402
  currentUsage: turnUsage,
12403
+ destination: context.destination,
12206
12404
  sessionId,
12207
12405
  sliceId: currentSliceId,
12208
12406
  allMessages: agent.state.messages,
@@ -12236,6 +12434,7 @@ async function generateAssistantReply(messageText2, context = {}) {
12236
12434
  const sessionRecord = await persistYieldSessionRecord({
12237
12435
  channelName: context.correlation?.channelName,
12238
12436
  conversationId: timeoutResumeConversationId,
12437
+ destination: context.destination,
12239
12438
  sessionId: timeoutResumeSessionId,
12240
12439
  currentSliceId: timeoutResumeSliceId,
12241
12440
  currentDurationMs: Date.now() - replyStartedAtMs,
@@ -12260,6 +12459,7 @@ async function generateAssistantReply(messageText2, context = {}) {
12260
12459
  const sessionRecord = await persistTimeoutSessionRecord({
12261
12460
  channelName: context.correlation?.channelName,
12262
12461
  conversationId: timeoutResumeConversationId,
12462
+ destination: context.destination,
12263
12463
  sessionId: timeoutResumeSessionId,
12264
12464
  currentSliceId: timeoutResumeSliceId,
12265
12465
  currentDurationMs: Date.now() - replyStartedAtMs,
@@ -12303,6 +12503,7 @@ async function generateAssistantReply(messageText2, context = {}) {
12303
12503
  const sessionRecord = await persistAuthPauseSessionRecord({
12304
12504
  channelName: context.correlation?.channelName,
12305
12505
  conversationId: timeoutResumeConversationId,
12506
+ destination: context.destination,
12306
12507
  sessionId: timeoutResumeSessionId,
12307
12508
  currentSliceId: timeoutResumeSliceId,
12308
12509
  currentDurationMs: Date.now() - replyStartedAtMs,
@@ -12335,6 +12536,9 @@ async function generateAssistantReply(messageText2, context = {}) {
12335
12536
  if (isRetryableTurnError(error)) {
12336
12537
  throw error;
12337
12538
  }
12539
+ if (isProviderRetryError(error)) {
12540
+ throw error;
12541
+ }
12338
12542
  if (isTurnInputCommitLostError(error)) {
12339
12543
  throw error;
12340
12544
  }
@@ -13136,11 +13340,80 @@ async function verifyDispatchCallbackRequest(request) {
13136
13340
 
13137
13341
  // src/chat/agent-dispatch/store.ts
13138
13342
  import { createHash } from "crypto";
13343
+ import {
13344
+ agentPluginCredentialSubjectSchema,
13345
+ destinationSchema
13346
+ } from "@sentry/junior-plugin-api";
13347
+ import { z as z3 } from "zod";
13139
13348
  var DISPATCH_PREFIX = "junior:agent_dispatch";
13140
13349
  var DISPATCH_LOCK_TTL_MS = 10 * 60 * 1e3;
13141
13350
  var DISPATCH_INDEX_LOCK_TTL_MS = 1e4;
13142
13351
  var DISPATCH_INDEX_MAX_LENGTH = 1e4;
13143
13352
  var DEFAULT_MAX_ATTEMPTS = 5;
13353
+ var nonEmptyExactStringSchema = z3.string().min(1).refine(
13354
+ (value) => value === value.trim() && value.toLowerCase() !== "unknown"
13355
+ );
13356
+ var dispatchStatusSchema = z3.enum([
13357
+ "pending",
13358
+ "running",
13359
+ "awaiting_resume",
13360
+ "completed",
13361
+ "failed",
13362
+ "blocked"
13363
+ ]);
13364
+ var dispatchActorSchema = z3.object({
13365
+ type: z3.literal("system"),
13366
+ id: nonEmptyExactStringSchema
13367
+ }).strict();
13368
+ var credentialSubjectBindingSchema = z3.object({
13369
+ type: z3.literal("slack-direct-conversation"),
13370
+ teamId: z3.string().min(1),
13371
+ channelId: z3.string().min(1),
13372
+ signature: z3.string().min(1)
13373
+ }).strict();
13374
+ var boundCredentialSubjectSchema = agentPluginCredentialSubjectSchema.extend({
13375
+ binding: credentialSubjectBindingSchema
13376
+ }).strict();
13377
+ var dispatchRecordSchema = z3.object({
13378
+ actor: dispatchActorSchema,
13379
+ attempt: z3.number().int().nonnegative(),
13380
+ createdAtMs: z3.number().finite(),
13381
+ credentialSubject: boundCredentialSubjectSchema.optional(),
13382
+ destination: destinationSchema,
13383
+ errorMessage: z3.string().optional(),
13384
+ id: nonEmptyExactStringSchema,
13385
+ idempotencyKey: z3.string().min(1),
13386
+ input: z3.string().min(1),
13387
+ lastCallbackAtMs: z3.number().finite().optional(),
13388
+ leaseExpiresAtMs: z3.number().finite().optional(),
13389
+ maxAttempts: z3.number().int().positive(),
13390
+ metadata: z3.record(z3.string(), z3.string()).optional(),
13391
+ plugin: nonEmptyExactStringSchema,
13392
+ resultMessageTs: z3.string().optional(),
13393
+ status: dispatchStatusSchema,
13394
+ updatedAtMs: z3.number().finite(),
13395
+ version: z3.number().int().positive()
13396
+ }).strict().superRefine((record, ctx) => {
13397
+ const subject = record.credentialSubject;
13398
+ if (!subject) {
13399
+ return;
13400
+ }
13401
+ if (!record.destination.channelId.startsWith("D")) {
13402
+ ctx.addIssue({
13403
+ code: z3.ZodIssueCode.custom,
13404
+ message: "Dispatch credentialSubject requires a private direct Slack destination",
13405
+ path: ["credentialSubject"]
13406
+ });
13407
+ return;
13408
+ }
13409
+ if (subject.binding.teamId !== record.destination.teamId || subject.binding.channelId !== record.destination.channelId) {
13410
+ ctx.addIssue({
13411
+ code: z3.ZodIssueCode.custom,
13412
+ message: "Dispatch credentialSubject binding must match destination",
13413
+ path: ["credentialSubject", "binding"]
13414
+ });
13415
+ }
13416
+ });
13144
13417
  function getDispatchStorageKey(id) {
13145
13418
  return `${DISPATCH_PREFIX}:record:${id}`;
13146
13419
  }
@@ -13166,8 +13439,12 @@ function buildDispatchId(plugin, idempotencyKey) {
13166
13439
  const digest = createHash("sha256").update(plugin).update("\0").update(idempotencyKey).digest("hex").slice(0, 32);
13167
13440
  return `dispatch_${digest}`;
13168
13441
  }
13442
+ function parseDispatchRecord(value) {
13443
+ const parsed = dispatchRecordSchema.safeParse(value);
13444
+ return parsed.success ? parsed.data : void 0;
13445
+ }
13169
13446
  function getDispatchDestinationLockId(destination) {
13170
- return `slack:${destination.teamId}:${destination.channelId}`;
13447
+ return destinationKey(destination);
13171
13448
  }
13172
13449
  function getDispatchConversationId(dispatch) {
13173
13450
  return `agent-dispatch:${dispatch.id}`;
@@ -13234,22 +13511,28 @@ async function syncIncompleteDispatchIndex(state, record) {
13234
13511
  });
13235
13512
  }
13236
13513
  async function putRecord(state, record) {
13514
+ const next = parseDispatchRecord(record);
13515
+ if (!next) {
13516
+ throw new Error("Dispatch record is invalid.");
13517
+ }
13237
13518
  await state.set(
13238
- getDispatchStorageKey(record.id),
13239
- record,
13519
+ getDispatchStorageKey(next.id),
13520
+ next,
13240
13521
  JUNIOR_THREAD_STATE_TTL_MS
13241
13522
  );
13242
- await syncIncompleteDispatchIndex(state, record);
13523
+ await syncIncompleteDispatchIndex(state, next);
13243
13524
  }
13244
13525
  async function getDispatchRecord(id) {
13245
13526
  const state = getStateAdapter();
13246
13527
  await state.connect();
13247
- return await state.get(getDispatchStorageKey(id)) ?? void 0;
13528
+ return parseDispatchRecord(await state.get(getDispatchStorageKey(id)));
13248
13529
  }
13249
13530
  async function createOrGetDispatch(args) {
13250
13531
  const id = buildDispatchId(args.plugin, args.options.idempotencyKey);
13251
13532
  return await withDispatchLock(id, async (state) => {
13252
- const existing = await state.get(getDispatchStorageKey(id)) ?? void 0;
13533
+ const existing = parseDispatchRecord(
13534
+ await state.get(getDispatchStorageKey(id))
13535
+ );
13253
13536
  if (existing) {
13254
13537
  return { record: existing, status: "already_exists" };
13255
13538
  }
@@ -13344,9 +13627,12 @@ async function persistRuntimePatch(args) {
13344
13627
  }
13345
13628
  async function markDispatch(args) {
13346
13629
  return await withDispatchLock(args.dispatch.id, async (state) => {
13347
- const current = await state.get(
13348
- getDispatchStorageKey(args.dispatch.id)
13349
- ) ?? args.dispatch;
13630
+ const current = parseDispatchRecord(
13631
+ await state.get(getDispatchStorageKey(args.dispatch.id))
13632
+ );
13633
+ if (!current) {
13634
+ throw new Error("Dispatch record is missing or invalid.");
13635
+ }
13350
13636
  return await updateDispatchRecord(state, {
13351
13637
  ...current,
13352
13638
  status: args.status,
@@ -13372,7 +13658,9 @@ async function runAgentDispatchSlice(callback, deps = {}) {
13372
13658
  const scheduleCallback = deps.scheduleCallback ?? scheduleDispatchCallback;
13373
13659
  const nowMs = Date.now();
13374
13660
  const claimedDispatch = await withDispatchLock(callback.id, async (state) => {
13375
- const current = await state.get(getDispatchStorageKey(callback.id)) ?? void 0;
13661
+ const current = parseDispatchRecord(
13662
+ await state.get(getDispatchStorageKey(callback.id))
13663
+ );
13376
13664
  if (!current || !canClaimDispatch(current, nowMs) || current.version !== callback.expectedVersion) {
13377
13665
  return void 0;
13378
13666
  }
@@ -13407,10 +13695,10 @@ async function runAgentDispatchSlice(callback, deps = {}) {
13407
13695
  const startedDispatch = await withDispatchLock(
13408
13696
  dispatch.id,
13409
13697
  async (state) => {
13410
- const current = await state.get(
13411
- getDispatchStorageKey(dispatch.id)
13412
- ) ?? dispatch;
13413
- if (current.status !== "running" || current.version !== dispatch.version || current.attempt >= current.maxAttempts) {
13698
+ const current = parseDispatchRecord(
13699
+ await state.get(getDispatchStorageKey(dispatch.id))
13700
+ );
13701
+ if (!current || current.status !== "running" || current.version !== dispatch.version || current.attempt >= current.maxAttempts) {
13414
13702
  return void 0;
13415
13703
  }
13416
13704
  return await updateDispatchRecord(state, {
@@ -13462,6 +13750,7 @@ async function runAgentDispatchSlice(callback, deps = {}) {
13462
13750
  conversationContext,
13463
13751
  artifactState: artifacts,
13464
13752
  piMessages: conversation.piMessages,
13753
+ destination: dispatch.destination,
13465
13754
  correlation: {
13466
13755
  conversationId,
13467
13756
  threadId: conversationId,
@@ -13729,14 +14018,16 @@ function normalizeMessage(value) {
13729
14018
  const conversationId = toOptionalString(value.conversationId);
13730
14019
  const inboundMessageId = toOptionalString(value.inboundMessageId);
13731
14020
  const source = normalizeSource(value.source);
14021
+ const destination = parseDestination(value.destination);
13732
14022
  const createdAtMs = toOptionalNumber(value.createdAtMs);
13733
14023
  const receivedAtMs = toOptionalNumber(value.receivedAtMs);
13734
14024
  const input = normalizeInput(value.input);
13735
- if (!conversationId || !inboundMessageId || !source || typeof createdAtMs !== "number" || typeof receivedAtMs !== "number" || !input) {
14025
+ if (!conversationId || !destination || !inboundMessageId || !source || typeof createdAtMs !== "number" || typeof receivedAtMs !== "number" || !input) {
13736
14026
  return void 0;
13737
14027
  }
13738
14028
  return {
13739
14029
  conversationId,
14030
+ destination,
13740
14031
  inboundMessageId,
13741
14032
  source,
13742
14033
  createdAtMs,
@@ -13768,14 +14059,16 @@ function normalizeWorkState(conversationId, value) {
13768
14059
  return void 0;
13769
14060
  }
13770
14061
  const storedConversationId = toOptionalString(value.conversationId);
14062
+ const destination = parseDestination(value.destination);
13771
14063
  const updatedAtMs = toOptionalNumber(value.updatedAtMs);
13772
- if (storedConversationId !== conversationId || typeof updatedAtMs !== "number") {
14064
+ if (storedConversationId !== conversationId || !destination || typeof updatedAtMs !== "number") {
13773
14065
  return void 0;
13774
14066
  }
13775
14067
  const messages = Array.isArray(value.messages) ? value.messages.map(normalizeMessage).filter((message) => Boolean(message)).filter((message) => message.conversationId === conversationId).sort(compareMessages) : [];
13776
14068
  return {
13777
14069
  schemaVersion: CONVERSATION_WORK_SCHEMA_VERSION,
13778
14070
  conversationId,
14071
+ destination,
13779
14072
  messages,
13780
14073
  needsRun: value.needsRun === true,
13781
14074
  updatedAtMs,
@@ -13787,6 +14080,7 @@ function emptyWorkState(args) {
13787
14080
  return {
13788
14081
  schemaVersion: CONVERSATION_WORK_SCHEMA_VERSION,
13789
14082
  conversationId: args.conversationId,
14083
+ destination: args.destination,
13790
14084
  messages: [],
13791
14085
  needsRun: false,
13792
14086
  updatedAtMs: args.nowMs
@@ -13900,10 +14194,15 @@ async function withConversationMutation(args, callback) {
13900
14194
  }
13901
14195
  }
13902
14196
  async function readWorkState(state, conversationId) {
13903
- return normalizeWorkState(
13904
- conversationId,
13905
- await state.get(stateKey(conversationId))
13906
- );
14197
+ const raw = await state.get(stateKey(conversationId));
14198
+ if (raw == null) {
14199
+ return void 0;
14200
+ }
14201
+ const work = normalizeWorkState(conversationId, raw);
14202
+ if (!work) {
14203
+ throw new Error(`Conversation work state is invalid for ${conversationId}`);
14204
+ }
14205
+ return work;
13907
14206
  }
13908
14207
  async function writeWorkState(state, work) {
13909
14208
  await state.set(
@@ -13920,6 +14219,14 @@ async function writeWorkState(state, work) {
13920
14219
  function hasRunnableWork(state) {
13921
14220
  return state.needsRun || pendingMessages(state).length > 0;
13922
14221
  }
14222
+ function assertSameConversationDestination(args) {
14223
+ if (sameDestination(args.current, args.next)) {
14224
+ return;
14225
+ }
14226
+ throw new Error(
14227
+ `Conversation work destination changed for ${args.conversationId}`
14228
+ );
14229
+ }
13923
14230
  async function getConversationWorkState(args) {
13924
14231
  const state = await getConnectedState(args.state);
13925
14232
  return await readWorkState(state, args.conversationId);
@@ -13937,8 +14244,14 @@ async function appendInboundMessage(args) {
13937
14244
  async (state) => {
13938
14245
  const current = await readWorkState(state, args.message.conversationId) ?? emptyWorkState({
13939
14246
  conversationId: args.message.conversationId,
14247
+ destination: args.message.destination,
13940
14248
  nowMs
13941
14249
  });
14250
+ assertSameConversationDestination({
14251
+ conversationId: args.message.conversationId,
14252
+ current: current.destination,
14253
+ next: args.message.destination
14254
+ });
13942
14255
  const existing = current.messages.find(
13943
14256
  (message) => message.inboundMessageId === args.message.inboundMessageId
13944
14257
  );
@@ -13981,7 +14294,10 @@ async function appendAndEnqueueInboundMessage(args) {
13981
14294
  idempotencyKey = duplicateInboundNudgeIdempotencyKey(args.message, nowMs);
13982
14295
  }
13983
14296
  const queueResult = await args.queue.send(
13984
- { conversationId: args.message.conversationId },
14297
+ {
14298
+ conversationId: args.message.conversationId,
14299
+ destination: args.message.destination
14300
+ },
13985
14301
  { idempotencyKey }
13986
14302
  );
13987
14303
  await markConversationWorkEnqueued({
@@ -13998,8 +14314,16 @@ async function requestConversationWork(args) {
13998
14314
  const nowMs = args.nowMs ?? now();
13999
14315
  return await withConversationMutation(args, async (state) => {
14000
14316
  const existing = await readWorkState(state, args.conversationId);
14317
+ if (existing) {
14318
+ assertSameConversationDestination({
14319
+ conversationId: args.conversationId,
14320
+ current: existing.destination,
14321
+ next: args.destination
14322
+ });
14323
+ }
14001
14324
  const current = existing ?? emptyWorkState({
14002
14325
  conversationId: args.conversationId,
14326
+ destination: args.destination,
14003
14327
  nowMs
14004
14328
  });
14005
14329
  await writeWorkState(state, {
@@ -14155,6 +14479,11 @@ async function requestConversationContinuation(args) {
14155
14479
  if (!current || current.lease?.leaseToken !== args.leaseToken) {
14156
14480
  return false;
14157
14481
  }
14482
+ assertSameConversationDestination({
14483
+ conversationId: args.conversationId,
14484
+ current: current.destination,
14485
+ next: args.destination
14486
+ });
14158
14487
  await writeWorkState(state, {
14159
14488
  ...current,
14160
14489
  needsRun: true,
@@ -14232,8 +14561,12 @@ async function getAwaitingTurnContinuationRequest(args) {
14232
14561
  if (!sessionRecord || sessionRecord.state !== "awaiting_resume" || sessionRecord.resumeReason !== "timeout" && sessionRecord.resumeReason !== "yield" || sessionRecord.resumeReason === "timeout" && sessionRecord.sliceId < 2) {
14233
14562
  return void 0;
14234
14563
  }
14564
+ if (!sessionRecord.destination) {
14565
+ return void 0;
14566
+ }
14235
14567
  return {
14236
14568
  conversationId: args.conversationId,
14569
+ destination: sessionRecord.destination,
14237
14570
  sessionId: args.sessionId,
14238
14571
  expectedVersion: sessionRecord.version
14239
14572
  };
@@ -14261,12 +14594,17 @@ function parseTurnTimeoutResumeRequest(value) {
14261
14594
  return void 0;
14262
14595
  }
14263
14596
  const record = value;
14264
- const expectedVersion = typeof record.expectedVersion === "number" ? record.expectedVersion : record.expectedCheckpointVersion;
14265
- if (typeof record.conversationId !== "string" || typeof record.sessionId !== "string" || typeof expectedVersion !== "number") {
14597
+ const destination = parseDestination(record.destination);
14598
+ let expectedVersion = record.expectedVersion;
14599
+ if (typeof expectedVersion !== "number") {
14600
+ expectedVersion = record.expectedCheckpointVersion;
14601
+ }
14602
+ if (typeof record.conversationId !== "string" || typeof record.sessionId !== "string" || typeof expectedVersion !== "number" || !destination) {
14266
14603
  return void 0;
14267
14604
  }
14268
14605
  return {
14269
14606
  conversationId: record.conversationId,
14607
+ destination,
14270
14608
  sessionId: record.sessionId,
14271
14609
  expectedVersion
14272
14610
  };
@@ -14275,12 +14613,16 @@ async function scheduleTurnTimeoutResume(request, options = {}) {
14275
14613
  const nowMs = options.nowMs ?? Date.now();
14276
14614
  await requestConversationWork({
14277
14615
  conversationId: request.conversationId,
14616
+ destination: request.destination,
14278
14617
  nowMs,
14279
14618
  state: options.state
14280
14619
  });
14281
14620
  const queue = options.queue ?? getVercelConversationWorkQueue();
14282
14621
  await queue.send(
14283
- { conversationId: request.conversationId },
14622
+ {
14623
+ conversationId: request.conversationId,
14624
+ destination: request.destination
14625
+ },
14284
14626
  {
14285
14627
  idempotencyKey: [
14286
14628
  "timeout",
@@ -14326,7 +14668,10 @@ function heartbeatIdempotencyKey(reason, conversationId, nowMs) {
14326
14668
  }
14327
14669
  async function sendRecoveryNudge(args) {
14328
14670
  await args.queue.send(
14329
- { conversationId: args.conversationId },
14671
+ {
14672
+ conversationId: args.conversationId,
14673
+ destination: args.destination
14674
+ },
14330
14675
  { idempotencyKey: args.idempotencyKey }
14331
14676
  );
14332
14677
  await markConversationWorkEnqueued({
@@ -14364,6 +14709,7 @@ async function recoverConversationWork(args) {
14364
14709
  }
14365
14710
  await sendRecoveryNudge({
14366
14711
  conversationId,
14712
+ destination: work.destination,
14367
14713
  idempotencyKey: heartbeatIdempotencyKey(
14368
14714
  "lease",
14369
14715
  conversationId,
@@ -14390,6 +14736,7 @@ async function recoverConversationWork(args) {
14390
14736
  }
14391
14737
  await sendRecoveryNudge({
14392
14738
  conversationId,
14739
+ destination: work.destination,
14393
14740
  idempotencyKey: heartbeatIdempotencyKey(
14394
14741
  "pending",
14395
14742
  conversationId,
@@ -14419,78 +14766,92 @@ async function recoverConversationWork(args) {
14419
14766
  return result;
14420
14767
  }
14421
14768
 
14422
- // src/chat/slack/ids.ts
14423
- function isSlackTeamId(value) {
14424
- return /^T[A-Z0-9]+$/.test(value);
14769
+ // src/chat/agent-dispatch/validation.ts
14770
+ import {
14771
+ dispatchOptionsSchema
14772
+ } from "@sentry/junior-plugin-api";
14773
+ function hasIssueAtPath(issues, path9) {
14774
+ return issues.some(
14775
+ (issue) => issue.path.length === path9.length && issue.path.every((value, index) => value === path9[index])
14776
+ );
14425
14777
  }
14426
- function isSlackConversationId(value) {
14427
- return /^(C|G|D)[A-Z0-9]+$/.test(value);
14778
+ function hasIssueUnderPath(issues, path9) {
14779
+ return issues.some(
14780
+ (issue) => path9.every((value, index) => issue.path[index] === value)
14781
+ );
14428
14782
  }
14429
-
14430
- // src/chat/agent-dispatch/validation.ts
14431
- var MAX_DISPATCH_INPUT_LENGTH = 32e3;
14432
- var MAX_IDEMPOTENCY_KEY_LENGTH = 512;
14433
- var MAX_METADATA_KEYS = 20;
14434
- var MAX_METADATA_KEY_LENGTH = 128;
14435
- var MAX_METADATA_VALUE_LENGTH = 512;
14436
- function validateDispatchOptions(options) {
14437
- if (!options.idempotencyKey.trim()) {
14438
- throw new Error("Dispatch idempotencyKey is required");
14783
+ function dispatchOptionsErrorMessage(issues) {
14784
+ if (hasIssueAtPath(issues, [])) {
14785
+ const unknownKeys = issues.some(
14786
+ (issue) => issue.code === "unrecognized_keys"
14787
+ );
14788
+ return unknownKeys ? "Dispatch options must not include unknown fields" : "Dispatch options are required";
14789
+ }
14790
+ if (issues.some(
14791
+ (issue) => issue.code === "unrecognized_keys" && issue.path[0] === "destination"
14792
+ )) {
14793
+ return "Dispatch destination must not include unknown fields";
14439
14794
  }
14440
- if (options.idempotencyKey.length > MAX_IDEMPOTENCY_KEY_LENGTH) {
14441
- throw new Error("Dispatch idempotencyKey exceeds the maximum length");
14795
+ if (issues.some(
14796
+ (issue) => issue.code === "unrecognized_keys" && issue.path[0] === "credentialSubject"
14797
+ )) {
14798
+ return "Dispatch credentialSubject binding is runtime-owned";
14442
14799
  }
14443
- if (options.destination.platform !== "slack") {
14444
- throw new Error("Dispatch destination platform must be slack");
14800
+ if (hasIssueAtPath(issues, ["destination"])) {
14801
+ return "Dispatch destination platform must be slack";
14445
14802
  }
14446
- if (!isSlackTeamId(options.destination.teamId)) {
14447
- throw new Error("Dispatch destination teamId must be a Slack team id");
14803
+ if (hasIssueUnderPath(issues, ["destination", "teamId"])) {
14804
+ return "Dispatch destination teamId must be a Slack team id";
14448
14805
  }
14449
- if (!isSlackConversationId(options.destination.channelId)) {
14450
- throw new Error(
14451
- "Dispatch destination channelId must be a Slack channel id"
14806
+ if (hasIssueUnderPath(issues, ["destination", "channelId"])) {
14807
+ return "Dispatch destination channelId must be a Slack channel id";
14808
+ }
14809
+ if (hasIssueUnderPath(issues, ["idempotencyKey"])) {
14810
+ const tooLong = issues.some(
14811
+ (issue) => issue.path[0] === "idempotencyKey" && issue.code === "too_big"
14812
+ );
14813
+ return tooLong ? "Dispatch idempotencyKey exceeds the maximum length" : "Dispatch idempotencyKey is required";
14814
+ }
14815
+ if (hasIssueUnderPath(issues, ["input"])) {
14816
+ const tooLong = issues.some(
14817
+ (issue) => issue.path[0] === "input" && issue.code === "too_big"
14452
14818
  );
14819
+ return tooLong ? "Dispatch input exceeds the maximum length" : "Dispatch input is required";
14453
14820
  }
14454
- if (!options.input.trim()) {
14455
- throw new Error("Dispatch input is required");
14821
+ if (hasIssueUnderPath(issues, ["credentialSubject", "userId"])) {
14822
+ return "Dispatch credentialSubject userId is required";
14456
14823
  }
14457
- if (options.input.length > MAX_DISPATCH_INPUT_LENGTH) {
14458
- throw new Error("Dispatch input exceeds the maximum length");
14824
+ if (hasIssueUnderPath(issues, ["credentialSubject", "allowedWhen"])) {
14825
+ return "Dispatch credentialSubject allowedWhen must be private-direct-conversation";
14459
14826
  }
14460
- if (options.credentialSubject) {
14461
- if (options.credentialSubject.type !== "user") {
14462
- throw new Error("Dispatch credentialSubject type must be user");
14463
- }
14464
- if (!isActorUserId(options.credentialSubject.userId)) {
14465
- throw new Error("Dispatch credentialSubject userId is required");
14466
- }
14467
- if (options.credentialSubject.allowedWhen !== "private-direct-conversation") {
14468
- throw new Error(
14469
- "Dispatch credentialSubject allowedWhen must be private-direct-conversation"
14470
- );
14471
- }
14472
- if (!isDmChannel(options.destination.channelId)) {
14827
+ if (hasIssueUnderPath(issues, ["credentialSubject"])) {
14828
+ return "Dispatch credentialSubject type must be user";
14829
+ }
14830
+ const metadataIssue = issues.find(
14831
+ (issue) => issue.path[0] === "metadata" && (issue.message === "Dispatch metadata has too many keys" || issue.message === "Dispatch metadata key exceeds the maximum length" || issue.message === "Dispatch metadata value exceeds the maximum length")
14832
+ );
14833
+ if (metadataIssue) {
14834
+ return metadataIssue.message;
14835
+ }
14836
+ if (hasIssueUnderPath(issues, ["metadata"])) {
14837
+ return "Dispatch metadata values must be strings";
14838
+ }
14839
+ return "Dispatch options are invalid";
14840
+ }
14841
+ function validateDispatchOptions(options) {
14842
+ const parsed = dispatchOptionsSchema.safeParse(options);
14843
+ if (!parsed.success) {
14844
+ throw new Error(dispatchOptionsErrorMessage(parsed.error.issues));
14845
+ }
14846
+ const candidate = parsed.data;
14847
+ const { credentialSubject, destination } = candidate;
14848
+ if (credentialSubject !== void 0) {
14849
+ if (!isDmChannel(destination.channelId)) {
14473
14850
  throw new Error(
14474
14851
  "Dispatch credentialSubject requires a private direct Slack destination"
14475
14852
  );
14476
14853
  }
14477
14854
  }
14478
- const metadata = options.metadata ?? {};
14479
- const entries = Object.entries(metadata);
14480
- if (entries.length > MAX_METADATA_KEYS) {
14481
- throw new Error("Dispatch metadata has too many keys");
14482
- }
14483
- for (const [key, value] of entries) {
14484
- if (!key.trim() || typeof value !== "string") {
14485
- throw new Error("Dispatch metadata values must be strings");
14486
- }
14487
- if (key.length > MAX_METADATA_KEY_LENGTH) {
14488
- throw new Error("Dispatch metadata key exceeds the maximum length");
14489
- }
14490
- if (value.length > MAX_METADATA_VALUE_LENGTH) {
14491
- throw new Error("Dispatch metadata value exceeds the maximum length");
14492
- }
14493
- }
14494
14855
  }
14495
14856
  async function verifyDispatchCredentialSubjectAccess(options) {
14496
14857
  if (!options.credentialSubject) {
@@ -14604,8 +14965,8 @@ function isStaleDispatch(args) {
14604
14965
  }
14605
14966
  async function failDispatch(args) {
14606
14967
  await withDispatchLock(args.record.id, async (state) => {
14607
- const current = await state.get(
14608
- getDispatchStorageKey(args.record.id)
14968
+ const current = parseDispatchRecord(
14969
+ await state.get(getDispatchStorageKey(args.record.id))
14609
14970
  ) ?? args.record;
14610
14971
  if (isTerminalDispatchStatus(current.status)) {
14611
14972
  return;
@@ -14737,7 +15098,7 @@ async function recoverStaleDispatches(args) {
14737
15098
  }
14738
15099
  return recovered;
14739
15100
  }
14740
- async function runTrustedPluginHeartbeats(args) {
15101
+ async function runPluginHeartbeats(args) {
14741
15102
  let count = 0;
14742
15103
  for (const plugin of getAgentPlugins()) {
14743
15104
  if (count >= (args.limit ?? DEFAULT_PLUGIN_LIMIT)) {
@@ -14763,7 +15124,7 @@ async function runTrustedPluginHeartbeats(args) {
14763
15124
  );
14764
15125
  if (typeof result?.dispatchCount === "number" && result.dispatchCount > 0) {
14765
15126
  logInfo(
14766
- "trusted_plugin_heartbeat_dispatched",
15127
+ "plugin_heartbeat_dispatched",
14767
15128
  {},
14768
15129
  {
14769
15130
  "app.dispatch.count": result.dispatchCount,
@@ -14775,10 +15136,10 @@ async function runTrustedPluginHeartbeats(args) {
14775
15136
  } catch (error) {
14776
15137
  logException(
14777
15138
  error,
14778
- "trusted_plugin_heartbeat_failed",
15139
+ "plugin_heartbeat_failed",
14779
15140
  {},
14780
15141
  { "app.plugin.name": plugin.name },
14781
- "Trusted plugin heartbeat failed"
15142
+ "Plugin heartbeat failed"
14782
15143
  );
14783
15144
  }
14784
15145
  }
@@ -14793,7 +15154,7 @@ async function runHeartbeat(args) {
14793
15154
  nowMs: args.nowMs
14794
15155
  });
14795
15156
  await recoverStaleDispatches({ nowMs: args.nowMs });
14796
- await runTrustedPluginHeartbeats({ nowMs: args.nowMs });
15157
+ await runPluginHeartbeats({ nowMs: args.nowMs });
14797
15158
  }
14798
15159
 
14799
15160
  // src/handlers/heartbeat.ts
@@ -15325,10 +15686,6 @@ function getWorkspaceTeamId() {
15325
15686
  }
15326
15687
 
15327
15688
  // src/chat/runtime/thread-context.ts
15328
- function toSlackTeamId(value) {
15329
- const candidate = toOptionalString(value);
15330
- return candidate && isSlackTeamId(candidate) ? candidate : void 0;
15331
- }
15332
15689
  function escapeRegExp2(value) {
15333
15690
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
15334
15691
  }
@@ -15404,14 +15761,6 @@ function getMessageTs(message) {
15404
15761
  const rawRecord = raw;
15405
15762
  return toOptionalString(rawRecord.ts) ?? toOptionalString(rawRecord.event_ts) ?? toOptionalString(rawRecord.message?.ts);
15406
15763
  }
15407
- function getTeamId(message) {
15408
- const raw = message.raw;
15409
- if (!raw || typeof raw !== "object") {
15410
- return void 0;
15411
- }
15412
- const rawRecord = raw;
15413
- return toSlackTeamId(rawRecord.team_id) ?? toSlackTeamId(rawRecord.team) ?? toSlackTeamId(getWorkspaceTeamId()) ?? toSlackTeamId(rawRecord.user_team);
15414
- }
15415
15764
 
15416
15765
  // src/chat/runtime/processing-reaction.ts
15417
15766
  var noProcessingReaction = {
@@ -16114,9 +16463,10 @@ async function persistFailedReplyState(channelId, threadTs, sessionId, expectedV
16114
16463
  }
16115
16464
  async function resumeAuthorizedMcpTurn(args) {
16116
16465
  const { authSession, provider } = args;
16117
- if (!authSession.channelId || !authSession.threadTs) {
16466
+ if (!authSession.channelId || !authSession.destination || !authSession.threadTs) {
16118
16467
  return;
16119
16468
  }
16469
+ const destination = authSession.destination;
16120
16470
  const threadId = `slack:${authSession.channelId}:${authSession.threadTs}`;
16121
16471
  const currentState = await getPersistedThreadState(threadId);
16122
16472
  const conversation = coerceThreadConversationState(currentState);
@@ -16225,6 +16575,7 @@ async function resumeAuthorizedMcpTurn(args) {
16225
16575
  actor: { type: "user", userId: authSession.userId }
16226
16576
  },
16227
16577
  requester,
16578
+ destination,
16228
16579
  correlation: {
16229
16580
  conversationId: authSession.conversationId,
16230
16581
  turnId: lockedSessionId,
@@ -16313,6 +16664,7 @@ async function resumeAuthorizedMcpTurn(args) {
16313
16664
  }
16314
16665
  await scheduleTurnTimeoutResume({
16315
16666
  conversationId: authSession.conversationId,
16667
+ destination,
16316
16668
  sessionId: lockedSessionId,
16317
16669
  expectedVersion: version
16318
16670
  });
@@ -16410,13 +16762,23 @@ async function buildSkillsSummaryText() {
16410
16762
  }
16411
16763
  return lines.join("\n");
16412
16764
  }
16413
- async function hasConnectedAccount(userId, plugin, userTokenStore) {
16414
- if (plugin.manifest.credentials?.type === "oauth-bearer") {
16765
+ function accountLabel(account) {
16766
+ const label = account.label ?? account.id;
16767
+ return account.url ? `<${account.url}|${label}>` : label;
16768
+ }
16769
+ function connectedAccountText(plugin, account) {
16770
+ return account ? `*${plugin.manifest.name}*
16771
+ Connected as ${accountLabel(account)}` : `*${plugin.manifest.name}*
16772
+ ${plugin.manifest.description}`;
16773
+ }
16774
+ async function connectedOAuthTokens(userId, plugin, userTokenStore) {
16775
+ if (plugin.manifest.oauth || plugin.manifest.credentials) {
16415
16776
  const stored = await userTokenStore.get(userId, plugin.manifest.name);
16416
- return Boolean(
16417
- stored && hasRequiredOAuthScope(stored.scope, plugin.manifest.oauth?.scope)
16418
- );
16777
+ return stored && hasRequiredOAuthScope(stored.scope, plugin.manifest.oauth?.scope) ? stored : void 0;
16419
16778
  }
16779
+ return void 0;
16780
+ }
16781
+ async function hasConnectedMcpAccount(userId, plugin) {
16420
16782
  if (plugin.manifest.mcp) {
16421
16783
  return Boolean(
16422
16784
  (await getMcpStoredOAuthCredentials(userId, plugin.manifest.name))?.tokens
@@ -16431,13 +16793,13 @@ async function buildHomeView(userId, userTokenStore) {
16431
16793
  const providers = getPluginProviders();
16432
16794
  const connectedSections = [];
16433
16795
  for (const plugin of providers) {
16434
- if (!await hasConnectedAccount(userId, plugin, userTokenStore)) continue;
16796
+ const tokens = await connectedOAuthTokens(userId, plugin, userTokenStore);
16797
+ if (!tokens && !await hasConnectedMcpAccount(userId, plugin)) continue;
16435
16798
  connectedSections.push({
16436
16799
  type: "section",
16437
16800
  text: {
16438
16801
  type: "mrkdwn",
16439
- text: `*${plugin.manifest.name}*
16440
- ${plugin.manifest.description}`
16802
+ text: connectedAccountText(plugin, tokens?.account)
16441
16803
  },
16442
16804
  accessory: {
16443
16805
  type: "button",
@@ -16582,22 +16944,10 @@ async function persistFailedOAuthReplyState(args) {
16582
16944
  });
16583
16945
  }
16584
16946
  async function resumeOAuthSessionRecordTurn(stored) {
16585
- if (!stored.resumeConversationId || !stored.resumeSessionId || !stored.channelId || !stored.threadTs) {
16586
- return false;
16587
- }
16588
- const sessionRecord = await getAgentTurnSessionRecord(
16589
- stored.resumeConversationId,
16590
- stored.resumeSessionId
16591
- );
16592
- if (!sessionRecord) {
16947
+ if (!stored.resumeConversationId || !stored.resumeSessionId || !stored.channelId || !stored.destination || !stored.threadTs) {
16593
16948
  return false;
16594
16949
  }
16595
- if (sessionRecord.state === "completed" || sessionRecord.state === "failed" || sessionRecord.state === "abandoned") {
16596
- return true;
16597
- }
16598
- if (sessionRecord.state !== "awaiting_resume" || sessionRecord.resumeReason !== "auth") {
16599
- return true;
16600
- }
16950
+ const destination = stored.destination;
16601
16951
  const currentState = await getPersistedThreadState(
16602
16952
  stored.resumeConversationId
16603
16953
  );
@@ -16606,7 +16956,8 @@ async function resumeOAuthSessionRecordTurn(stored) {
16606
16956
  conversation,
16607
16957
  kind: "plugin",
16608
16958
  provider: stored.provider,
16609
- requesterId: stored.userId
16959
+ requesterId: stored.userId,
16960
+ ...stored.scope ? { scope: stored.scope } : {}
16610
16961
  });
16611
16962
  const resolvedSessionId = pendingAuth?.sessionId ?? stored.resumeSessionId;
16612
16963
  const userMessage2 = resolvedSessionId ? getTurnUserMessage(conversation, resolvedSessionId) : void 0;
@@ -16623,32 +16974,34 @@ async function resumeOAuthSessionRecordTurn(stored) {
16623
16974
  });
16624
16975
  return true;
16625
16976
  }
16626
- } else {
16627
- if (!userMessage2?.author?.userId) {
16628
- return false;
16629
- }
16630
- if (conversation.processing.activeTurnId !== stored.resumeSessionId) {
16631
- return true;
16632
- }
16633
16977
  }
16634
- if (!userMessage2?.author?.userId || !resolvedSessionId) {
16978
+ const sessionRecord = await getAgentTurnSessionRecord(
16979
+ stored.resumeConversationId,
16980
+ resolvedSessionId
16981
+ );
16982
+ if (!sessionRecord) {
16635
16983
  return false;
16636
16984
  }
16985
+ if (sessionRecord.state === "completed" || sessionRecord.state === "failed" || sessionRecord.state === "abandoned") {
16986
+ return true;
16987
+ }
16988
+ if (sessionRecord.state !== "awaiting_resume" || sessionRecord.resumeReason !== "auth") {
16989
+ return true;
16990
+ }
16991
+ if (!userMessage2?.author?.userId) {
16992
+ return false;
16993
+ }
16994
+ if (!pendingAuth && conversation.processing.activeTurnId !== stored.resumeSessionId) {
16995
+ return true;
16996
+ }
16637
16997
  await resumeSlackTurn({
16638
- messageText: stored.pendingMessage ?? userMessage2.text,
16998
+ messageText: pendingAuth ? userMessage2.text : stored.pendingMessage ?? userMessage2.text,
16639
16999
  channelId: stored.channelId,
16640
17000
  threadTs: stored.threadTs,
16641
17001
  messageTs: getTurnUserSlackMessageTs(userMessage2),
16642
17002
  lockKey: stored.resumeConversationId,
16643
17003
  initialText: "",
16644
17004
  beforeStart: async () => {
16645
- const lockedSessionRecord = await getAgentTurnSessionRecord(
16646
- stored.resumeConversationId,
16647
- stored.resumeSessionId
16648
- );
16649
- if (!lockedSessionRecord || lockedSessionRecord.state !== "awaiting_resume" || lockedSessionRecord.resumeReason !== "auth") {
16650
- return false;
16651
- }
16652
17005
  const lockedState = await getPersistedThreadState(
16653
17006
  stored.resumeConversationId
16654
17007
  );
@@ -16658,12 +17011,20 @@ async function resumeOAuthSessionRecordTurn(stored) {
16658
17011
  conversation: lockedConversation,
16659
17012
  kind: "plugin",
16660
17013
  provider: stored.provider,
16661
- requesterId: stored.userId
17014
+ requesterId: stored.userId,
17015
+ ...stored.scope ? { scope: stored.scope } : {}
16662
17016
  });
16663
17017
  const lockedSessionId = lockedPendingAuth?.sessionId ?? stored.resumeSessionId;
16664
17018
  if (lockedSessionId !== resolvedSessionId) {
16665
17019
  return false;
16666
17020
  }
17021
+ const lockedSessionRecord = await getAgentTurnSessionRecord(
17022
+ stored.resumeConversationId,
17023
+ lockedSessionId
17024
+ );
17025
+ if (!lockedSessionRecord || lockedSessionRecord.state !== "awaiting_resume" || lockedSessionRecord.resumeReason !== "auth") {
17026
+ return false;
17027
+ }
16667
17028
  if (lockedPendingAuth) {
16668
17029
  if (!isPendingAuthLatestRequest(lockedConversation, lockedPendingAuth)) {
16669
17030
  clearPendingAuth(lockedConversation, lockedPendingAuth.sessionId);
@@ -16711,7 +17072,7 @@ async function resumeOAuthSessionRecordTurn(stored) {
16711
17072
  lockedUserMessage.author.userId
16712
17073
  );
16713
17074
  return {
16714
- messageText: stored.pendingMessage ?? lockedUserMessage.text,
17075
+ messageText: lockedPendingAuth ? lockedUserMessage.text : stored.pendingMessage ?? lockedUserMessage.text,
16715
17076
  messageTs: getTurnUserSlackMessageTs(lockedUserMessage),
16716
17077
  replyContext: {
16717
17078
  credentialContext: {
@@ -16721,6 +17082,7 @@ async function resumeOAuthSessionRecordTurn(stored) {
16721
17082
  }
16722
17083
  },
16723
17084
  requester,
17085
+ destination,
16724
17086
  correlation: {
16725
17087
  conversationId: stored.resumeConversationId,
16726
17088
  turnId: lockedSessionId,
@@ -16797,6 +17159,7 @@ async function resumeOAuthSessionRecordTurn(stored) {
16797
17159
  }
16798
17160
  await scheduleTurnTimeoutResume({
16799
17161
  conversationId: stored.resumeConversationId,
17162
+ destination,
16800
17163
  sessionId: lockedSessionId,
16801
17164
  expectedVersion: version
16802
17165
  });
@@ -16807,7 +17170,9 @@ async function resumeOAuthSessionRecordTurn(stored) {
16807
17170
  return true;
16808
17171
  }
16809
17172
  async function resumePendingOAuthMessage(stored) {
16810
- if (!stored.pendingMessage || !stored.channelId || !stored.threadTs) return;
17173
+ if (!stored.pendingMessage || !stored.channelId || !stored.destination || !stored.threadTs) {
17174
+ return;
17175
+ }
16811
17176
  const threadId = `slack:${stored.channelId}:${stored.threadTs}`;
16812
17177
  const conversation = coerceThreadConversationState(
16813
17178
  await getPersistedThreadState(threadId)
@@ -16828,6 +17193,13 @@ async function resumePendingOAuthMessage(stored) {
16828
17193
  actor: { type: "user", userId: stored.userId }
16829
17194
  },
16830
17195
  requester,
17196
+ destination: stored.destination,
17197
+ correlation: {
17198
+ conversationId: threadId,
17199
+ channelId: stored.channelId,
17200
+ threadTs: stored.threadTs,
17201
+ requesterId: stored.userId
17202
+ },
16831
17203
  conversationContext,
16832
17204
  piMessages: conversation.piMessages,
16833
17205
  configuration: stored.configuration
@@ -16887,7 +17259,7 @@ async function GET4(request, provider, waitUntil) {
16887
17259
  }
16888
17260
  const stateAdapter = getStateAdapter();
16889
17261
  const stateKey2 = `oauth-state:${state}`;
16890
- const stored = await stateAdapter.get(stateKey2);
17262
+ const stored = parseOAuthStatePayload(await stateAdapter.get(stateKey2));
16891
17263
  if (!stored) {
16892
17264
  return htmlErrorResponse(
16893
17265
  "Link expired",
@@ -16921,6 +17293,7 @@ async function GET4(request, provider, waitUntil) {
16921
17293
  );
16922
17294
  }
16923
17295
  const redirectUri = `${baseUrl}${providerConfig.callbackPath}`;
17296
+ const requestedScope = stored.scope ?? providerConfig.scope;
16924
17297
  let tokenResponse;
16925
17298
  try {
16926
17299
  const tokenRequest = buildOAuthTokenRequest({
@@ -16953,13 +17326,12 @@ async function GET4(request, provider, waitUntil) {
16953
17326
  500
16954
17327
  );
16955
17328
  }
16956
- const tokenData = await tokenResponse.json();
16957
17329
  let parsedTokenResponse;
16958
17330
  try {
16959
- parsedTokenResponse = parseOAuthTokenResponse(
16960
- tokenData,
16961
- providerConfig.scope
16962
- );
17331
+ const tokenData = await tokenResponse.json();
17332
+ parsedTokenResponse = parseOAuthTokenResponse(tokenData, requestedScope, {
17333
+ treatEmptyScopeAsUnreported: providerConfig.treatEmptyScopeAsUnreported
17334
+ });
16963
17335
  } catch {
16964
17336
  return htmlErrorResponse(
16965
17337
  "Connection failed",
@@ -16967,7 +17339,7 @@ async function GET4(request, provider, waitUntil) {
16967
17339
  500
16968
17340
  );
16969
17341
  }
16970
- if (!hasRequiredOAuthScope(parsedTokenResponse.scope, providerConfig.scope)) {
17342
+ if (!hasRequiredOAuthScope(parsedTokenResponse.scope, requestedScope)) {
16971
17343
  return htmlErrorResponse(
16972
17344
  "Connection failed",
16973
17345
  `The ${providerLabel} authorization did not grant the access Junior requires. Return to Slack and ask Junior to connect your ${providerLabel} account again.`,
@@ -16975,7 +17347,23 @@ async function GET4(request, provider, waitUntil) {
16975
17347
  );
16976
17348
  }
16977
17349
  const userTokenStore = createUserTokenStore();
16978
- await userTokenStore.set(stored.userId, provider, parsedTokenResponse);
17350
+ let account;
17351
+ try {
17352
+ account = await resolvePluginOAuthAccount({
17353
+ provider,
17354
+ tokens: parsedTokenResponse
17355
+ });
17356
+ } catch {
17357
+ return htmlErrorResponse(
17358
+ "Connection failed",
17359
+ `Junior could not verify the connected ${providerLabel} account. Please try again.`,
17360
+ 500
17361
+ );
17362
+ }
17363
+ await userTokenStore.set(stored.userId, provider, {
17364
+ ...parsedTokenResponse,
17365
+ ...account ? { account } : {}
17366
+ });
16979
17367
  waitUntil(async () => {
16980
17368
  try {
16981
17369
  await publishAppHomeView(getSlackClient(), stored.userId, userTokenStore);
@@ -17023,6 +17411,149 @@ async function GET4(request, provider, waitUntil) {
17023
17411
  });
17024
17412
  }
17025
17413
 
17414
+ // src/chat/sandbox/egress-credentials.ts
17415
+ var HTTP_READ_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
17416
+ var SandboxEgressCredentialNeededError = class extends Error {
17417
+ authorization;
17418
+ grant;
17419
+ provider;
17420
+ constructor(input) {
17421
+ super(input.message);
17422
+ this.name = "SandboxEgressCredentialNeededError";
17423
+ this.authorization = input.authorization;
17424
+ this.grant = input.grant;
17425
+ this.provider = input.provider;
17426
+ }
17427
+ };
17428
+ function defaultGrantForProvider(input) {
17429
+ const access = HTTP_READ_METHODS.has(
17430
+ input.method.toUpperCase()
17431
+ ) ? "read" : "write";
17432
+ return {
17433
+ source: "broker",
17434
+ grant: {
17435
+ name: "default",
17436
+ access,
17437
+ reason: `sandbox-egress:${input.provider}:${access}`
17438
+ }
17439
+ };
17440
+ }
17441
+ function oauthAuthorizationForProvider(provider) {
17442
+ const oauth = getPluginOAuthConfig(provider);
17443
+ return oauth ? {
17444
+ type: "oauth",
17445
+ provider,
17446
+ ...oauth.scope ? { scope: oauth.scope } : {}
17447
+ } : void 0;
17448
+ }
17449
+ function credentialSubjectFromContext(context) {
17450
+ return "subject" in context.credentials && context.credentials.subject ? { type: "user", userId: context.credentials.subject.userId } : void 0;
17451
+ }
17452
+ function assertLeaseTransformsOwnedByProvider(provider, lease) {
17453
+ for (const transform of lease.headerTransforms) {
17454
+ if (resolveSandboxEgressProviderForHost(transform.domain) !== provider) {
17455
+ throw new Error(
17456
+ `Credential lease for ${provider} included header transform for unowned domain ${transform.domain}`
17457
+ );
17458
+ }
17459
+ }
17460
+ }
17461
+ async function selectSandboxEgressGrant(input) {
17462
+ if (!hasEgressCredentialHooks(input.provider)) {
17463
+ return defaultGrantForProvider(input);
17464
+ }
17465
+ const pluginGrant = await selectPluginGrant({
17466
+ ...input.bodyText !== void 0 ? { bodyText: input.bodyText } : {},
17467
+ provider: input.provider,
17468
+ method: input.method,
17469
+ upstreamUrl: input.upstreamUrl
17470
+ });
17471
+ if (!pluginGrant) {
17472
+ throw new Error(
17473
+ `Plugin "${input.provider}" grantForEgress must return a grant for sandbox egress`
17474
+ );
17475
+ }
17476
+ return { source: "plugin", grant: pluginGrant };
17477
+ }
17478
+ function authorizationForSandboxEgressGrant(provider, selection) {
17479
+ return selection.source === "broker" ? oauthAuthorizationForProvider(provider) : void 0;
17480
+ }
17481
+ async function sandboxEgressCredentialLease(provider, selection, context) {
17482
+ const { grant } = selection;
17483
+ const cached = await getSandboxEgressCredentialLease(
17484
+ provider,
17485
+ grant.name,
17486
+ context
17487
+ );
17488
+ if (cached) {
17489
+ if (selection.source === "plugin" && cached.grant.access !== grant.access) {
17490
+ throw new Error(
17491
+ `Cached credential lease for ${provider}/${grant.name} has ${cached.grant.access} access, but ${grant.access} was selected`
17492
+ );
17493
+ }
17494
+ return {
17495
+ ...cached,
17496
+ grant
17497
+ };
17498
+ }
17499
+ let lease;
17500
+ if (selection.source === "plugin") {
17501
+ const credentialSubject = credentialSubjectFromContext(context);
17502
+ const pluginResult = await issuePluginCredential({
17503
+ provider,
17504
+ grant,
17505
+ actor: context.credentials.actor,
17506
+ ...credentialSubject ? { credentialSubject } : {},
17507
+ userTokenStore: createUserTokenStore()
17508
+ });
17509
+ if (pluginResult.type === "needed") {
17510
+ throw new SandboxEgressCredentialNeededError({
17511
+ provider,
17512
+ grant,
17513
+ authorization: pluginResult.authorization,
17514
+ message: pluginResult.message
17515
+ });
17516
+ }
17517
+ if (pluginResult.type === "unavailable") {
17518
+ throw new CredentialUnavailableError(provider, pluginResult.message);
17519
+ }
17520
+ lease = pluginResult.lease;
17521
+ } else {
17522
+ lease = await issueProviderCredentialLease({
17523
+ context: context.credentials,
17524
+ provider,
17525
+ reason: grant.reason ?? `sandbox-egress:${provider}:default`
17526
+ });
17527
+ }
17528
+ const headerTransforms = lease.headerTransforms ?? [];
17529
+ if (headerTransforms.length === 0) {
17530
+ throw new Error(
17531
+ `Credential lease for ${provider} did not include header transforms`
17532
+ );
17533
+ }
17534
+ const leaseExpiresAtMs = Date.parse(lease.expiresAt);
17535
+ if (!Number.isFinite(leaseExpiresAtMs) || leaseExpiresAtMs <= Date.now()) {
17536
+ throw new Error(`Credential lease for ${provider} is expired`);
17537
+ }
17538
+ const authorization = selection.source === "broker" ? oauthAuthorizationForProvider(provider) : lease.authorization;
17539
+ const cachedLease = {
17540
+ provider,
17541
+ grant,
17542
+ ...lease.account ? { account: lease.account } : {},
17543
+ ...authorization ? { authorization } : {},
17544
+ expiresAt: lease.expiresAt,
17545
+ headerTransforms
17546
+ };
17547
+ assertLeaseTransformsOwnedByProvider(provider, cachedLease);
17548
+ await setSandboxEgressCredentialLease(context, cachedLease);
17549
+ return cachedLease;
17550
+ }
17551
+ function hasSandboxEgressLeaseTransformForHost(lease, host) {
17552
+ return lease.headerTransforms.some(
17553
+ (transform) => matchesSandboxEgressDomain(host, transform.domain)
17554
+ );
17555
+ }
17556
+
17026
17557
  // src/chat/sandbox/egress-oidc.ts
17027
17558
  import {
17028
17559
  createRemoteJWKSet,
@@ -17094,6 +17625,7 @@ async function verifyVercelSandboxOidcToken(token) {
17094
17625
  }
17095
17626
 
17096
17627
  // src/chat/sandbox/egress-proxy.ts
17628
+ import { EgressAuthRequired } from "@sentry/junior-plugin-api";
17097
17629
  var OIDC_TOKEN_HEADER = "vercel-sandbox-oidc-token";
17098
17630
  var FORWARDED_HOST_HEADER = "vercel-forwarded-host";
17099
17631
  var FORWARDED_SCHEME_HEADER = "vercel-forwarded-scheme";
@@ -17123,9 +17655,24 @@ var DECODED_RESPONSE_HEADERS = /* @__PURE__ */ new Set([
17123
17655
  ]);
17124
17656
  var UPSTREAM_TOKEN_REJECTION_STATUS = 401;
17125
17657
  var UPSTREAM_PERMISSION_REJECTION_STATUS = 403;
17658
+ var GRANT_SELECTION_BODY_TEXT_LIMIT_BYTES = 64 * 1024;
17659
+ var RESPONSE_BODY_TEXT_LIMIT_BYTES = 64 * 1024;
17126
17660
  function jsonError(message, status) {
17127
17661
  return Response.json({ error: message }, { status });
17128
17662
  }
17663
+ function authRequiredResponse(input) {
17664
+ return new Response(
17665
+ `junior-auth-required provider=${input.provider} grant=${input.grant.name} access=${input.grant.access} 401 unauthorized
17666
+ ${input.message}`,
17667
+ {
17668
+ status: 401,
17669
+ headers: {
17670
+ "content-type": "text/plain; charset=utf-8",
17671
+ "cache-control": "no-store"
17672
+ }
17673
+ }
17674
+ );
17675
+ }
17129
17676
  function shouldLogSandboxEgressInfo() {
17130
17677
  const environment = (process.env.SENTRY_ENVIRONMENT ?? process.env.VERCEL_ENV ?? process.env.NODE_ENV ?? "").trim().toLowerCase();
17131
17678
  return environment !== "production";
@@ -17133,7 +17680,10 @@ function shouldLogSandboxEgressInfo() {
17133
17680
  function egressAttributes(input) {
17134
17681
  return {
17135
17682
  ...input.egressId ? { "app.sandbox.egress_id": input.egressId } : {},
17136
- ...input.provider ? { "app.credential.provider": input.provider } : {},
17683
+ ...input.provider ? { "app.provider.name": input.provider } : {},
17684
+ ...input.grantName ? { "app.grant.name": input.grantName } : {},
17685
+ ...input.grantAccess ? { "app.grant.access": input.grantAccess } : {},
17686
+ ...input.grantReason ? { "app.grant.reason": input.grantReason } : {},
17137
17687
  ...input.host ? { "server.address": input.host } : {},
17138
17688
  ...input.method ? { "http.request.method": input.method } : {},
17139
17689
  ...input.path ? { "url.path": input.path } : {},
@@ -17169,9 +17719,45 @@ function routingAttributes(request, upstreamUrl) {
17169
17719
  };
17170
17720
  if (upstreamUrl) {
17171
17721
  attributes["app.sandbox.egress.upstream_path"] = upstreamUrl.pathname;
17722
+ const gitService = upstreamUrl.searchParams.get("service");
17723
+ if (upstreamUrl.hostname.toLowerCase() === "github.com" && (gitService === "git-upload-pack" || gitService === "git-receive-pack")) {
17724
+ attributes["app.sandbox.egress.git_service"] = gitService;
17725
+ }
17172
17726
  }
17173
17727
  return attributes;
17174
17728
  }
17729
+ function displayedUpstreamPath(upstreamUrl) {
17730
+ const gitService = upstreamUrl.searchParams.get("service");
17731
+ if (upstreamUrl.hostname.toLowerCase() === "github.com" && (gitService === "git-upload-pack" || gitService === "git-receive-pack")) {
17732
+ return `${upstreamUrl.pathname}?service=${gitService}`;
17733
+ }
17734
+ return upstreamUrl.pathname;
17735
+ }
17736
+ function upstreamPermissionAttributes(provider, upstream) {
17737
+ if (provider !== "github") {
17738
+ return {};
17739
+ }
17740
+ return {
17741
+ "app.github.accepted_permissions": upstream.headers.get("x-accepted-github-permissions") ?? void 0,
17742
+ "app.github.sso": upstream.headers.get("x-github-sso") ?? void 0
17743
+ };
17744
+ }
17745
+ function githubPermissionHeaders(upstream) {
17746
+ const acceptedPermissions = upstream.headers.get(
17747
+ "x-accepted-github-permissions"
17748
+ );
17749
+ const sso = upstream.headers.get("x-github-sso");
17750
+ return {
17751
+ ...acceptedPermissions ? { acceptedPermissions } : {},
17752
+ ...sso ? { sso } : {}
17753
+ };
17754
+ }
17755
+ function permissionDeniedMessage(provider, grant) {
17756
+ return `${provider} returned HTTP 403 after Junior injected the ${grant.name} grant. Junior forwarded the request; this is not a local runtime block.`;
17757
+ }
17758
+ function isEgressAuthRequired(error) {
17759
+ return error instanceof EgressAuthRequired || error instanceof Error && error.name === "EgressAuthRequired";
17760
+ }
17175
17761
  function logSandboxEgressUpstreamRequest(input) {
17176
17762
  if (!shouldLogSandboxEgressInfo()) {
17177
17763
  return;
@@ -17182,6 +17768,9 @@ function logSandboxEgressUpstreamRequest(input) {
17182
17768
  {
17183
17769
  ...egressAttributes({
17184
17770
  egressId: input.egressId,
17771
+ grantAccess: input.grantAccess,
17772
+ grantName: input.grantName,
17773
+ grantReason: input.grantReason,
17185
17774
  host: input.upstreamUrl.hostname,
17186
17775
  method: input.request.method,
17187
17776
  path: input.upstreamUrl.pathname,
@@ -17191,7 +17780,7 @@ function logSandboxEgressUpstreamRequest(input) {
17191
17780
  ...routingAttributes(input.request, input.upstreamUrl),
17192
17781
  "app.sandbox.egress.upstream_ok": input.upstream.ok
17193
17782
  },
17194
- `Sandbox egress ${input.request.method} ${input.upstreamUrl.hostname}${input.upstreamUrl.pathname} -> ${input.upstream.status}`
17783
+ `Sandbox egress ${input.request.method} ${input.upstreamUrl.hostname}${displayedUpstreamPath(input.upstreamUrl)} -> ${input.upstream.status}`
17195
17784
  );
17196
17785
  }
17197
17786
  function normalizeHost(value) {
@@ -17277,6 +17866,78 @@ async function requestBodyBytes(request) {
17277
17866
  }
17278
17867
  return await request.arrayBuffer();
17279
17868
  }
17869
+ function isGrantSelectionBodyVisible(input) {
17870
+ return input.provider === "github" && input.upstreamUrl.hostname.toLowerCase() === "api.github.com" && input.upstreamUrl.pathname.toLowerCase().endsWith("/graphql");
17871
+ }
17872
+ function requestBodyText(body) {
17873
+ if (body === void 0 || body.byteLength > GRANT_SELECTION_BODY_TEXT_LIMIT_BYTES) {
17874
+ return void 0;
17875
+ }
17876
+ return new TextDecoder().decode(body);
17877
+ }
17878
+ function responseContentLength(upstream) {
17879
+ const raw = upstream.headers.get("content-length");
17880
+ if (!raw) {
17881
+ return void 0;
17882
+ }
17883
+ const parsed = Number(raw);
17884
+ return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : void 0;
17885
+ }
17886
+ async function responseTextWithinLimit(upstream, maxBytes) {
17887
+ const limit = Math.min(
17888
+ Math.max(0, Math.floor(maxBytes)),
17889
+ RESPONSE_BODY_TEXT_LIMIT_BYTES
17890
+ );
17891
+ if (limit <= 0) {
17892
+ return void 0;
17893
+ }
17894
+ const contentLength = responseContentLength(upstream);
17895
+ if (contentLength !== void 0 && contentLength > limit) {
17896
+ return void 0;
17897
+ }
17898
+ let clone;
17899
+ try {
17900
+ clone = upstream.clone();
17901
+ } catch {
17902
+ return void 0;
17903
+ }
17904
+ const body = clone.body;
17905
+ if (!body) {
17906
+ return "";
17907
+ }
17908
+ const reader = body.getReader();
17909
+ const chunks = [];
17910
+ let bytes = 0;
17911
+ try {
17912
+ while (true) {
17913
+ const { done, value } = await reader.read();
17914
+ if (done) {
17915
+ break;
17916
+ }
17917
+ if (!value) {
17918
+ continue;
17919
+ }
17920
+ bytes += value.byteLength;
17921
+ if (bytes > limit) {
17922
+ await reader.cancel().catch(() => void 0);
17923
+ return void 0;
17924
+ }
17925
+ chunks.push(value);
17926
+ }
17927
+ } catch {
17928
+ await reader.cancel().catch(() => void 0);
17929
+ return void 0;
17930
+ } finally {
17931
+ reader.releaseLock();
17932
+ }
17933
+ const combined = new Uint8Array(bytes);
17934
+ let offset = 0;
17935
+ for (const chunk of chunks) {
17936
+ combined.set(chunk, offset);
17937
+ offset += chunk.byteLength;
17938
+ }
17939
+ return new TextDecoder().decode(combined);
17940
+ }
17280
17941
  function requestHeaders(request, lease, upstreamHost) {
17281
17942
  const headers = new Headers();
17282
17943
  request.headers.forEach((value, key) => {
@@ -17306,35 +17967,6 @@ function responseHeaders(upstream) {
17306
17967
  });
17307
17968
  return headers;
17308
17969
  }
17309
- async function credentialLease(provider, context) {
17310
- const cached = await getSandboxEgressCredentialLease(provider, context);
17311
- if (cached) {
17312
- return cached;
17313
- }
17314
- const lease = await issueProviderCredentialLease({
17315
- context: context.credentials,
17316
- provider,
17317
- reason: `sandbox-egress:${provider}`
17318
- });
17319
- const headerTransforms = lease.headerTransforms ?? [];
17320
- if (headerTransforms.length === 0) {
17321
- throw new Error(
17322
- `Credential lease for ${provider} did not include header transforms`
17323
- );
17324
- }
17325
- const cachedLease = {
17326
- provider,
17327
- expiresAt: lease.expiresAt,
17328
- headerTransforms
17329
- };
17330
- await setSandboxEgressCredentialLease(context, cachedLease);
17331
- return cachedLease;
17332
- }
17333
- function hasTransformForHost(lease, host) {
17334
- return lease.headerTransforms.some(
17335
- (transform) => matchesSandboxEgressDomain(host, transform.domain)
17336
- );
17337
- }
17338
17970
  function isSandboxEgressForwardedRequest(request) {
17339
17971
  return Boolean(
17340
17972
  request.headers.get(OIDC_TOKEN_HEADER)?.trim() && request.headers.get(FORWARDED_HOST_HEADER)?.trim() && request.headers.get(FORWARDED_SCHEME_HEADER)?.trim()
@@ -17440,17 +18072,79 @@ async function proxySandboxEgressRequest(request, deps = {}) {
17440
18072
  403
17441
18073
  );
17442
18074
  }
18075
+ let body;
18076
+ let bodyRead = false;
18077
+ if (isGrantSelectionBodyVisible({ provider, upstreamUrl })) {
18078
+ body = await requestBodyBytes(request);
18079
+ bodyRead = true;
18080
+ }
18081
+ const grantSelection = await selectSandboxEgressGrant({
18082
+ bodyText: requestBodyText(body),
18083
+ provider,
18084
+ method: request.method,
18085
+ upstreamUrl
18086
+ });
17443
18087
  let lease;
17444
18088
  try {
17445
- lease = await credentialLease(provider, credentialContext);
18089
+ lease = await sandboxEgressCredentialLease(
18090
+ provider,
18091
+ grantSelection,
18092
+ credentialContext
18093
+ );
17446
18094
  } catch (error) {
18095
+ if (error instanceof SandboxEgressCredentialNeededError) {
18096
+ await setSandboxEgressAuthRequiredSignal(credentialContext, {
18097
+ provider: error.provider,
18098
+ grant: error.grant,
18099
+ ...error.authorization ? { authorization: error.authorization } : {},
18100
+ message: error.message
18101
+ });
18102
+ logWarn(
18103
+ "sandbox_egress_credential_needed",
18104
+ {},
18105
+ {
18106
+ ...egressAttributes({
18107
+ egressId: activeEgressId,
18108
+ grantAccess: error.grant.access,
18109
+ grantName: error.grant.name,
18110
+ grantReason: error.grant.reason,
18111
+ host: upstreamUrl.hostname,
18112
+ method: request.method,
18113
+ path: upstreamUrl.pathname,
18114
+ provider: error.provider,
18115
+ status: 401
18116
+ }),
18117
+ ...routingAttributes(request, upstreamUrl)
18118
+ },
18119
+ "Sandbox egress grant needs user authorization before issuing a credential lease"
18120
+ );
18121
+ return authRequiredResponse({
18122
+ provider: error.provider,
18123
+ grant: error.grant,
18124
+ message: error.message
18125
+ });
18126
+ }
17447
18127
  if (error instanceof CredentialUnavailableError) {
18128
+ const failedGrant = grantSelection.grant;
18129
+ const authorization = authorizationForSandboxEgressGrant(
18130
+ error.provider,
18131
+ grantSelection
18132
+ );
18133
+ await setSandboxEgressAuthRequiredSignal(credentialContext, {
18134
+ provider: error.provider,
18135
+ grant: failedGrant,
18136
+ ...authorization ? { authorization } : {},
18137
+ message: error.message
18138
+ });
17448
18139
  logWarn(
17449
18140
  "sandbox_egress_credential_unavailable",
17450
18141
  {},
17451
18142
  {
17452
18143
  ...egressAttributes({
17453
18144
  egressId: activeEgressId,
18145
+ grantAccess: failedGrant.access,
18146
+ grantName: failedGrant.name,
18147
+ grantReason: failedGrant.reason,
17454
18148
  host: upstreamUrl.hostname,
17455
18149
  method: request.method,
17456
18150
  path: upstreamUrl.pathname,
@@ -17459,26 +18153,26 @@ async function proxySandboxEgressRequest(request, deps = {}) {
17459
18153
  }),
17460
18154
  ...routingAttributes(request, upstreamUrl)
17461
18155
  },
17462
- "Sandbox egress provider credential is unavailable"
17463
- );
17464
- return new Response(
17465
- `junior-auth-required provider=${error.provider} 401 unauthorized
17466
- ${error.message}`,
17467
- {
17468
- status: 401,
17469
- headers: { "content-type": "text/plain; charset=utf-8" }
17470
- }
18156
+ "Sandbox egress credential lease is unavailable for selected grant"
17471
18157
  );
18158
+ return authRequiredResponse({
18159
+ provider: error.provider,
18160
+ grant: failedGrant,
18161
+ message: error.message
18162
+ });
17472
18163
  }
17473
18164
  throw error;
17474
18165
  }
17475
- if (!hasTransformForHost(lease, upstreamUrl.hostname)) {
18166
+ if (!hasSandboxEgressLeaseTransformForHost(lease, upstreamUrl.hostname)) {
17476
18167
  logWarn(
17477
18168
  "sandbox_egress_transform_missing",
17478
18169
  {},
17479
18170
  {
17480
18171
  ...egressAttributes({
17481
18172
  egressId: activeEgressId,
18173
+ grantAccess: lease.grant.access,
18174
+ grantName: lease.grant.name,
18175
+ grantReason: lease.grant.reason,
17482
18176
  host: upstreamUrl.hostname,
17483
18177
  method: request.method,
17484
18178
  path: upstreamUrl.pathname,
@@ -17494,9 +18188,11 @@ ${error.message}`,
17494
18188
  );
17495
18189
  return jsonError("Credential lease does not cover forwarded host", 403);
17496
18190
  }
17497
- const body = await requestBodyBytes(request);
17498
18191
  const fetchImpl = deps.fetch ?? fetch;
17499
18192
  const headers = requestHeaders(request, lease, upstreamUrl.hostname);
18193
+ if (!bodyRead) {
18194
+ body = await requestBodyBytes(request);
18195
+ }
17500
18196
  const intercepted = await deps.interceptHttp?.({
17501
18197
  provider,
17502
18198
  request: new Request(upstreamUrl, {
@@ -17515,8 +18211,98 @@ ${error.message}`,
17515
18211
  ...body !== void 0 ? { body } : {},
17516
18212
  redirect: "manual"
17517
18213
  });
18214
+ try {
18215
+ const effects = await onPluginEgressResponse({
18216
+ provider,
18217
+ grant: lease.grant,
18218
+ method: request.method,
18219
+ upstreamUrl,
18220
+ response: {
18221
+ headers: new Headers(upstream.headers),
18222
+ readText: async (maxBytes) => await responseTextWithinLimit(upstream, maxBytes),
18223
+ status: upstream.status
18224
+ }
18225
+ });
18226
+ if (effects.permissionDenied) {
18227
+ await setSandboxEgressPermissionDeniedSignal(credentialContext, {
18228
+ provider,
18229
+ grant: lease.grant,
18230
+ ...lease.account ? { account: lease.account } : {},
18231
+ message: effects.permissionDenied.message,
18232
+ source: "upstream",
18233
+ status: upstream.status,
18234
+ upstreamHost: upstreamUrl.hostname,
18235
+ upstreamPath: displayedUpstreamPath(upstreamUrl),
18236
+ ...provider === "github" ? githubPermissionHeaders(upstream) : {}
18237
+ });
18238
+ logWarn(
18239
+ "sandbox_egress_upstream_permission_classified",
18240
+ {},
18241
+ {
18242
+ ...egressAttributes({
18243
+ egressId: activeEgressId,
18244
+ grantAccess: lease.grant.access,
18245
+ grantName: lease.grant.name,
18246
+ grantReason: lease.grant.reason,
18247
+ host: upstreamUrl.hostname,
18248
+ method: request.method,
18249
+ path: upstreamUrl.pathname,
18250
+ provider,
18251
+ status: upstream.status
18252
+ }),
18253
+ ...routingAttributes(request, upstreamUrl),
18254
+ ...upstreamPermissionAttributes(provider, upstream)
18255
+ },
18256
+ "Sandbox egress plugin classified upstream response as permission denied"
18257
+ );
18258
+ }
18259
+ } catch (error) {
18260
+ if (!isEgressAuthRequired(error)) {
18261
+ throw error;
18262
+ }
18263
+ await clearSandboxEgressCredentialLease(
18264
+ provider,
18265
+ lease.grant.name,
18266
+ credentialContext
18267
+ );
18268
+ await setSandboxEgressAuthRequiredSignal(credentialContext, {
18269
+ provider,
18270
+ grant: lease.grant,
18271
+ ...error.authorization ?? lease.authorization ? { authorization: error.authorization ?? lease.authorization } : {},
18272
+ message: error.message
18273
+ });
18274
+ logWarn(
18275
+ "sandbox_egress_upstream_auth_required_classified",
18276
+ {},
18277
+ {
18278
+ ...egressAttributes({
18279
+ egressId: activeEgressId,
18280
+ grantAccess: lease.grant.access,
18281
+ grantName: lease.grant.name,
18282
+ grantReason: lease.grant.reason,
18283
+ host: upstreamUrl.hostname,
18284
+ method: request.method,
18285
+ path: upstreamUrl.pathname,
18286
+ provider,
18287
+ status: upstream.status
18288
+ }),
18289
+ ...routingAttributes(request, upstreamUrl),
18290
+ ...upstreamPermissionAttributes(provider, upstream)
18291
+ },
18292
+ "Sandbox egress plugin classified upstream response as auth required"
18293
+ );
18294
+ await upstream.body?.cancel().catch(() => void 0);
18295
+ return authRequiredResponse({
18296
+ provider,
18297
+ grant: lease.grant,
18298
+ message: error.message
18299
+ });
18300
+ }
17518
18301
  logSandboxEgressUpstreamRequest({
17519
18302
  egressId: activeEgressId,
18303
+ grantAccess: lease.grant.access,
18304
+ grantName: lease.grant.name,
18305
+ grantReason: lease.grant.reason,
17520
18306
  provider,
17521
18307
  request,
17522
18308
  upstream,
@@ -17529,6 +18315,9 @@ ${error.message}`,
17529
18315
  {
17530
18316
  ...egressAttributes({
17531
18317
  egressId: activeEgressId,
18318
+ grantAccess: lease.grant.access,
18319
+ grantName: lease.grant.name,
18320
+ grantReason: lease.grant.reason,
17532
18321
  host: upstreamUrl.hostname,
17533
18322
  method: request.method,
17534
18323
  path: upstreamUrl.pathname,
@@ -17536,6 +18325,7 @@ ${error.message}`,
17536
18325
  status: upstream.status
17537
18326
  }),
17538
18327
  ...routingAttributes(request, upstreamUrl),
18328
+ ...upstreamPermissionAttributes(provider, upstream),
17539
18329
  "error.type": `http_${upstream.status}`
17540
18330
  },
17541
18331
  `Sandbox egress upstream returned HTTP ${upstream.status}`
@@ -17548,6 +18338,9 @@ ${error.message}`,
17548
18338
  {
17549
18339
  ...egressAttributes({
17550
18340
  egressId: activeEgressId,
18341
+ grantAccess: lease.grant.access,
18342
+ grantName: lease.grant.name,
18343
+ grantReason: lease.grant.reason,
17551
18344
  host: upstreamUrl.hostname,
17552
18345
  method: request.method,
17553
18346
  path: upstreamUrl.pathname,
@@ -17555,27 +18348,49 @@ ${error.message}`,
17555
18348
  status: upstream.status
17556
18349
  }),
17557
18350
  ...routingAttributes(request, upstreamUrl),
18351
+ ...upstreamPermissionAttributes(provider, upstream),
17558
18352
  ...upstream.status === UPSTREAM_TOKEN_REJECTION_STATUS ? {
17559
18353
  "app.sandbox.egress.www_authenticate": upstream.headers.get("www-authenticate") ?? void 0
17560
18354
  } : {}
17561
18355
  },
17562
18356
  upstream.status === UPSTREAM_TOKEN_REJECTION_STATUS ? "Sandbox egress upstream auth rejected injected credential" : "Sandbox egress upstream permission denied"
17563
18357
  );
17564
- await clearSandboxEgressCredentialLease(provider, credentialContext);
17565
18358
  if (upstream.status === UPSTREAM_TOKEN_REJECTION_STATUS) {
18359
+ await clearSandboxEgressCredentialLease(
18360
+ provider,
18361
+ lease.grant.name,
18362
+ credentialContext
18363
+ );
18364
+ await setSandboxEgressAuthRequiredSignal(credentialContext, {
18365
+ provider,
18366
+ grant: lease.grant,
18367
+ ...lease.authorization ? { authorization: lease.authorization } : {},
18368
+ message: `Provider rejected the injected ${provider} credential.`
18369
+ });
17566
18370
  await upstream.body?.cancel().catch(() => void 0);
17567
- return new Response(
17568
- `junior-auth-required provider=${provider} 401 unauthorized
17569
- Provider rejected the injected ${provider} credential.
17570
- `,
17571
- {
17572
- status: 401,
17573
- headers: {
17574
- "content-type": "text/plain; charset=utf-8",
17575
- "cache-control": "no-store"
17576
- }
17577
- }
18371
+ return authRequiredResponse({
18372
+ provider,
18373
+ grant: lease.grant,
18374
+ message: `Provider rejected the injected ${provider} credential.
18375
+ `
18376
+ });
18377
+ } else {
18378
+ await clearSandboxEgressCredentialLease(
18379
+ provider,
18380
+ lease.grant.name,
18381
+ credentialContext
17578
18382
  );
18383
+ await setSandboxEgressPermissionDeniedSignal(credentialContext, {
18384
+ provider,
18385
+ grant: lease.grant,
18386
+ ...lease.account ? { account: lease.account } : {},
18387
+ message: permissionDeniedMessage(provider, lease.grant),
18388
+ source: "upstream",
18389
+ status: UPSTREAM_PERMISSION_REJECTION_STATUS,
18390
+ upstreamHost: upstreamUrl.hostname,
18391
+ upstreamPath: displayedUpstreamPath(upstreamUrl),
18392
+ ...provider === "github" ? githubPermissionHeaders(upstream) : {}
18393
+ });
17579
18394
  }
17580
18395
  }
17581
18396
  return new Response(upstream.body, {
@@ -17720,6 +18535,7 @@ async function resumeTimedOutTurn(payload, options = {}) {
17720
18535
  }
17721
18536
  },
17722
18537
  requester,
18538
+ destination: payload.destination,
17723
18539
  correlation: {
17724
18540
  conversationId: payload.conversationId,
17725
18541
  turnId: payload.sessionId,
@@ -17787,6 +18603,7 @@ async function resumeTimedOutTurn(payload, options = {}) {
17787
18603
  }
17788
18604
  await scheduleTurnTimeoutResume2({
17789
18605
  conversationId: payload.conversationId,
18606
+ destination: payload.destination,
17790
18607
  sessionId: payload.sessionId,
17791
18608
  expectedVersion: version
17792
18609
  });
@@ -17864,14 +18681,14 @@ async function POST2(request, waitUntil, options = {}) {
17864
18681
  }
17865
18682
 
17866
18683
  // src/chat/services/subscribed-decision.ts
17867
- import { z as z2 } from "zod";
17868
- var replyDecisionSchema = z2.object({
17869
- should_reply: z2.boolean().describe("Whether Junior should respond to this thread message."),
17870
- should_unsubscribe: z2.boolean().optional().describe(
18684
+ import { z as z4 } from "zod";
18685
+ var replyDecisionSchema = z4.object({
18686
+ should_reply: z4.boolean().describe("Whether Junior should respond to this thread message."),
18687
+ should_unsubscribe: z4.boolean().optional().describe(
17871
18688
  "Whether Junior should unsubscribe from this thread because the user clearly asked it to stop participating."
17872
18689
  ),
17873
- confidence: z2.number().min(0).max(1).describe("Classifier confidence from 0 to 1."),
17874
- reason: z2.string().optional().describe("Short reason for the decision.")
18690
+ confidence: z4.number().min(0).max(1).describe("Classifier confidence from 0 to 1."),
18691
+ reason: z4.string().optional().describe("Short reason for the decision.")
17875
18692
  });
17876
18693
  var ROUTER_CONFIDENCE_THRESHOLD = 0.8;
17877
18694
  var ROUTER_CLASSIFIER_MAX_TOKENS = 240;
@@ -18181,6 +18998,9 @@ async function decideSubscribedThreadReply(args) {
18181
18998
  reasonDetail: reason
18182
18999
  };
18183
19000
  } catch (error) {
19001
+ if (isProviderRetryError(error)) {
19002
+ throw error;
19003
+ }
18184
19004
  args.logClassifierFailure(error, args.input);
18185
19005
  return {
18186
19006
  shouldReply: false,
@@ -18265,6 +19085,9 @@ async function ensureSlackMessageActorIdentity(message, lookupSlackUser2) {
18265
19085
 
18266
19086
  // src/chat/runtime/slack-runtime.ts
18267
19087
  var THREAD_OPTOUT_ACK = "Understood. I'll stay out of this thread unless someone @mentions me again.";
19088
+ function shouldRethrowTurnControlError(error) {
19089
+ return isCooperativeTurnYieldError(error) || isTurnInputCommitLostError(error) || isProviderRetryError(error);
19090
+ }
18268
19091
  async function maybeHandleThreadOptOutDecision(args) {
18269
19092
  if (!args.decision?.shouldUnsubscribe) {
18270
19093
  return false;
@@ -18294,7 +19117,7 @@ function getQueuedMessagesFromSlackMessages(messages, options) {
18294
19117
  );
18295
19118
  }
18296
19119
  function createSteeringMessageDrain(hooks, options) {
18297
- if (!hooks?.drainSteeringMessages) {
19120
+ if (!hooks.drainSteeringMessages) {
18298
19121
  return void 0;
18299
19122
  }
18300
19123
  return async (inject) => {
@@ -18332,7 +19155,7 @@ function createSlackTurnRuntime(deps) {
18332
19155
  if (shouldKeepProcessingReactionForToolInvocation(invocation)) {
18333
19156
  processingReaction.keep();
18334
19157
  }
18335
- hooks?.onToolInvocation?.(invocation);
19158
+ hooks.onToolInvocation?.(invocation);
18336
19159
  };
18337
19160
  };
18338
19161
  const stopProcessingReactions = async (processingReactions) => {
@@ -18423,6 +19246,7 @@ function createSlackTurnRuntime(deps) {
18423
19246
  text: args.text
18424
19247
  });
18425
19248
  }
19249
+ await args.onInputCommitted?.();
18426
19250
  };
18427
19251
  return {
18428
19252
  async handleNewMention(thread, message, hooks) {
@@ -18450,7 +19274,7 @@ function createSlackTurnRuntime(deps) {
18450
19274
  );
18451
19275
  await deps.withSpan("chat.turn", "chat.turn", context, async () => {
18452
19276
  await thread.subscribe();
18453
- const queuedMessages = getQueuedMessages(hooks?.messageContext, {
19277
+ const queuedMessages = getQueuedMessages(hooks.messageContext, {
18454
19278
  explicitMention: true,
18455
19279
  stripLeadingBotMention: deps.stripLeadingBotMention
18456
19280
  });
@@ -18467,7 +19291,7 @@ function createSlackTurnRuntime(deps) {
18467
19291
  );
18468
19292
  };
18469
19293
  const onInputCommitted = async () => {
18470
- await hooks?.onInputCommitted?.();
19294
+ await hooks.onInputCommitted?.();
18471
19295
  await startQueuedProcessingReactions();
18472
19296
  };
18473
19297
  const drainSteeringMessages = createSteeringMessageDrain(hooks, {
@@ -18483,18 +19307,19 @@ function createSlackTurnRuntime(deps) {
18483
19307
  });
18484
19308
  await deps.replyToThread(thread, message, {
18485
19309
  explicitMention: true,
18486
- beforeFirstResponsePost: hooks?.beforeFirstResponsePost,
19310
+ beforeFirstResponsePost: hooks.beforeFirstResponsePost,
19311
+ destination: hooks.destination,
18487
19312
  queuedMessages,
18488
19313
  onInputCommitted,
18489
19314
  onToolInvocation: toolInvocationHook,
18490
19315
  onTurnCompleted,
18491
19316
  drainSteeringMessages,
18492
- onTurnStatePersisted: hooks?.onTurnStatePersisted,
18493
- shouldYield: hooks?.shouldYield
19317
+ onTurnStatePersisted: hooks.onTurnStatePersisted,
19318
+ shouldYield: hooks.shouldYield
18494
19319
  });
18495
19320
  });
18496
19321
  } catch (error) {
18497
- if (isCooperativeTurnYieldError(error) || isTurnInputCommitLostError(error)) {
19322
+ if (shouldRethrowTurnControlError(error)) {
18498
19323
  throw error;
18499
19324
  }
18500
19325
  const errorContext = logContext({
@@ -18526,7 +19351,7 @@ function createSlackTurnRuntime(deps) {
18526
19351
  "Sentry did not return an event ID for mention_handler_failed"
18527
19352
  );
18528
19353
  }
18529
- await hooks?.beforeFirstResponsePost?.();
19354
+ await hooks.beforeFirstResponsePost?.();
18530
19355
  await postFallbackErrorReplyWithLogging({
18531
19356
  thread,
18532
19357
  errorContext,
@@ -18580,7 +19405,7 @@ function createSlackTurnRuntime(deps) {
18580
19405
  channelId,
18581
19406
  runId
18582
19407
  };
18583
- const queuedMessages = getQueuedMessages(hooks?.messageContext, {
19408
+ const queuedMessages = getQueuedMessages(hooks.messageContext, {
18584
19409
  explicitMention: Boolean(message.isMention),
18585
19410
  stripLeadingBotMention: deps.stripLeadingBotMention
18586
19411
  });
@@ -18610,6 +19435,7 @@ function createSlackTurnRuntime(deps) {
18610
19435
  message,
18611
19436
  decision: { shouldReply: false, reason },
18612
19437
  context: threadContext,
19438
+ onInputCommitted: hooks.onInputCommitted,
18613
19439
  text: combinedText
18614
19440
  });
18615
19441
  return;
@@ -18639,13 +19465,14 @@ function createSlackTurnRuntime(deps) {
18639
19465
  if (await maybeHandleThreadOptOutDecision({
18640
19466
  thread,
18641
19467
  decision,
18642
- beforeFirstResponsePost: hooks?.beforeFirstResponsePost
19468
+ beforeFirstResponsePost: hooks.beforeFirstResponsePost
18643
19469
  })) {
18644
19470
  await skipSubscribedMessage({
18645
19471
  thread,
18646
19472
  message,
18647
19473
  decision,
18648
19474
  context: threadContext,
19475
+ onInputCommitted: hooks.onInputCommitted,
18649
19476
  preparedState,
18650
19477
  text: combinedText
18651
19478
  });
@@ -18657,6 +19484,7 @@ function createSlackTurnRuntime(deps) {
18657
19484
  message,
18658
19485
  decision,
18659
19486
  context: threadContext,
19487
+ onInputCommitted: hooks.onInputCommitted,
18660
19488
  preparedState,
18661
19489
  text: combinedText
18662
19490
  });
@@ -18679,7 +19507,7 @@ function createSlackTurnRuntime(deps) {
18679
19507
  );
18680
19508
  };
18681
19509
  const onInputCommitted = async () => {
18682
- await hooks?.onInputCommitted?.();
19510
+ await hooks.onInputCommitted?.();
18683
19511
  await startQueuedProcessingReactions();
18684
19512
  };
18685
19513
  const toolInvocationHook = createToolInvocationHook(
@@ -18688,19 +19516,20 @@ function createSlackTurnRuntime(deps) {
18688
19516
  );
18689
19517
  await deps.replyToThread(thread, message, {
18690
19518
  explicitMention: Boolean(message.isMention),
19519
+ destination: hooks.destination,
18691
19520
  preparedState,
18692
- beforeFirstResponsePost: hooks?.beforeFirstResponsePost,
19521
+ beforeFirstResponsePost: hooks.beforeFirstResponsePost,
18693
19522
  queuedMessages,
18694
19523
  onInputCommitted,
18695
19524
  onToolInvocation: toolInvocationHook,
18696
19525
  onTurnCompleted,
18697
19526
  drainSteeringMessages,
18698
- onTurnStatePersisted: hooks?.onTurnStatePersisted,
18699
- shouldYield: hooks?.shouldYield
19527
+ onTurnStatePersisted: hooks.onTurnStatePersisted,
19528
+ shouldYield: hooks.shouldYield
18700
19529
  });
18701
19530
  });
18702
19531
  } catch (error) {
18703
- if (isCooperativeTurnYieldError(error) || isTurnInputCommitLostError(error)) {
19532
+ if (shouldRethrowTurnControlError(error)) {
18704
19533
  throw error;
18705
19534
  }
18706
19535
  const errorContext = logContext({
@@ -18732,7 +19561,7 @@ function createSlackTurnRuntime(deps) {
18732
19561
  "Sentry did not return an event ID for subscribed_message_handler_failed"
18733
19562
  );
18734
19563
  }
18735
- await hooks?.beforeFirstResponsePost?.();
19564
+ await hooks.beforeFirstResponsePost?.();
18736
19565
  await postFallbackErrorReplyWithLogging({
18737
19566
  thread,
18738
19567
  errorContext,
@@ -19857,7 +20686,7 @@ async function loadPiMessagesForTurn(args) {
19857
20686
  return { piMessages: fallback };
19858
20687
  }
19859
20688
  function createReplyToThread(deps) {
19860
- return async function replyToThread(thread, message, options = {}) {
20689
+ return async function replyToThread(thread, message, options) {
19861
20690
  if (message.author.isMe) {
19862
20691
  return;
19863
20692
  }
@@ -19872,7 +20701,8 @@ function createReplyToThread(deps) {
19872
20701
  const threadTs = getThreadTs(threadId);
19873
20702
  const assistantThreadContext = getAssistantThreadContext(message);
19874
20703
  const messageTs = getMessageTs(message);
19875
- const teamId = getTeamId(message);
20704
+ const destination = options.destination;
20705
+ const teamId = destination.teamId;
19876
20706
  const runId = getRunId(thread, message);
19877
20707
  const conversationId = threadId ?? runId;
19878
20708
  await withSpan(
@@ -20080,27 +20910,58 @@ function createReplyToThread(deps) {
20080
20910
  updateConversationStats
20081
20911
  });
20082
20912
  if (conversationId) {
20913
+ const turnStartedAtMs = message.metadata.dateSent.getTime();
20083
20914
  void recordAgentTurnSessionSummary({
20084
20915
  channelName,
20085
20916
  conversationId,
20086
20917
  sessionId: turnId,
20087
20918
  sliceId: 1,
20088
- startedAtMs: message.metadata.dateSent.getTime(),
20919
+ startedAtMs: turnStartedAtMs,
20089
20920
  state: "running",
20090
20921
  surface: "slack",
20091
20922
  requester,
20923
+ destination,
20092
20924
  traceId: getActiveTraceId()
20093
20925
  }).catch((error) => {
20094
20926
  logException(
20095
20927
  error,
20096
20928
  "agent_turn_summary_record_failed",
20097
20929
  turnTraceContext,
20098
- {
20099
- "app.agent.turn.state": "running"
20100
- },
20930
+ { "app.agent.turn.state": "running" },
20101
20931
  "Failed to record running turn summary"
20102
20932
  );
20103
20933
  });
20934
+ void initConversationContext(conversationId, {
20935
+ channelName,
20936
+ originSurface: "slack",
20937
+ originRequester: requester,
20938
+ startedAtMs: turnStartedAtMs
20939
+ }).catch((error) => {
20940
+ logException(
20941
+ error,
20942
+ "conversation_details_context_init_failed",
20943
+ turnTraceContext,
20944
+ { "app.agent.turn.state": "running" },
20945
+ "Failed to init conversation context at turn start"
20946
+ );
20947
+ });
20948
+ const existingAssistantTitle = preparedState.artifacts.assistantTitle?.trim();
20949
+ if (existingAssistantTitle) {
20950
+ void setConversationTitle(conversationId, {
20951
+ displayTitle: existingAssistantTitle,
20952
+ ...preparedState.artifacts.assistantTitleSourceMessageId ? {
20953
+ titleSourceMessageId: preparedState.artifacts.assistantTitleSourceMessageId
20954
+ } : {}
20955
+ }).catch((error) => {
20956
+ logException(
20957
+ error,
20958
+ "conversation_details_title_refresh_failed",
20959
+ turnTraceContext,
20960
+ { "app.agent.turn.state": "running" },
20961
+ "Failed to refresh conversation title from artifacts"
20962
+ );
20963
+ });
20964
+ }
20104
20965
  }
20105
20966
  setTags({
20106
20967
  conversationId
@@ -20176,6 +21037,7 @@ function createReplyToThread(deps) {
20176
21037
  let persistedAtLeastOnce = false;
20177
21038
  let shouldPersistFailureState = true;
20178
21039
  let latestArtifacts = preparedState.artifacts;
21040
+ let assistantTitleArtifacts = {};
20179
21041
  try {
20180
21042
  const loadedPiMessages = await loadPiMessagesForTurn({
20181
21043
  conversationId,
@@ -20218,6 +21080,54 @@ function createReplyToThread(deps) {
20218
21080
  runId,
20219
21081
  threadId
20220
21082
  });
21083
+ void assistantTitleTask.then(async (titleUpdateResult) => {
21084
+ if (!titleUpdateResult) return;
21085
+ assistantTitleArtifacts = {
21086
+ assistantTitleSourceMessageId: titleUpdateResult.sourceMessageId,
21087
+ ...titleUpdateResult.title ? { assistantTitle: titleUpdateResult.title } : {}
21088
+ };
21089
+ latestArtifacts = {
21090
+ ...latestArtifacts,
21091
+ ...assistantTitleArtifacts
21092
+ };
21093
+ if (conversationId && titleUpdateResult.title) {
21094
+ try {
21095
+ await setConversationTitle(conversationId, {
21096
+ displayTitle: titleUpdateResult.title,
21097
+ titleSourceMessageId: titleUpdateResult.sourceMessageId
21098
+ });
21099
+ } catch (error) {
21100
+ logException(
21101
+ error,
21102
+ "conversation_details_title_set_failed",
21103
+ turnTraceContext,
21104
+ {},
21105
+ "Failed to set conversation title in details record"
21106
+ );
21107
+ }
21108
+ }
21109
+ try {
21110
+ await persistThreadState(thread, {
21111
+ artifacts: latestArtifacts
21112
+ });
21113
+ } catch (error) {
21114
+ logException(
21115
+ error,
21116
+ "assistant_title_artifact_persist_failed",
21117
+ turnTraceContext,
21118
+ {},
21119
+ "Failed to persist async assistant title artifact state"
21120
+ );
21121
+ }
21122
+ }).catch((error) => {
21123
+ logException(
21124
+ error,
21125
+ "assistant_title_task_failed",
21126
+ turnTraceContext,
21127
+ {},
21128
+ "Async assistant title task failed"
21129
+ );
21130
+ });
20221
21131
  const toolChannelId = preparedState.artifacts.assistantContextChannelId ?? channelId;
20222
21132
  const resolveSteeringMessages = async (queuedMessages) => {
20223
21133
  return await Promise.all(
@@ -20269,6 +21179,7 @@ function createReplyToThread(deps) {
20269
21179
  omittedImageAttachmentCount,
20270
21180
  userAttachments,
20271
21181
  slackConversation,
21182
+ destination,
20272
21183
  surface: "slack",
20273
21184
  turnDeadlineAtMs: getTurnRequestDeadline()?.deadlineAtMs,
20274
21185
  correlation: {
@@ -20295,8 +21206,13 @@ function createReplyToThread(deps) {
20295
21206
  });
20296
21207
  },
20297
21208
  onArtifactStateUpdated: async (artifacts) => {
20298
- latestArtifacts = artifacts;
20299
- await persistThreadState(thread, { artifacts });
21209
+ latestArtifacts = {
21210
+ ...artifacts,
21211
+ ...assistantTitleArtifacts
21212
+ };
21213
+ await persistThreadState(thread, {
21214
+ artifacts: latestArtifacts
21215
+ });
20300
21216
  },
20301
21217
  onAuthPending: async (pendingAuth) => {
20302
21218
  await applyPendingAuthUpdate({
@@ -20397,24 +21313,26 @@ function createReplyToThread(deps) {
20397
21313
  await sent.delete();
20398
21314
  }
20399
21315
  }
20400
- const titleUpdateResult = await assistantTitleTask;
20401
- if (titleUpdateResult) {
20402
- artifactStatePatch.assistantTitleSourceMessageId = titleUpdateResult.sourceMessageId;
20403
- if (titleUpdateResult.title) {
20404
- artifactStatePatch.assistantTitle = titleUpdateResult.title;
20405
- }
20406
- }
20407
21316
  const completedState = buildDeliveredTurnStatePatch({
20408
- artifactStatePatch,
20409
- artifacts: preparedState.artifacts,
21317
+ artifactStatePatch: {
21318
+ ...artifactStatePatch,
21319
+ ...assistantTitleArtifacts
21320
+ },
21321
+ artifacts: latestArtifacts,
20410
21322
  conversation: preparedState.conversation,
20411
21323
  reply,
20412
21324
  sessionId: turnId,
20413
21325
  userMessageId: preparedState.userMessageId
20414
21326
  });
21327
+ if (completedState.artifacts) {
21328
+ latestArtifacts = completedState.artifacts;
21329
+ }
20415
21330
  await persistThreadState(thread, {
20416
21331
  ...completedState
20417
21332
  });
21333
+ if (completedState.artifacts && (assistantTitleArtifacts.assistantTitle !== void 0 || assistantTitleArtifacts.assistantTitleSourceMessageId !== void 0) && (completedState.artifacts.assistantTitle !== assistantTitleArtifacts.assistantTitle || completedState.artifacts.assistantTitleSourceMessageId !== assistantTitleArtifacts.assistantTitleSourceMessageId)) {
21334
+ await persistThreadState(thread, { artifacts: latestArtifacts });
21335
+ }
20418
21336
  if (conversationId) {
20419
21337
  await recordAgentTurnSessionSummary({
20420
21338
  channelName,
@@ -20425,8 +21343,8 @@ function createReplyToThread(deps) {
20425
21343
  sliceId: 1,
20426
21344
  startedAtMs: message.metadata.dateSent.getTime(),
20427
21345
  state: "completed",
20428
- conversationTitle: titleUpdateResult?.title,
20429
21346
  requester,
21347
+ destination,
20430
21348
  traceId: getActiveTraceId()
20431
21349
  });
20432
21350
  }
@@ -20467,10 +21385,11 @@ function createReplyToThread(deps) {
20467
21385
  const conversationIdForResume = error.metadata?.conversationId;
20468
21386
  const sessionIdForResume = error.metadata?.sessionId;
20469
21387
  const version = error.metadata?.version;
20470
- if (conversationIdForResume && sessionIdForResume && typeof version === "number") {
21388
+ if (conversationIdForResume && sessionIdForResume && typeof version === "number" && destination) {
20471
21389
  try {
20472
21390
  await deps.services.scheduleTurnTimeoutResume({
20473
21391
  conversationId: conversationIdForResume,
21392
+ destination,
20474
21393
  sessionId: sessionIdForResume,
20475
21394
  expectedVersion: version
20476
21395
  });
@@ -20578,6 +21497,7 @@ function createReplyToThread(deps) {
20578
21497
  startedAtMs: message.metadata.dateSent.getTime(),
20579
21498
  state: "failed",
20580
21499
  requester,
21500
+ destination,
20581
21501
  traceId: getActiveTraceId()
20582
21502
  });
20583
21503
  const sessionRecord = await getAgentTurnSessionRecord(
@@ -21016,7 +21936,7 @@ function nonEmptyString(value) {
21016
21936
  return trimmed || void 0;
21017
21937
  }
21018
21938
 
21019
- // src/chat/queue/thread-message-dispatcher.ts
21939
+ // src/chat/slack/attachment-fetchers.ts
21020
21940
  function rehydrateAttachmentFetchers(message, downloadPrivateSlackFile2 = downloadPrivateSlackFile) {
21021
21941
  for (const attachment of message.attachments) {
21022
21942
  if (!attachment.fetchData && attachment.url) {
@@ -21316,6 +22236,7 @@ function createSlackConversationWorker(options) {
21316
22236
  try {
21317
22237
  if (route === "mention") {
21318
22238
  await options.runtime.handleNewMention(thread, latestMessage, {
22239
+ destination: context.destination,
21319
22240
  messageContext,
21320
22241
  drainSteeringMessages,
21321
22242
  onInputCommitted,
@@ -21324,6 +22245,7 @@ function createSlackConversationWorker(options) {
21324
22245
  return;
21325
22246
  }
21326
22247
  await options.runtime.handleSubscribedMessage(thread, latestMessage, {
22248
+ destination: context.destination,
21327
22249
  messageContext,
21328
22250
  drainSteeringMessages,
21329
22251
  onInputCommitted,
@@ -21348,8 +22270,16 @@ function createSlackConversationWorker(options) {
21348
22270
  }
21349
22271
  function buildSlackInboundMessage(args) {
21350
22272
  const authorId = requireSlackAuthorId(args.message);
22273
+ const destination = createSlackDestination({
22274
+ channelId: args.thread.channelId,
22275
+ teamId: args.installation?.teamId
22276
+ });
22277
+ if (!destination) {
22278
+ throw new Error("Slack inbound message requires destination context");
22279
+ }
21351
22280
  return {
21352
22281
  conversationId: args.conversationId,
22282
+ destination,
21353
22283
  inboundMessageId: [
21354
22284
  "slack",
21355
22285
  args.installation?.teamId ?? args.installation?.enterpriseId ?? "unknown",
@@ -22256,7 +23186,11 @@ async function POST3(request, platform, waitUntil) {
22256
23186
  }
22257
23187
 
22258
23188
  // src/chat/task-execution/vercel-callback.ts
22259
- import { handleCallback } from "@vercel/queue";
23189
+ import {
23190
+ handleCallback,
23191
+ QueueClient,
23192
+ registerDevConsumer
23193
+ } from "@vercel/queue";
22260
23194
 
22261
23195
  // src/chat/task-execution/worker.ts
22262
23196
  var CONVERSATION_WORK_DEFER_DELAY_MS = 15e3;
@@ -22269,7 +23203,10 @@ function nudgeIdempotencyKey(reason, conversationId, nowMs) {
22269
23203
  }
22270
23204
  async function sendWakeNudge(args) {
22271
23205
  await args.options.queue.send(
22272
- { conversationId: args.conversationId },
23206
+ {
23207
+ conversationId: args.conversationId,
23208
+ destination: args.destination
23209
+ },
22273
23210
  {
22274
23211
  delayMs: args.delayMs,
22275
23212
  idempotencyKey: args.idempotencyKey
@@ -22284,6 +23221,7 @@ async function sendWakeNudge(args) {
22284
23221
  async function requestLostLeaseRecovery(args) {
22285
23222
  const continuationMarked = await requestConversationContinuation({
22286
23223
  conversationId: args.conversationId,
23224
+ destination: args.destination,
22287
23225
  leaseToken: args.leaseToken,
22288
23226
  nowMs: args.nowMs,
22289
23227
  state: args.options.state
@@ -22302,6 +23240,7 @@ async function requestLostLeaseRecovery(args) {
22302
23240
  }
22303
23241
  await sendWakeNudge({
22304
23242
  conversationId: args.conversationId,
23243
+ destination: args.destination,
22305
23244
  idempotencyKey: nudgeIdempotencyKey(
22306
23245
  "lost_lease",
22307
23246
  args.conversationId,
@@ -22345,7 +23284,8 @@ function startLeaseCheckIn(args) {
22345
23284
  timer.unref?.();
22346
23285
  return timer;
22347
23286
  }
22348
- async function processConversationWork(conversationId, options) {
23287
+ async function processConversationWork(message, options) {
23288
+ const conversationId = message.conversationId;
22349
23289
  const initial = await getConversationWorkState({
22350
23290
  conversationId,
22351
23291
  state: options.state
@@ -22353,6 +23293,12 @@ async function processConversationWork(conversationId, options) {
22353
23293
  if (!initial || countPendingConversationMessages(initial) === 0 && !initial.needsRun && !initial.lease) {
22354
23294
  return { status: "no_work" };
22355
23295
  }
23296
+ if (!sameDestination(initial.destination, message.destination)) {
23297
+ throw new Error(
23298
+ `Conversation work queue destination changed for ${conversationId}`
23299
+ );
23300
+ }
23301
+ const destination = initial.destination;
22356
23302
  const lease = await startConversationWork({
22357
23303
  conversationId,
22358
23304
  nowMs: now2(options),
@@ -22365,6 +23311,7 @@ async function processConversationWork(conversationId, options) {
22365
23311
  const nudgeNowMs = now2(options);
22366
23312
  await sendWakeNudge({
22367
23313
  conversationId,
23314
+ destination,
22368
23315
  delayMs: CONVERSATION_WORK_DEFER_DELAY_MS,
22369
23316
  idempotencyKey: nudgeIdempotencyKey("active", conversationId, nudgeNowMs),
22370
23317
  nowMs: nudgeNowMs,
@@ -22403,6 +23350,7 @@ async function processConversationWork(conversationId, options) {
22403
23350
  );
22404
23351
  const workerContext = {
22405
23352
  conversationId,
23353
+ destination,
22406
23354
  leaseToken: lease.leaseToken,
22407
23355
  shouldYield: () => leaseLost || now2(options) >= softYieldDeadlineMs,
22408
23356
  checkIn: async () => {
@@ -22430,6 +23378,7 @@ async function processConversationWork(conversationId, options) {
22430
23378
  if (result.status === "lost_lease") {
22431
23379
  await requestLostLeaseRecovery({
22432
23380
  conversationId,
23381
+ destination,
22433
23382
  leaseToken: lease.leaseToken,
22434
23383
  nowMs: now2(options),
22435
23384
  options
@@ -22439,6 +23388,7 @@ async function processConversationWork(conversationId, options) {
22439
23388
  if (leaseLost) {
22440
23389
  await requestLostLeaseRecovery({
22441
23390
  conversationId,
23391
+ destination,
22442
23392
  leaseToken: lease.leaseToken,
22443
23393
  nowMs: now2(options),
22444
23394
  options
@@ -22449,6 +23399,7 @@ async function processConversationWork(conversationId, options) {
22449
23399
  const yieldNowMs = now2(options);
22450
23400
  const continuationMarked = await requestConversationContinuation({
22451
23401
  conversationId,
23402
+ destination,
22452
23403
  leaseToken: lease.leaseToken,
22453
23404
  nowMs: yieldNowMs,
22454
23405
  state: options.state
@@ -22458,6 +23409,7 @@ async function processConversationWork(conversationId, options) {
22458
23409
  }
22459
23410
  await sendWakeNudge({
22460
23411
  conversationId,
23412
+ destination,
22461
23413
  idempotencyKey: nudgeIdempotencyKey(
22462
23414
  "yield",
22463
23415
  conversationId,
@@ -22496,6 +23448,7 @@ async function processConversationWork(conversationId, options) {
22496
23448
  const nudgeNowMs = now2(options);
22497
23449
  await sendWakeNudge({
22498
23450
  conversationId,
23451
+ destination,
22499
23452
  idempotencyKey: nudgeIdempotencyKey(
22500
23453
  "pending",
22501
23454
  conversationId,
@@ -22520,6 +23473,7 @@ async function processConversationWork(conversationId, options) {
22520
23473
  try {
22521
23474
  const continuationMarked = await requestConversationContinuation({
22522
23475
  conversationId,
23476
+ destination,
22523
23477
  leaseToken: lease.leaseToken,
22524
23478
  nowMs: errorNowMs,
22525
23479
  state: options.state
@@ -22527,6 +23481,7 @@ async function processConversationWork(conversationId, options) {
22527
23481
  if (continuationMarked) {
22528
23482
  await sendWakeNudge({
22529
23483
  conversationId,
23484
+ destination,
22530
23485
  idempotencyKey: nudgeIdempotencyKey(
22531
23486
  "error",
22532
23487
  conversationId,
@@ -22561,15 +23516,17 @@ async function processConversationWork(conversationId, options) {
22561
23516
  "Conversation work release failed after runner error"
22562
23517
  );
22563
23518
  }
22564
- logException(
22565
- error,
22566
- "conversation_work_failed",
22567
- { conversationId },
22568
- {
22569
- "app.worker.elapsed_ms": now2(options) - startedAtMs
22570
- },
22571
- "Conversation work failed"
22572
- );
23519
+ if (!isProviderRetryError(error)) {
23520
+ logException(
23521
+ error,
23522
+ "conversation_work_failed",
23523
+ { conversationId },
23524
+ {
23525
+ "app.worker.elapsed_ms": now2(options) - startedAtMs
23526
+ },
23527
+ "Conversation work failed"
23528
+ );
23529
+ }
22573
23530
  throw error;
22574
23531
  } finally {
22575
23532
  clearInterval(timer);
@@ -22578,12 +23535,19 @@ async function processConversationWork(conversationId, options) {
22578
23535
 
22579
23536
  // src/chat/task-execution/vercel-callback.ts
22580
23537
  var CONVERSATION_WORK_VISIBILITY_TIMEOUT_BUFFER_SECONDS = 30;
23538
+ var CONVERSATION_WORK_DEV_CONSUMER_GROUP = "junior_conversation_work_dev";
22581
23539
  function parseConversationQueueMessage(message) {
22582
- if (!message || typeof message !== "object" || typeof message.conversationId !== "string" || !message.conversationId.trim()) {
22583
- throw new Error("Conversation queue message is missing conversationId");
23540
+ const destination = parseDestination(
23541
+ message?.destination
23542
+ );
23543
+ if (!message || typeof message !== "object" || typeof message.conversationId !== "string" || !message.conversationId.trim() || !destination) {
23544
+ throw new Error(
23545
+ "Conversation queue message is missing destination context"
23546
+ );
22584
23547
  }
22585
23548
  return {
22586
- conversationId: message.conversationId
23549
+ conversationId: message.conversationId,
23550
+ destination
22587
23551
  };
22588
23552
  }
22589
23553
  function resolveConversationWorkVisibilityTimeoutSeconds(functionMaxDurationSeconds = getChatConfig().functionMaxDurationSeconds) {
@@ -22591,7 +23555,7 @@ function resolveConversationWorkVisibilityTimeoutSeconds(functionMaxDurationSeco
22591
23555
  }
22592
23556
  async function processConversationQueueMessage(message, options) {
22593
23557
  const parsed = parseConversationQueueMessage(message);
22594
- return await processConversationWork(parsed.conversationId, {
23558
+ return await processConversationWork(parsed, {
22595
23559
  checkInIntervalMs: options.checkInIntervalMs,
22596
23560
  nowMs: options.nowMs,
22597
23561
  queue: options.queue ?? getVercelConversationWorkQueue(),
@@ -22600,22 +23564,35 @@ async function processConversationQueueMessage(message, options) {
22600
23564
  state: options.state
22601
23565
  });
22602
23566
  }
23567
+ async function handleConversationQueueMessage(message, options) {
23568
+ const verified = verifySignedConversationQueueMessage(message);
23569
+ if (!verified) {
23570
+ throw new Error("Unauthorized conversation queue message");
23571
+ }
23572
+ await runWithTurnRequestDeadline(
23573
+ () => processConversationQueueMessage(verified, options)
23574
+ );
23575
+ }
22603
23576
  function createVercelConversationWorkCallback(options) {
22604
23577
  return handleCallback(
22605
- async (message) => {
22606
- const verified = verifySignedConversationQueueMessage(message);
22607
- if (!verified) {
22608
- throw new Error("Unauthorized conversation queue message");
22609
- }
22610
- await runWithTurnRequestDeadline(
22611
- () => processConversationQueueMessage(verified, options)
22612
- );
22613
- },
23578
+ (message) => handleConversationQueueMessage(message, options),
22614
23579
  {
22615
23580
  visibilityTimeoutSeconds: options.visibilityTimeoutSeconds ?? resolveConversationWorkVisibilityTimeoutSeconds()
22616
23581
  }
22617
23582
  );
22618
23583
  }
23584
+ function registerVercelConversationWorkDevConsumer(options) {
23585
+ if (process.env.NODE_ENV !== "development") {
23586
+ return void 0;
23587
+ }
23588
+ return registerDevConsumer({
23589
+ client: new QueueClient(),
23590
+ consumerGroup: CONVERSATION_WORK_DEV_CONSUMER_GROUP,
23591
+ handler: (message) => handleConversationQueueMessage(message, options),
23592
+ topic: resolveConversationWorkQueueTopic(options),
23593
+ visibilityTimeoutSeconds: options.visibilityTimeoutSeconds ?? resolveConversationWorkVisibilityTimeoutSeconds()
23594
+ });
23595
+ }
22619
23596
 
22620
23597
  // src/app.ts
22621
23598
  async function defaultWaitUntil() {
@@ -22638,7 +23615,7 @@ async function resolveVirtualConfig() {
22638
23615
  return {
22639
23616
  pluginSet: mod.pluginSet,
22640
23617
  plugins: mod.plugins,
22641
- trustedPluginRegistrations: mod.trustedPluginRegistrations ?? []
23618
+ pluginHookRegistrations: mod.pluginHookRegistrations ?? []
22642
23619
  };
22643
23620
  } catch (error) {
22644
23621
  if (!isMissingVirtualConfig(error)) {
@@ -22707,20 +23684,20 @@ function validateBuildIncludesPluginPackages(pluginConfig, virtualConfig) {
22707
23684
  `createApp() registered plugin package(s) not bundled by juniorNitro(): ${missing.join(", ")}. Point juniorNitro({ plugins: "./plugins" }) at the runtime plugin module or pass the same defineJuniorPlugins(...) set to juniorNitro({ plugins }) and createApp({ plugins }).`
22708
23685
  );
22709
23686
  }
22710
- function validateBuildIncludesTrustedRegistrations(trustedRegistrations, virtualConfig) {
22711
- const bundledTrustedRegistrations = virtualConfig?.trustedPluginRegistrations ?? [];
22712
- if (bundledTrustedRegistrations.length === 0) {
23687
+ function validateBuildIncludesPluginHookRegistrations(hookRegistrations, virtualConfig) {
23688
+ const bundledHookRegistrations = virtualConfig?.pluginHookRegistrations ?? [];
23689
+ if (bundledHookRegistrations.length === 0) {
22713
23690
  return;
22714
23691
  }
22715
- const registered = new Set(trustedRegistrations.map((plugin) => plugin.name));
22716
- const missing = bundledTrustedRegistrations.filter(
23692
+ const registered = new Set(hookRegistrations.map((plugin) => plugin.name));
23693
+ const missing = bundledHookRegistrations.filter(
22717
23694
  (pluginName) => !registered.has(pluginName)
22718
23695
  );
22719
23696
  if (missing.length === 0) {
22720
23697
  return;
22721
23698
  }
22722
23699
  throw new Error(
22723
- `createApp() is missing trusted plugin registration(s) bundled by juniorNitro(): ${missing.join(", ")}. Pass a runtime-safe plugin module to juniorNitro({ plugins: "./plugins" }) or pass the same defineJuniorPlugins(...) set to createApp({ plugins }).`
23700
+ `createApp() is missing plugin registration(s) with runtime hooks bundled by juniorNitro(): ${missing.join(", ")}. Pass a runtime-safe plugin module to juniorNitro({ plugins: "./plugins" }) or pass the same defineJuniorPlugins(...) set to createApp({ plugins }).`
22724
23701
  );
22725
23702
  }
22726
23703
  function validatePluginRegistrations(registrations) {
@@ -22736,6 +23713,51 @@ function validatePluginRegistrations(registrations) {
22736
23713
  }
22737
23714
  }
22738
23715
  }
23716
+ function validatePluginEgressCredentialHooks(registrations) {
23717
+ const plugins = new Map(
23718
+ registrations.map((registration) => [registration.name, registration])
23719
+ );
23720
+ for (const provider of getPluginProviders()) {
23721
+ const hooks = plugins.get(provider.manifest.name)?.hooks;
23722
+ const hasGrantHook = Boolean(hooks?.grantForEgress);
23723
+ const hasIssueHook = Boolean(hooks?.issueCredential);
23724
+ const hasGenericCredentials = Boolean(
23725
+ provider.manifest.credentials || provider.manifest.apiHeaders
23726
+ );
23727
+ const hasDomains = Boolean(provider.manifest.domains?.length);
23728
+ const hasHookManagedOAuth = Boolean(
23729
+ provider.manifest.oauth && !provider.manifest.credentials
23730
+ );
23731
+ if (!hasGrantHook && !hasIssueHook) {
23732
+ if (hasDomains && !hasGenericCredentials) {
23733
+ throw new Error(
23734
+ `Plugin "${provider.manifest.name}" manifest.domains requires egress credential hooks when no generic credentials or apiHeaders are configured.`
23735
+ );
23736
+ }
23737
+ if (hasHookManagedOAuth) {
23738
+ throw new Error(
23739
+ `Plugin "${provider.manifest.name}" manifest.oauth without oauth-bearer credentials requires egress credential hooks.`
23740
+ );
23741
+ }
23742
+ continue;
23743
+ }
23744
+ if (!hasGrantHook || !hasIssueHook) {
23745
+ throw new Error(
23746
+ `Plugin "${provider.manifest.name}" egress credential hooks must include both grantForEgress and issueCredential.`
23747
+ );
23748
+ }
23749
+ if (hasGenericCredentials) {
23750
+ throw new Error(
23751
+ `Plugin "${provider.manifest.name}" egress credential hooks must use manifest.domains instead of generic credentials or apiHeaders.`
23752
+ );
23753
+ }
23754
+ if (!hasDomains) {
23755
+ throw new Error(
23756
+ `Plugin "${provider.manifest.name}" egress credential hooks require manifest.domains to list sandbox egress hosts.`
23757
+ );
23758
+ }
23759
+ }
23760
+ }
22739
23761
  function mountAgentPluginRoutes(app, routes) {
22740
23762
  for (const route of routes) {
22741
23763
  const handler = (c) => route.handler(c.req.raw);
@@ -22753,12 +23775,12 @@ function mountAgentPluginRoutes(app, routes) {
22753
23775
  async function createApp(options) {
22754
23776
  const virtualConfig = await resolveVirtualConfig();
22755
23777
  const configuredPlugins = options?.plugins ?? virtualConfig?.pluginSet;
22756
- const agentPlugins = trustedPluginRegistrationsFromPluginSet(configuredPlugins);
23778
+ const agentPlugins = pluginHookRegistrationsFromPluginSet(configuredPlugins);
22757
23779
  const pluginConfig = configuredPlugins ? pluginCatalogConfigFromPluginSet(configuredPlugins) : virtualConfig?.plugins ?? resolveEnvPluginCatalogConfig();
22758
23780
  if (configuredPlugins) {
22759
23781
  validateBuildIncludesPluginPackages(pluginConfig, virtualConfig);
22760
23782
  }
22761
- validateBuildIncludesTrustedRegistrations(agentPlugins, virtualConfig);
23783
+ validateBuildIncludesPluginHookRegistrations(agentPlugins, virtualConfig);
22762
23784
  validateAgentPlugins(agentPlugins);
22763
23785
  const shouldValidatePluginCatalog = hasConfiguredPluginCatalog(pluginConfig) || Boolean(configuredPlugins?.registrations.length) || Boolean(Object.keys(options?.configDefaults ?? {}).length);
22764
23786
  const previousPluginCatalogConfig = setPluginCatalogConfig(pluginConfig);
@@ -22774,6 +23796,9 @@ async function createApp(options) {
22774
23796
  if (shouldValidatePluginCatalog) {
22775
23797
  getPluginCatalogSignature();
22776
23798
  validatePluginRegistrations(configuredPlugins?.registrations ?? []);
23799
+ validatePluginEgressCredentialHooks(
23800
+ configuredPlugins?.registrations ?? []
23801
+ );
22777
23802
  }
22778
23803
  agentPluginRoutes = getAgentPluginRoutes();
22779
23804
  } catch (error) {
@@ -22811,9 +23836,17 @@ async function createApp(options) {
22811
23836
  return POST(c.req.raw, waitUntil);
22812
23837
  });
22813
23838
  let agentContinuePOST;
23839
+ let conversationWorkOptions;
23840
+ const getConversationWorkOptions = () => {
23841
+ conversationWorkOptions ??= options?.conversationWork ?? getProductionConversationWorkOptions();
23842
+ return conversationWorkOptions;
23843
+ };
23844
+ if (process.env.NODE_ENV === "development") {
23845
+ registerVercelConversationWorkDevConsumer(getConversationWorkOptions());
23846
+ }
22814
23847
  app.post("/api/internal/agent/continue", (c) => {
22815
23848
  agentContinuePOST ??= createVercelConversationWorkCallback(
22816
- options?.conversationWork ?? getProductionConversationWorkOptions()
23849
+ getConversationWorkOptions()
22817
23850
  );
22818
23851
  return agentContinuePOST(c.req.raw);
22819
23852
  });