@sentry/junior 0.68.0 → 0.69.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 (65) hide show
  1. package/dist/app.js +1464 -732
  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 +34 -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 +33 -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.d.ts +1 -0
  37. package/dist/chat/state/turn-session.d.ts +4 -0
  38. package/dist/chat/task-execution/queue.d.ts +2 -0
  39. package/dist/chat/task-execution/store.d.ts +5 -0
  40. package/dist/chat/task-execution/vercel-callback.d.ts +4 -0
  41. package/dist/chat/task-execution/vercel-queue.d.ts +2 -0
  42. package/dist/chat/task-execution/worker.d.ts +4 -2
  43. package/dist/chat/tools/slack/context.d.ts +3 -0
  44. package/dist/chat/tools/types.d.ts +21 -2
  45. package/dist/chunk-76YMBKW7.js +326 -0
  46. package/dist/{chunk-PIVOJIUD.js → chunk-B5HKWWQB.js} +9 -5
  47. package/dist/chunk-BBXYXOJW.js +1858 -0
  48. package/dist/{chunk-V47RLIO2.js → chunk-GT67ZWZQ.js} +4 -4
  49. package/dist/{chunk-75UZ4JLC.js → chunk-IGLNC5H6.js} +21 -9
  50. package/dist/{chunk-EBVQXCD2.js → chunk-JS4HURDT.js} +362 -280
  51. package/dist/{chunk-UQQSW7QB.js → chunk-N3MORKTH.js} +74 -331
  52. package/dist/chunk-R62YWUNO.js +264 -0
  53. package/dist/{chunk-OIIXZOOC.js → chunk-UXG6TU2U.js} +311 -2015
  54. package/dist/cli/check.js +4 -4
  55. package/dist/cli/snapshot-warmup.js +5 -4
  56. package/dist/nitro.d.ts +1 -1
  57. package/dist/nitro.js +21 -19
  58. package/dist/plugins.d.ts +2 -2
  59. package/dist/reporting.d.ts +2 -2
  60. package/dist/reporting.js +13 -11
  61. package/package.json +6 -4
  62. package/dist/chat/plugins/auth/github-app-broker.d.ts +0 -4
  63. package/dist/chat/plugins/github-permissions.d.ts +0 -11
  64. package/dist/chat/queue/thread-message-dispatcher.d.ts +0 -33
  65. 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,23 +18,17 @@ 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,
31
22
  listAgentTurnSessionSummaries,
32
23
  listAgentTurnSessionSummariesForConversation,
33
24
  loadConnectedMcpProviders,
34
25
  loadProjection,
35
- normalizeSlackConversationId,
36
26
  normalizeSlackStatusText,
37
27
  recordAgentTurnSessionSummary,
38
28
  recordAuthorizationCompleted,
39
29
  recordAuthorizationRequested,
40
30
  recordMcpProviderConnected,
31
+ resolveChannelCapabilities,
41
32
  resolveSlackChannelTypeFromMessage,
42
33
  resolveSlackConversationContext,
43
34
  setAgentPlugins,
@@ -45,15 +36,14 @@ import {
45
36
  truncateStatusText,
46
37
  upsertAgentTurnSessionRecord,
47
38
  validateAgentPlugins,
48
- verifySlackDirectCredentialSubject,
49
- withSlackRetries
50
- } from "./chunk-UQQSW7QB.js";
39
+ verifySlackDirectCredentialSubject
40
+ } from "./chunk-N3MORKTH.js";
51
41
  import {
52
42
  discoverSkills,
53
43
  findSkillByName,
54
44
  loadSkillsByName,
55
45
  parseSkillInvocation
56
- } from "./chunk-V47RLIO2.js";
46
+ } from "./chunk-GT67ZWZQ.js";
57
47
  import {
58
48
  buildNonInteractiveShellScript,
59
49
  createSandboxInstance,
@@ -62,104 +52,143 @@ import {
62
52
  isSnapshotMissingError,
63
53
  resolveRuntimeDependencySnapshot,
64
54
  runNonInteractiveCommand
65
- } from "./chunk-PIVOJIUD.js";
55
+ } from "./chunk-B5HKWWQB.js";
66
56
  import {
67
57
  ACTIVE_LOCK_TTL_MS,
58
+ SANDBOX_DATA_ROOT,
59
+ SANDBOX_SKILLS_ROOT,
60
+ SANDBOX_WORKSPACE_ROOT,
61
+ getStateAdapter,
62
+ sandboxSkillDir,
63
+ sandboxSkillFile
64
+ } from "./chunk-R62YWUNO.js";
65
+ import {
66
+ CredentialUnavailableError,
67
+ buildActorIdentity,
68
+ buildOAuthTokenRequest,
69
+ createPluginBroker,
70
+ credentialContextSchema,
71
+ getPluginCapabilityProviders,
72
+ getPluginCatalogSignature,
73
+ getPluginDefinition,
74
+ getPluginMcpProviders,
75
+ getPluginOAuthConfig,
76
+ getPluginProviders,
77
+ hasRequiredOAuthScope,
78
+ isActorUserId,
79
+ isPluginConfigKey,
80
+ isPluginProvider,
81
+ parseActorUserId,
82
+ parseOAuthTokenResponse,
83
+ resolveAuthTokenPlaceholder,
84
+ resolvePluginCommandEnv,
85
+ setPluginCatalogConfig,
86
+ slackActorIdentity
87
+ } from "./chunk-UXG6TU2U.js";
88
+ import {
89
+ defineJuniorPlugins,
90
+ getVercelConversationWorkQueue,
91
+ pluginCatalogConfigFromPluginSet,
92
+ pluginHookRegistrationsFromPluginSet,
93
+ resolveConversationWorkQueueTopic,
94
+ verifySignedConversationQueueMessage
95
+ } from "./chunk-IGLNC5H6.js";
96
+ import {
97
+ SlackActionError,
98
+ createSlackDestination,
99
+ destinationKey,
100
+ downloadPrivateSlackFile,
101
+ getFilePermalink,
102
+ getHeaderString,
103
+ getSlackClient,
104
+ isConversationScopedChannel,
105
+ isDmChannel,
106
+ normalizeSlackConversationId,
107
+ parseDestination,
108
+ sameDestination,
109
+ withSlackRetries
110
+ } from "./chunk-76YMBKW7.js";
111
+ import {
68
112
  FUNCTION_TIMEOUT_BUFFER_SECONDS,
69
113
  GEN_AI_PROVIDER_NAME,
70
114
  GEN_AI_SERVER_ADDRESS,
71
115
  GEN_AI_SERVER_PORT,
72
116
  MISSING_GATEWAY_CREDENTIALS_ERROR,
73
- SANDBOX_DATA_ROOT,
74
- SANDBOX_SKILLS_ROOT,
75
- SANDBOX_WORKSPACE_ROOT,
76
117
  botConfig,
118
+ buildUserTurnText,
77
119
  completeObject,
78
120
  completeText,
121
+ encodeNonImageAttachmentForPrompt,
122
+ extractAssistantText,
79
123
  getChatConfig,
80
124
  getGatewayApiKey,
81
125
  getPiGatewayApiKeyOverride,
126
+ getPiMessageRole,
82
127
  getRuntimeMetadata,
128
+ getSessionIdentifiers,
83
129
  getSlackBotToken,
84
130
  getSlackClientId,
85
131
  getSlackClientSecret,
86
132
  getSlackReactionConfig,
87
133
  getSlackSigningSecret,
88
- getStateAdapter,
134
+ getTerminalAssistantMessages,
135
+ hasRuntimeTurnContext,
136
+ isAssistantMessage,
137
+ isExecutionEscapeResponse,
138
+ isProviderRetryError,
139
+ isRawToolPayloadResponse,
140
+ isToolResultError,
141
+ isToolResultMessage,
142
+ nextProviderRetry,
89
143
  normalizeSlackEmojiName,
144
+ normalizeToolNameFromResult,
90
145
  parseSlackThreadId,
146
+ prependMissingRuntimeTurnContext,
91
147
  resolveConversationPrivacy,
92
148
  resolveGatewayModel,
93
149
  resolveSlackChannelIdFromMessage,
94
150
  resolveSlackChannelIdFromThreadId,
95
- sandboxSkillDir,
96
- sandboxSkillFile,
97
151
  setSlackReactionConfig,
152
+ stripRuntimeTurnContext,
153
+ summarizeMessageText,
98
154
  toGenAiMessageMetadata,
99
155
  toGenAiMessagesTraceAttributes,
100
156
  toGenAiPayloadMetadata,
101
157
  toGenAiPayloadTraceAttributes,
102
- toGenAiTextMetadata
103
- } from "./chunk-EBVQXCD2.js";
158
+ toGenAiTextMetadata,
159
+ toObservablePromptPart,
160
+ trimTrailingAssistantMessages,
161
+ upsertActiveSkill
162
+ } from "./chunk-JS4HURDT.js";
104
163
  import {
105
- CredentialUnavailableError,
106
- buildActorIdentity,
107
- buildOAuthTokenRequest,
108
164
  buildTurnFailureResponse,
109
165
  createChatSdkLogger,
110
- createPluginBroker,
111
166
  createRequestContext,
112
167
  extractGenAiUsageAttributes,
113
168
  extractGenAiUsageSummary,
114
169
  getActiveTraceId,
115
170
  getLogContextAttributes,
116
- getPluginCapabilityProviders,
117
- getPluginCatalogSignature,
118
- getPluginDefinition,
119
- getPluginMcpProviders,
120
- getPluginOAuthConfig,
121
- getPluginProviders,
122
- hasRequiredOAuthScope,
123
- isActorUserId,
124
- isPluginConfigKey,
125
- isPluginProvider,
171
+ homeDir,
126
172
  isRecord,
173
+ listReferenceFiles,
127
174
  logError,
128
175
  logException,
129
176
  logInfo,
130
177
  logWarn,
131
178
  normalizeGenAiFinishReason,
132
- parseActorUserId,
133
- parseCredentialContext,
134
- parseOAuthTokenResponse,
135
- resolveAuthTokenPlaceholder,
136
- resolvePluginCommandEnv,
137
179
  serializeGenAiAttribute,
138
- setPluginCatalogConfig,
139
180
  setSentryUser,
140
181
  setSpanAttributes,
141
182
  setSpanStatus,
142
183
  setTags,
143
- slackActorIdentity,
144
184
  toOptionalNumber,
145
185
  toOptionalString,
146
186
  withContext,
147
187
  withSpan
148
- } from "./chunk-OIIXZOOC.js";
188
+ } from "./chunk-BBXYXOJW.js";
149
189
  import {
150
190
  sentry_exports
151
191
  } 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
192
  import "./chunk-2KG3PWR4.js";
164
193
 
165
194
  // src/app.ts
@@ -3240,6 +3269,11 @@ async function listThreadReplies(input) {
3240
3269
  return replies.slice(0, targetLimit);
3241
3270
  }
3242
3271
 
3272
+ // src/chat/tools/slack/context.ts
3273
+ function getSlackDeliveryChannelId(context) {
3274
+ return context.deliveryChannelId ?? context.channelId;
3275
+ }
3276
+
3243
3277
  // src/chat/tools/slack/channel-list-messages.ts
3244
3278
  function createSlackChannelListMessagesTool(context) {
3245
3279
  return tool({
@@ -3292,7 +3326,7 @@ function createSlackChannelListMessagesTool(context) {
3292
3326
  inclusive,
3293
3327
  max_pages
3294
3328
  }) => {
3295
- const targetChannelId = context.channelId;
3329
+ const targetChannelId = getSlackDeliveryChannelId(context);
3296
3330
  if (!targetChannelId) {
3297
3331
  return {
3298
3332
  ok: false,
@@ -3600,7 +3634,7 @@ function createSlackChannelPostMessageTool(context, state) {
3600
3634
  })
3601
3635
  }),
3602
3636
  execute: async ({ text }) => {
3603
- const targetChannelId = context.channelId;
3637
+ const targetChannelId = getSlackDeliveryChannelId(context);
3604
3638
  if (!targetChannelId) {
3605
3639
  return {
3606
3640
  ok: false,
@@ -3971,7 +4005,7 @@ function createSlackCanvasCreateTool(context, state) {
3971
4005
  })
3972
4006
  }),
3973
4007
  execute: async ({ title, markdown }) => {
3974
- const targetChannelId = context.channelId;
4008
+ const targetChannelId = getSlackDeliveryChannelId(context);
3975
4009
  if (!isConversationScopedChannel(targetChannelId)) {
3976
4010
  logError(
3977
4011
  "slack_canvas_create_invalid_context",
@@ -4860,7 +4894,10 @@ function createSlackThreadReadTool(context) {
4860
4894
  error: "Provide either a Slack message `url` or both `channel_id` and `ts`."
4861
4895
  };
4862
4896
  }
4863
- const access = checkChannelAccess(channelId, context.channelId);
4897
+ const access = checkChannelAccess(
4898
+ channelId,
4899
+ getSlackDeliveryChannelId(context)
4900
+ );
4864
4901
  if (!access.allowed) {
4865
4902
  return {
4866
4903
  ok: false,
@@ -5205,263 +5242,6 @@ import {
5205
5242
  } from "@earendil-works/pi-agent-core";
5206
5243
  import { Type as Type21 } from "@sinclair/typebox";
5207
5244
 
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
5245
  // src/chat/state/ttl.ts
5466
5246
  var JUNIOR_THREAD_STATE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
5467
5247
 
@@ -6431,18 +6211,20 @@ function createTools(availableSkills, hooks = {}, context) {
6431
6211
  tools.searchMcpTools = createSearchMcpToolsTool(context.mcpToolManager);
6432
6212
  tools.callMcpTool = createCallMcpToolTool(context.mcpToolManager);
6433
6213
  }
6434
- const { channelCapabilities } = context;
6435
- if (channelCapabilities.canCreateCanvas) {
6214
+ const outputChannelId = getSlackDeliveryChannelId(context);
6215
+ const outputCapabilities = resolveChannelCapabilities(outputChannelId);
6216
+ const rawChannelCapabilities = resolveChannelCapabilities(context.channelId);
6217
+ if (outputCapabilities.canCreateCanvas) {
6436
6218
  tools.slackCanvasCreate = createSlackCanvasCreateTool(context, state);
6437
6219
  }
6438
- if (channelCapabilities.canPostToChannel) {
6220
+ if (outputCapabilities.canPostToChannel) {
6439
6221
  tools.slackChannelPostMessage = createSlackChannelPostMessageTool(
6440
6222
  context,
6441
6223
  state
6442
6224
  );
6443
6225
  tools.slackChannelListMessages = createSlackChannelListMessagesTool(context);
6444
6226
  }
6445
- if (channelCapabilities.canAddReactions) {
6227
+ if (rawChannelCapabilities.canAddReactions) {
6446
6228
  tools.slackMessageAddReaction = createSlackMessageAddReactionTool(
6447
6229
  context,
6448
6230
  state
@@ -6452,24 +6234,13 @@ function createTools(availableSkills, hooks = {}, context) {
6452
6234
  getAgentPluginTools(context)
6453
6235
  )) {
6454
6236
  if (tools[name]) {
6455
- throw new Error(
6456
- `Trusted plugin tool "${name}" conflicts with a core tool`
6457
- );
6237
+ throw new Error(`Plugin tool "${name}" conflicts with a core tool`);
6458
6238
  }
6459
6239
  tools[name] = pluginTool;
6460
6240
  }
6461
6241
  return tools;
6462
6242
  }
6463
6243
 
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
6244
  // src/chat/pi/traced-stream.ts
6474
6245
  import {
6475
6246
  streamSimple
@@ -6581,6 +6352,33 @@ import fs3 from "fs/promises";
6581
6352
  // src/chat/oauth-flow.ts
6582
6353
  import { randomBytes } from "crypto";
6583
6354
  var OAUTH_STATE_TTL_MS = 10 * 60 * 1e3;
6355
+ function optionalString(value) {
6356
+ return typeof value === "string" ? value : void 0;
6357
+ }
6358
+ function parseOAuthStatePayload(value) {
6359
+ if (!isRecord(value)) {
6360
+ return void 0;
6361
+ }
6362
+ if (typeof value.userId !== "string" || typeof value.provider !== "string") {
6363
+ return void 0;
6364
+ }
6365
+ const destination = parseDestination(value.destination);
6366
+ if (value.destination !== void 0 && !destination) {
6367
+ return void 0;
6368
+ }
6369
+ return {
6370
+ userId: value.userId,
6371
+ provider: value.provider,
6372
+ ...optionalString(value.channelId) ? { channelId: optionalString(value.channelId) } : {},
6373
+ ...destination ? { destination } : {},
6374
+ ...optionalString(value.threadTs) ? { threadTs: optionalString(value.threadTs) } : {},
6375
+ ...optionalString(value.pendingMessage) ? { pendingMessage: optionalString(value.pendingMessage) } : {},
6376
+ ...isRecord(value.configuration) ? { configuration: value.configuration } : {},
6377
+ ...optionalString(value.resumeConversationId) ? { resumeConversationId: optionalString(value.resumeConversationId) } : {},
6378
+ ...optionalString(value.resumeSessionId) ? { resumeSessionId: optionalString(value.resumeSessionId) } : {},
6379
+ ...optionalString(value.scope) ? { scope: optionalString(value.scope) } : {}
6380
+ };
6381
+ }
6584
6382
  function formatProviderLabel(provider) {
6585
6383
  return provider.charAt(0).toUpperCase() + provider.slice(1);
6586
6384
  }
@@ -6684,17 +6482,20 @@ async function startOAuthFlow(provider, input) {
6684
6482
  }
6685
6483
  const configuration = input.userMessage && input.channelConfiguration ? await input.channelConfiguration.resolveValues() : void 0;
6686
6484
  const state = randomBytes(32).toString("hex");
6485
+ const requestedScope = input.scope ?? providerConfig.scope;
6687
6486
  await getStateAdapter().set(
6688
6487
  `oauth-state:${state}`,
6689
6488
  {
6690
6489
  userId: input.requesterId,
6691
6490
  provider,
6692
6491
  ...input.channelId ? { channelId: input.channelId } : {},
6492
+ ...input.destination ? { destination: input.destination } : {},
6693
6493
  ...input.threadTs ? { threadTs: input.threadTs } : {},
6694
6494
  ...input.userMessage ? { pendingMessage: input.userMessage } : {},
6695
6495
  ...configuration && Object.keys(configuration).length > 0 ? { configuration } : {},
6696
6496
  ...input.resumeConversationId ? { resumeConversationId: input.resumeConversationId } : {},
6697
- ...input.resumeSessionId ? { resumeSessionId: input.resumeSessionId } : {}
6497
+ ...input.resumeSessionId ? { resumeSessionId: input.resumeSessionId } : {},
6498
+ ...requestedScope ? { scope: requestedScope } : {}
6698
6499
  },
6699
6500
  OAUTH_STATE_TTL_MS
6700
6501
  );
@@ -6704,8 +6505,8 @@ async function startOAuthFlow(provider, input) {
6704
6505
  redirect_uri: `${baseUrl}${providerConfig.callbackPath}`,
6705
6506
  response_type: "code"
6706
6507
  });
6707
- if (providerConfig.scope) {
6708
- authorizeParams.set("scope", providerConfig.scope);
6508
+ if (requestedScope) {
6509
+ authorizeParams.set("scope", requestedScope);
6709
6510
  }
6710
6511
  for (const [key, value] of Object.entries(
6711
6512
  providerConfig.authorizeParams ?? {}
@@ -6734,22 +6535,87 @@ async function startOAuthFlow(provider, input) {
6734
6535
 
6735
6536
  // src/chat/sandbox/egress-session.ts
6736
6537
  import { createHmac, randomUUID, timingSafeEqual } from "crypto";
6538
+
6539
+ // src/chat/sandbox/egress-schemas.ts
6540
+ import { z } from "zod";
6541
+ import {
6542
+ agentPluginAuthorizationSchema,
6543
+ agentPluginCredentialHeaderTransformSchema,
6544
+ agentPluginGrantSchema,
6545
+ agentPluginProviderAccountSchema
6546
+ } from "@sentry/junior-plugin-api";
6547
+ var finiteNumberSchema = z.number().refine(Number.isFinite);
6548
+ var providerNameSchema = z.string().regex(/^[a-z][a-z0-9-]*$/);
6549
+ var sandboxEgressGrantSchema = agentPluginGrantSchema;
6550
+ var sandboxEgressCredentialContextSchema = z.object({
6551
+ credentials: credentialContextSchema,
6552
+ egressId: z.string().min(1),
6553
+ expiresAtMs: finiteNumberSchema,
6554
+ contextId: z.string().min(1)
6555
+ }).strict();
6556
+ var sandboxEgressCredentialLeaseSchema = z.object({
6557
+ account: agentPluginProviderAccountSchema.optional(),
6558
+ authorization: agentPluginAuthorizationSchema.optional(),
6559
+ grant: sandboxEgressGrantSchema,
6560
+ provider: providerNameSchema,
6561
+ expiresAt: z.string().min(1),
6562
+ headerTransforms: z.array(agentPluginCredentialHeaderTransformSchema).min(1)
6563
+ }).strict();
6564
+ var sandboxEgressAuthRequiredSignalSchema = z.object({
6565
+ authorization: agentPluginAuthorizationSchema.optional(),
6566
+ grant: sandboxEgressGrantSchema,
6567
+ provider: providerNameSchema,
6568
+ message: z.string().optional(),
6569
+ createdAtMs: finiteNumberSchema
6570
+ }).strict().superRefine((signal, ctx) => {
6571
+ if (signal.authorization && signal.authorization.provider !== signal.provider) {
6572
+ ctx.addIssue({
6573
+ code: z.ZodIssueCode.custom,
6574
+ message: "Auth signal authorization provider must match provider",
6575
+ path: ["authorization", "provider"]
6576
+ });
6577
+ }
6578
+ });
6579
+ var sandboxEgressPermissionDeniedSignalSchema = z.object({
6580
+ account: agentPluginProviderAccountSchema.optional(),
6581
+ acceptedPermissions: z.string().optional(),
6582
+ grant: sandboxEgressGrantSchema,
6583
+ message: z.string().min(1),
6584
+ provider: providerNameSchema,
6585
+ source: z.literal("upstream"),
6586
+ sso: z.string().optional(),
6587
+ status: z.literal(403),
6588
+ upstreamHost: z.string().min(1),
6589
+ upstreamPath: z.string().min(1),
6590
+ createdAtMs: finiteNumberSchema
6591
+ }).strict();
6592
+ function parseSandboxEgressAuthRequiredSignal(value) {
6593
+ const result = sandboxEgressAuthRequiredSignalSchema.safeParse(value);
6594
+ return result.success ? result.data : void 0;
6595
+ }
6596
+ function parseSandboxEgressPermissionDeniedSignal(value) {
6597
+ const result = sandboxEgressPermissionDeniedSignalSchema.safeParse(value);
6598
+ return result.success ? result.data : void 0;
6599
+ }
6600
+
6601
+ // src/chat/sandbox/egress-session.ts
6737
6602
  var SANDBOX_EGRESS_PROXY_PATH = "/api/internal/sandbox-egress";
6738
6603
  var SANDBOX_EGRESS_TOKEN_VERSION = "v1";
6739
6604
  var SANDBOX_EGRESS_HMAC_CONTEXT = "junior.sandbox_egress.v1";
6605
+ var SANDBOX_EGRESS_AUTH_SIGNAL_PREFIX = "sandbox-egress-auth-required";
6606
+ var SANDBOX_EGRESS_PERMISSION_SIGNAL_PREFIX = "sandbox-egress-permission-denied";
6740
6607
  var SANDBOX_EGRESS_LEASE_PREFIX = "sandbox-egress-lease";
6741
6608
  var DEFAULT_SESSION_TTL_MS = 30 * 60 * 1e3;
6742
- function leaseKey(provider, context) {
6609
+ function leaseKey(provider, grantName, context) {
6743
6610
  const actor = context.credentials.actor;
6744
6611
  const actorKey = actor.type === "user" ? `user:${actor.userId}` : `system:${actor.id}`;
6745
- return `${SANDBOX_EGRESS_LEASE_PREFIX}:${provider}:${actorKey}:${context.egressId}:${context.contextId}`;
6612
+ return `${SANDBOX_EGRESS_LEASE_PREFIX}:${provider}:${grantName}:${actorKey}:${context.egressId}:${context.contextId}`;
6746
6613
  }
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)");
6614
+ function authSignalKey(egressId, access) {
6615
+ return `${SANDBOX_EGRESS_AUTH_SIGNAL_PREFIX}:${egressId}:${access}`;
6616
+ }
6617
+ function permissionSignalKey(egressId, access) {
6618
+ return `${SANDBOX_EGRESS_PERMISSION_SIGNAL_PREFIX}:${egressId}:${access}`;
6753
6619
  }
6754
6620
  function base64Url(input) {
6755
6621
  return Buffer.from(input, "utf8").toString("base64url");
@@ -6769,49 +6635,32 @@ function timingSafeMatch(expected, actual) {
6769
6635
  return timingSafeEqual(expectedBuffer, actualBuffer);
6770
6636
  }
6771
6637
  function parseSandboxEgressContext(value) {
6772
- if (!value || typeof value !== "object") {
6638
+ const result = sandboxEgressCredentialContextSchema.safeParse(value);
6639
+ if (!result.success) {
6773
6640
  return void 0;
6774
6641
  }
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()) {
6642
+ if (result.data.expiresAtMs <= Date.now()) {
6781
6643
  return void 0;
6782
6644
  }
6783
- return {
6784
- credentials,
6785
- egressId: record.egressId,
6786
- expiresAtMs: record.expiresAtMs,
6787
- contextId: record.contextId
6788
- };
6645
+ return result.data;
6789
6646
  }
6790
6647
  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)) {
6648
+ const result = sandboxEgressCredentialLeaseSchema.safeParse(value);
6649
+ if (!result.success) {
6796
6650
  return void 0;
6797
6651
  }
6798
- const expiresAtMs = Date.parse(record.expiresAt);
6652
+ const expiresAtMs = Date.parse(result.data.expiresAt);
6799
6653
  if (!Number.isFinite(expiresAtMs) || expiresAtMs <= Date.now()) {
6800
6654
  return void 0;
6801
6655
  }
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;
6656
+ return result.data;
6657
+ }
6658
+ function getSandboxEgressSecret() {
6659
+ const secret = process.env.JUNIOR_SECRET?.trim();
6660
+ if (secret) {
6661
+ return secret;
6809
6662
  }
6810
- return {
6811
- provider: record.provider,
6812
- expiresAt: record.expiresAt,
6813
- headerTransforms
6814
- };
6663
+ throw new Error("Cannot determine sandbox egress secret (set JUNIOR_SECRET)");
6815
6664
  }
6816
6665
  function createSandboxEgressCredentialToken(input) {
6817
6666
  const ttlMs = Math.max(1, input.ttlMs ?? DEFAULT_SESSION_TTL_MS);
@@ -6861,17 +6710,94 @@ async function setSandboxEgressCredentialLease(context, lease) {
6861
6710
  );
6862
6711
  const state = getStateAdapter();
6863
6712
  await state.connect();
6864
- await state.set(leaseKey(lease.provider, context), lease, ttlMs);
6865
- }
6866
- async function getSandboxEgressCredentialLease(provider, context) {
6867
- const state = getStateAdapter();
6713
+ await state.set(
6714
+ leaseKey(lease.provider, lease.grant.name, context),
6715
+ lease,
6716
+ ttlMs
6717
+ );
6718
+ }
6719
+ async function getSandboxEgressCredentialLease(provider, grantName, context) {
6720
+ const state = getStateAdapter();
6721
+ await state.connect();
6722
+ return parseLease(await state.get(leaseKey(provider, grantName, context)));
6723
+ }
6724
+ async function clearSandboxEgressCredentialLease(provider, grantName, context) {
6725
+ const state = getStateAdapter();
6726
+ await state.connect();
6727
+ await state.delete(leaseKey(provider, grantName, context));
6728
+ }
6729
+ async function setSandboxEgressAuthRequiredSignal(context, signal) {
6730
+ const ttlMs = Math.max(1, context.expiresAtMs - Date.now());
6731
+ const state = getStateAdapter();
6732
+ await state.connect();
6733
+ await state.set(
6734
+ authSignalKey(context.egressId, signal.grant.access),
6735
+ {
6736
+ ...signal,
6737
+ createdAtMs: Date.now()
6738
+ },
6739
+ ttlMs
6740
+ );
6741
+ }
6742
+ async function setSandboxEgressPermissionDeniedSignal(context, signal) {
6743
+ const ttlMs = Math.max(1, context.expiresAtMs - Date.now());
6744
+ const state = getStateAdapter();
6745
+ await state.connect();
6746
+ await state.set(
6747
+ permissionSignalKey(context.egressId, signal.grant.access),
6748
+ {
6749
+ ...signal,
6750
+ createdAtMs: Date.now()
6751
+ },
6752
+ ttlMs
6753
+ );
6754
+ }
6755
+ async function clearSandboxEgressSignals(egressId) {
6756
+ if (!egressId) {
6757
+ return;
6758
+ }
6759
+ const state = getStateAdapter();
6760
+ await state.connect();
6761
+ await Promise.all([
6762
+ state.delete(authSignalKey(egressId, "read")),
6763
+ state.delete(authSignalKey(egressId, "write")),
6764
+ state.delete(permissionSignalKey(egressId, "read")),
6765
+ state.delete(permissionSignalKey(egressId, "write"))
6766
+ ]);
6767
+ }
6768
+ async function consumeSandboxEgressAuthRequiredSignal(egressId) {
6769
+ if (!egressId) {
6770
+ return void 0;
6771
+ }
6772
+ const state = getStateAdapter();
6868
6773
  await state.connect();
6869
- return parseLease(await state.get(leaseKey(provider, context)));
6774
+ const [writeSignal, readSignal] = await Promise.all([
6775
+ state.get(authSignalKey(egressId, "write")),
6776
+ state.get(authSignalKey(egressId, "read"))
6777
+ ]);
6778
+ const signal = parseSandboxEgressAuthRequiredSignal(writeSignal) ?? parseSandboxEgressAuthRequiredSignal(readSignal);
6779
+ await Promise.all([
6780
+ state.delete(authSignalKey(egressId, "read")),
6781
+ state.delete(authSignalKey(egressId, "write"))
6782
+ ]);
6783
+ return signal;
6870
6784
  }
6871
- async function clearSandboxEgressCredentialLease(provider, context) {
6785
+ async function consumeSandboxEgressPermissionDeniedSignal(egressId) {
6786
+ if (!egressId) {
6787
+ return void 0;
6788
+ }
6872
6789
  const state = getStateAdapter();
6873
6790
  await state.connect();
6874
- await state.delete(leaseKey(provider, context));
6791
+ const [writeSignal, readSignal] = await Promise.all([
6792
+ state.get(permissionSignalKey(egressId, "write")),
6793
+ state.get(permissionSignalKey(egressId, "read"))
6794
+ ]);
6795
+ const signal = parseSandboxEgressPermissionDeniedSignal(writeSignal) ?? parseSandboxEgressPermissionDeniedSignal(readSignal);
6796
+ await Promise.all([
6797
+ state.delete(permissionSignalKey(egressId, "read")),
6798
+ state.delete(permissionSignalKey(egressId, "write"))
6799
+ ]);
6800
+ return signal;
6875
6801
  }
6876
6802
 
6877
6803
  // src/chat/sandbox/egress-policy.ts
@@ -6929,7 +6855,7 @@ async function resolveSandboxCommandEnvironment() {
6929
6855
  )) {
6930
6856
  Object.assign(env, resolvePluginCommandEnv(plugin.manifest));
6931
6857
  const credentials = plugin.manifest.credentials;
6932
- if (credentials) {
6858
+ if (credentials?.authTokenEnv) {
6933
6859
  env[credentials.authTokenEnv] = resolveAuthTokenPlaceholder(credentials);
6934
6860
  }
6935
6861
  }
@@ -7829,10 +7755,6 @@ function createSandboxSessionManager(options) {
7829
7755
  let timeoutId;
7830
7756
  let onAbort;
7831
7757
  try {
7832
- if (input.signal?.aborted) {
7833
- return getCommandAbortedResult();
7834
- }
7835
- await refreshNetworkPolicy(sandboxInstance);
7836
7758
  if (input.signal?.aborted) {
7837
7759
  return getCommandAbortedResult();
7838
7760
  }
@@ -7943,6 +7865,9 @@ function createSandboxSessionManager(options) {
7943
7865
  getSandboxId() {
7944
7866
  return sandbox ? sandbox.sandboxId : sandboxIdHint;
7945
7867
  },
7868
+ getSandboxEgressId() {
7869
+ return sandbox?.sandboxEgressId;
7870
+ },
7946
7871
  getDependencyProfileHash() {
7947
7872
  return dependencyProfileHash;
7948
7873
  },
@@ -8075,6 +8000,8 @@ function createSandboxExecutor(options) {
8075
8000
  "app.sandbox.command_length": command.length
8076
8001
  });
8077
8002
  const executeBash = (await sessionManager.ensureToolExecutors()).bash;
8003
+ const activeEgressId = sessionManager.getSandboxEgressId();
8004
+ await clearSandboxEgressSignals(activeEgressId);
8078
8005
  const result = await withSandboxSpan(
8079
8006
  "bash",
8080
8007
  "process.exec",
@@ -8112,6 +8039,8 @@ function createSandboxExecutor(options) {
8112
8039
  }
8113
8040
  }
8114
8041
  );
8042
+ const authRequired = result.exitCode !== 0 ? await consumeSandboxEgressAuthRequiredSignal(activeEgressId) : void 0;
8043
+ const permissionDenied = result.exitCode !== 0 ? await consumeSandboxEgressPermissionDeniedSignal(activeEgressId) : void 0;
8115
8044
  return {
8116
8045
  result: {
8117
8046
  ok: result.exitCode === 0,
@@ -8123,7 +8052,9 @@ function createSandboxExecutor(options) {
8123
8052
  stdout: result.stdout,
8124
8053
  stderr: result.stderr,
8125
8054
  stdout_truncated: result.stdoutTruncated,
8126
- stderr_truncated: result.stderrTruncated
8055
+ stderr_truncated: result.stderrTruncated,
8056
+ ...authRequired ? { auth_required: authRequired } : {},
8057
+ ...permissionDenied ? { permission_denied: permissionDenied } : {}
8127
8058
  }
8128
8059
  };
8129
8060
  };
@@ -8544,13 +8475,90 @@ function toToolContentText(value) {
8544
8475
  return String(value);
8545
8476
  }
8546
8477
  }
8478
+ function isRecord3(value) {
8479
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
8480
+ }
8481
+ function stringField(record, key) {
8482
+ const value = record[key];
8483
+ return typeof value === "string" ? value : "";
8484
+ }
8485
+ function stringListField(record, key) {
8486
+ const value = record[key];
8487
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
8488
+ }
8489
+ function accountText(value) {
8490
+ if (!isRecord3(value)) {
8491
+ return void 0;
8492
+ }
8493
+ const label = stringField(value, "label") || stringField(value, "id");
8494
+ const id = stringField(value, "id");
8495
+ if (!label) {
8496
+ return void 0;
8497
+ }
8498
+ return id && id !== label ? `${label} (${id})` : label;
8499
+ }
8500
+ function upstreamPermissionDeniedText(value) {
8501
+ if (!isRecord3(value) || !isRecord3(value.permission_denied)) {
8502
+ return void 0;
8503
+ }
8504
+ const signal = value.permission_denied;
8505
+ if (signal.source !== "upstream" || signal.status !== 403) {
8506
+ return void 0;
8507
+ }
8508
+ const provider = stringField(signal, "provider");
8509
+ const message = stringField(signal, "message");
8510
+ const upstreamHost = stringField(signal, "upstreamHost");
8511
+ const upstreamPath2 = stringField(signal, "upstreamPath");
8512
+ if (!provider || !message || !upstreamHost || !upstreamPath2) {
8513
+ return void 0;
8514
+ }
8515
+ const grant = isRecord3(signal.grant) ? signal.grant : {};
8516
+ const grantName = stringField(grant, "name");
8517
+ const grantAccess = stringField(grant, "access");
8518
+ const grantReason = stringField(grant, "reason");
8519
+ const grantRequirements = stringListField(grant, "requirements");
8520
+ const account = accountText(signal.account);
8521
+ const command = stringField(value, "command");
8522
+ const stderr = stringField(value, "stderr").trim();
8523
+ const stdout = stringField(value, "stdout").trim();
8524
+ const acceptedPermissions = stringField(signal, "acceptedPermissions");
8525
+ const sso = stringField(signal, "sso");
8526
+ return [
8527
+ "Upstream permission denied.",
8528
+ message,
8529
+ "",
8530
+ `Provider: ${provider}`,
8531
+ ...account ? [`Provider account: ${account}`] : [],
8532
+ `Grant: ${grantName || "unknown"}${grantAccess ? ` (${grantAccess}${grantReason ? `, ${grantReason}` : ""})` : ""}`,
8533
+ ...grantRequirements.length > 0 ? [
8534
+ "Provider requirements:",
8535
+ ...grantRequirements.map((item) => `- ${item}`)
8536
+ ] : [],
8537
+ `Upstream: ${upstreamHost}${upstreamPath2}`,
8538
+ "Status: 403",
8539
+ ...acceptedPermissions ? [`Accepted provider permissions: ${acceptedPermissions}`] : [],
8540
+ ...sso ? [`Provider SSO: ${sso}`] : [],
8541
+ ...command ? [`Command: ${command}`] : [],
8542
+ "",
8543
+ "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.",
8544
+ ...stderr ? ["", `stderr:
8545
+ ${stderr}`] : [],
8546
+ ...stdout ? ["", `stdout:
8547
+ ${stdout}`] : []
8548
+ ].join("\n");
8549
+ }
8547
8550
  function normalizeToolResult(result, isSandboxResult) {
8548
8551
  const unwrapped = isSandboxResult && result && typeof result === "object" && "result" in result ? result.result : result;
8549
8552
  if (isStructuredToolExecutionResult(unwrapped)) {
8550
8553
  return unwrapped;
8551
8554
  }
8552
8555
  return {
8553
- content: [{ type: "text", text: toToolContentText(unwrapped) }],
8556
+ content: [
8557
+ {
8558
+ type: "text",
8559
+ text: upstreamPermissionDeniedText(unwrapped) ?? toToolContentText(unwrapped)
8560
+ }
8561
+ ],
8554
8562
  details: unwrapped
8555
8563
  };
8556
8564
  }
@@ -8609,11 +8617,16 @@ function parseMcpAuthSession(value) {
8609
8617
  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
8618
  return void 0;
8611
8619
  }
8620
+ const destination = parsed.destination === void 0 ? void 0 : parseDestination(parsed.destination);
8621
+ if (parsed.destination !== void 0 && !destination) {
8622
+ return void 0;
8623
+ }
8612
8624
  return {
8613
8625
  authSessionId: parsed.authSessionId,
8614
8626
  provider: parsed.provider,
8615
8627
  userId: parsed.userId,
8616
8628
  conversationId: parsed.conversationId,
8629
+ ...destination ? { destination } : {},
8617
8630
  sessionId: parsed.sessionId,
8618
8631
  userMessage: parsed.userMessage,
8619
8632
  createdAtMs: parsed.createdAtMs,
@@ -8709,6 +8722,7 @@ async function patchMcpAuthSession(authSessionId, patch) {
8709
8722
  provider: current.provider,
8710
8723
  userId: current.userId,
8711
8724
  conversationId: current.conversationId,
8725
+ ...current.destination ? { destination: current.destination } : {},
8712
8726
  sessionId: current.sessionId,
8713
8727
  userMessage: current.userMessage,
8714
8728
  createdAtMs: current.createdAtMs,
@@ -8830,14 +8844,14 @@ function canReusePendingAuthLink(args) {
8830
8844
  if (!pendingAuth) {
8831
8845
  return false;
8832
8846
  }
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());
8847
+ 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
8848
  }
8835
8849
  function getConversationPendingAuth(args) {
8836
8850
  const pendingAuth = args.conversation.processing.pendingAuth;
8837
8851
  if (!pendingAuth) {
8838
8852
  return void 0;
8839
8853
  }
8840
- if (pendingAuth.kind !== args.kind || pendingAuth.provider !== args.provider || pendingAuth.requesterId !== args.requesterId) {
8854
+ if (pendingAuth.kind !== args.kind || pendingAuth.provider !== args.provider || pendingAuth.requesterId !== args.requesterId || pendingAuth.scope !== args.scope) {
8841
8855
  return void 0;
8842
8856
  }
8843
8857
  return pendingAuth;
@@ -8873,6 +8887,148 @@ function isPendingAuthLatestRequest(conversation, pendingAuth) {
8873
8887
  return false;
8874
8888
  }
8875
8889
 
8890
+ // src/chat/plugins/credential-hooks.ts
8891
+ import {
8892
+ agentPluginAuthorizationSchema as agentPluginAuthorizationSchema2,
8893
+ agentPluginCredentialResultSchema,
8894
+ agentPluginGrantSchema as agentPluginGrantSchema2,
8895
+ agentPluginProviderAccountSchema as agentPluginProviderAccountSchema2
8896
+ } from "@sentry/junior-plugin-api";
8897
+ function parseSchema(schema, value, message) {
8898
+ const result = schema.safeParse(value);
8899
+ if (!result.success) {
8900
+ throw new Error(message);
8901
+ }
8902
+ return result.data;
8903
+ }
8904
+ function parseAuthorization(value, pluginName) {
8905
+ if (value === void 0) {
8906
+ return void 0;
8907
+ }
8908
+ const authorization = parseSchema(
8909
+ agentPluginAuthorizationSchema2,
8910
+ value,
8911
+ `Plugin "${pluginName}" grant authorization is invalid`
8912
+ );
8913
+ if (authorization.provider !== pluginName) {
8914
+ throw new Error(
8915
+ `Plugin "${pluginName}" grant authorization provider must match the issuing plugin`
8916
+ );
8917
+ }
8918
+ return authorization;
8919
+ }
8920
+ function parseGrant(value, pluginName) {
8921
+ return parseSchema(
8922
+ agentPluginGrantSchema2,
8923
+ value,
8924
+ `Plugin "${pluginName}" grantForEgress returned an invalid grant`
8925
+ );
8926
+ }
8927
+ function agentPluginFor(provider) {
8928
+ return getAgentPlugins().find((candidate) => candidate.name === provider);
8929
+ }
8930
+ function parseCredentialResult(value, pluginName) {
8931
+ const result = parseSchema(
8932
+ agentPluginCredentialResultSchema,
8933
+ value,
8934
+ `Plugin "${pluginName}" issueCredential result is invalid`
8935
+ );
8936
+ if (result.type === "lease") {
8937
+ parseAuthorization(result.lease.authorization, pluginName);
8938
+ return result;
8939
+ }
8940
+ if (result.type === "unavailable") {
8941
+ return result;
8942
+ }
8943
+ parseAuthorization(result.authorization, pluginName);
8944
+ return result;
8945
+ }
8946
+ async function selectPluginGrant(input) {
8947
+ const plugin = agentPluginFor(input.provider);
8948
+ const hook = plugin?.hooks?.grantForEgress;
8949
+ if (!plugin || !hook) {
8950
+ return void 0;
8951
+ }
8952
+ const result = await hook({
8953
+ plugin: { name: plugin.name },
8954
+ log: createAgentPluginLogger(plugin.name),
8955
+ request: {
8956
+ method: input.method,
8957
+ url: input.upstreamUrl.toString()
8958
+ }
8959
+ });
8960
+ return result === void 0 ? void 0 : parseGrant(result, plugin.name);
8961
+ }
8962
+ function hasEgressCredentialHooks(provider) {
8963
+ const hooks = agentPluginFor(provider)?.hooks;
8964
+ return Boolean(hooks?.grantForEgress || hooks?.issueCredential);
8965
+ }
8966
+ async function resolvePluginOAuthAccount(input) {
8967
+ const plugin = agentPluginFor(input.provider);
8968
+ const hook = plugin?.hooks?.resolveOAuthAccount;
8969
+ if (!plugin || !hook) {
8970
+ return void 0;
8971
+ }
8972
+ const account = await hook({
8973
+ plugin: { name: plugin.name },
8974
+ log: createAgentPluginLogger(plugin.name),
8975
+ tokens: input.tokens
8976
+ });
8977
+ return account === void 0 ? void 0 : parseSchema(
8978
+ agentPluginProviderAccountSchema2,
8979
+ account,
8980
+ `Plugin "${plugin.name}" resolveOAuthAccount returned an invalid account`
8981
+ );
8982
+ }
8983
+ async function issuePluginCredential(input) {
8984
+ const plugin = agentPluginFor(input.provider);
8985
+ const hook = plugin?.hooks?.issueCredential;
8986
+ if (!plugin || !hook) {
8987
+ throw new Error(`Plugin "${input.provider}" has no issueCredential hook`);
8988
+ }
8989
+ const currentUserId = input.actor.type === "user" ? input.actor.userId : void 0;
8990
+ const credentialSubjectUserId = input.credentialSubject?.userId;
8991
+ const result = await hook({
8992
+ plugin: { name: plugin.name },
8993
+ log: createAgentPluginLogger(plugin.name),
8994
+ actor: input.actor,
8995
+ grant: input.grant,
8996
+ ...input.credentialSubject ? { credentialSubject: input.credentialSubject } : {},
8997
+ tokens: {
8998
+ ...currentUserId ? {
8999
+ currentUser: {
9000
+ userId: currentUserId,
9001
+ get: async () => await input.userTokenStore.get(currentUserId, plugin.name),
9002
+ set: async (tokens) => {
9003
+ await input.userTokenStore.set(
9004
+ currentUserId,
9005
+ plugin.name,
9006
+ tokens
9007
+ );
9008
+ }
9009
+ }
9010
+ } : {},
9011
+ ...credentialSubjectUserId ? {
9012
+ credentialSubject: {
9013
+ userId: credentialSubjectUserId,
9014
+ get: async () => await input.userTokenStore.get(
9015
+ credentialSubjectUserId,
9016
+ plugin.name
9017
+ ),
9018
+ set: async (tokens) => {
9019
+ await input.userTokenStore.set(
9020
+ credentialSubjectUserId,
9021
+ plugin.name,
9022
+ tokens
9023
+ );
9024
+ }
9025
+ }
9026
+ } : {}
9027
+ }
9028
+ });
9029
+ return parseCredentialResult(result, plugin.name);
9030
+ }
9031
+
8876
9032
  // src/chat/services/plugin-auth-orchestration.ts
8877
9033
  var PluginAuthorizationPauseError = class extends AuthorizationPauseError {
8878
9034
  constructor(provider, disposition) {
@@ -8901,7 +9057,6 @@ ${typeof result.stderr === "string" ? result.stderr : ""}`.toLowerCase();
8901
9057
  return false;
8902
9058
  }
8903
9059
  return [
8904
- /\bjunior-auth-required\b/,
8905
9060
  /\b401\b/,
8906
9061
  /\bunauthorized\b/,
8907
9062
  /\bbad credentials\b/,
@@ -8923,18 +9078,20 @@ function commandText(details) {
8923
9078
  return `${typeof result.stdout === "string" ? result.stdout : ""}
8924
9079
  ${typeof result.stderr === "string" ? result.stderr : ""}`;
8925
9080
  }
8926
- function isGitHubSmartHttpAuthFailure(provider, command, details) {
8927
- if (provider !== "github" || !/^\s*(?:gh|git)\b/i.test(command)) {
8928
- return false;
9081
+ function pluginAuthRequiredSignal(details) {
9082
+ if (!details || typeof details !== "object") {
9083
+ return void 0;
8929
9084
  }
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];
9085
+ const signal = details.auth_required;
9086
+ const parsedSignal = parseSandboxEgressAuthRequiredSignal(signal);
9087
+ if (!parsedSignal) {
9088
+ return void 0;
9089
+ }
9090
+ return {
9091
+ provider: parsedSignal.provider,
9092
+ grant: parsedSignal.grant,
9093
+ ...parsedSignal.authorization ? { authorization: parsedSignal.authorization } : {}
9094
+ };
8938
9095
  }
8939
9096
  function registeredProviderNames() {
8940
9097
  const providers = /* @__PURE__ */ new Set();
@@ -8954,15 +9111,14 @@ function commandTargetsProvider(provider, command, details) {
8954
9111
  if (!normalizedCommand) {
8955
9112
  return false;
8956
9113
  }
8957
- if (provider === "github" && /^(gh|git)\b/.test(normalizedCommand)) {
8958
- return true;
8959
- }
8960
9114
  const plugin = getPluginDefinition(provider);
8961
9115
  const candidates = /* @__PURE__ */ new Set([provider.toLowerCase()]);
8962
9116
  const manifest = plugin?.manifest;
8963
9117
  const credentials = manifest?.credentials;
8964
9118
  if (credentials) {
8965
- candidates.add(credentials.authTokenEnv.toLowerCase());
9119
+ if (credentials.authTokenEnv) {
9120
+ candidates.add(credentials.authTokenEnv.toLowerCase());
9121
+ }
8966
9122
  for (const domain of credentials.domains) {
8967
9123
  candidates.add(domain.toLowerCase());
8968
9124
  }
@@ -8982,14 +9138,11 @@ function authorizationId(args) {
8982
9138
  return `${args.sessionId}:${args.kind}:${args.provider}`;
8983
9139
  }
8984
9140
  function buildCredentialFailureError(provider, command) {
8985
- const providerLabel = provider === "github" ? "GitHub" : formatProviderLabel(provider);
8986
- const plugin = getPluginDefinition(provider);
8987
- const credentialType = plugin?.manifest.credentials?.type;
9141
+ const providerLabel = formatProviderLabel(provider);
8988
9142
  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
9143
  return new PluginCredentialFailureError(
8991
9144
  provider,
8992
- `${providerLabel} credentials were rejected while running \`${commandSummary}\`. ${remediation}`
9145
+ `${providerLabel} credentials were rejected while running \`${commandSummary}\`. Verify the ${providerLabel} provider credentials before retrying.`
8993
9146
  );
8994
9147
  }
8995
9148
  function createPluginAuthOrchestration(deps, abortAgent) {
@@ -9009,16 +9162,19 @@ function createPluginAuthOrchestration(deps, abortAgent) {
9009
9162
  pendingAuth: deps.currentPendingAuth,
9010
9163
  kind: "plugin",
9011
9164
  provider,
9012
- requesterId: deps.requesterId
9165
+ requesterId: deps.requesterId,
9166
+ ...options?.scope ? { scope: options.scope } : {}
9013
9167
  });
9014
9168
  if (!reusingPendingLink) {
9015
9169
  const oauthResult = await startOAuthFlow(provider, {
9016
9170
  requesterId: deps.requesterId,
9017
9171
  channelId: deps.channelId,
9172
+ destination: deps.destination,
9018
9173
  threadTs: deps.threadTs,
9019
9174
  userMessage: deps.userMessage,
9020
9175
  channelConfiguration: deps.channelConfiguration,
9021
9176
  activeSkillName: activeSkill?.name ?? void 0,
9177
+ ...options?.scope ? { scope: options.scope } : {},
9022
9178
  resumeConversationId: deps.conversationId,
9023
9179
  resumeSessionId: deps.sessionId
9024
9180
  });
@@ -9039,6 +9195,7 @@ function createPluginAuthOrchestration(deps, abortAgent) {
9039
9195
  kind: "plugin",
9040
9196
  provider,
9041
9197
  requesterId: deps.requesterId,
9198
+ ...options?.scope ? { scope: options.scope } : {},
9042
9199
  sessionId: deps.sessionId,
9043
9200
  linkSentAtMs: reusingPendingLink ? deps.currentPendingAuth.linkSentAtMs : Date.now()
9044
9201
  });
@@ -9068,8 +9225,9 @@ function createPluginAuthOrchestration(deps, abortAgent) {
9068
9225
  return {
9069
9226
  handleCommandFailure: async (input) => {
9070
9227
  const providers = registeredProviderNames();
9071
- const explicitProvider = explicitAuthRequiredProvider(input.details);
9072
- const provider = explicitProvider && providers.includes(explicitProvider) ? explicitProvider : providers.find(
9228
+ const parsedAuthSignal = pluginAuthRequiredSignal(input.details);
9229
+ const authSignal = parsedAuthSignal && providers.includes(parsedAuthSignal.provider) ? parsedAuthSignal : void 0;
9230
+ const provider = authSignal ? authSignal.provider : providers.find(
9073
9231
  (availableProvider) => commandTargetsProvider(
9074
9232
  availableProvider,
9075
9233
  input.command,
@@ -9079,20 +9237,30 @@ function createPluginAuthOrchestration(deps, abortAgent) {
9079
9237
  if (!provider) {
9080
9238
  return;
9081
9239
  }
9082
- const authFailure = isCommandAuthFailure(input.details) || isGitHubSmartHttpAuthFailure(provider, input.command, input.details);
9240
+ const authFailure = Boolean(authSignal) || isCommandAuthFailure(input.details);
9083
9241
  if (!authFailure) {
9084
9242
  return;
9085
9243
  }
9244
+ const providerOAuth = getPluginOAuthConfig(provider);
9245
+ const authorization = authSignal?.authorization ?? (!authSignal && !hasEgressCredentialHooks(provider) && providerOAuth ? {
9246
+ type: "oauth",
9247
+ provider,
9248
+ ...providerOAuth.scope ? { scope: providerOAuth.scope } : {}
9249
+ } : void 0);
9086
9250
  if (!deps.requesterId || !deps.userTokenStore) {
9087
9251
  if (deps.authorizationFlowMode === "disabled") {
9088
9252
  throw new AuthorizationFlowDisabledError("plugin", provider);
9089
9253
  }
9090
9254
  throw buildCredentialFailureError(provider, input.command);
9091
9255
  }
9092
- if (!getPluginOAuthConfig(provider)) {
9256
+ if (authorization?.type !== "oauth") {
9257
+ throw buildCredentialFailureError(provider, input.command);
9258
+ }
9259
+ if (!getPluginOAuthConfig(authorization.provider)) {
9093
9260
  throw buildCredentialFailureError(provider, input.command);
9094
9261
  }
9095
- await startAuthorizationPause(provider, input.activeSkill, {
9262
+ await startAuthorizationPause(authorization.provider, input.activeSkill, {
9263
+ ...authorization.scope ? { scope: authorization.scope } : {},
9096
9264
  unlinkExistingProvider: true
9097
9265
  });
9098
9266
  },
@@ -9579,6 +9747,7 @@ function coercePendingAuthState(value) {
9579
9747
  const kind = value.kind;
9580
9748
  const provider = toOptionalString(value.provider);
9581
9749
  const requesterId = toOptionalString(value.requesterId);
9750
+ const scope = toOptionalString(value.scope);
9582
9751
  const sessionId = toOptionalString(value.sessionId);
9583
9752
  const linkSentAtMs = toOptionalNumber(value.linkSentAtMs);
9584
9753
  if (kind !== "mcp" && kind !== "plugin" || !provider || !requesterId || !sessionId || typeof linkSentAtMs !== "number") {
@@ -9588,6 +9757,7 @@ function coercePendingAuthState(value) {
9588
9757
  kind,
9589
9758
  provider,
9590
9759
  requesterId,
9760
+ ...scope ? { scope } : {},
9591
9761
  sessionId,
9592
9762
  linkSentAtMs
9593
9763
  };
@@ -10170,29 +10340,8 @@ function buildTurnResult(input) {
10170
10340
  };
10171
10341
  }
10172
10342
 
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
10343
  // src/chat/services/turn-thinking-level.ts
10195
- import { z } from "zod";
10344
+ import { z as z2 } from "zod";
10196
10345
  var CLASSIFIER_CONFIDENCE_THRESHOLD = 0.75;
10197
10346
  var MAX_ROUTER_CONTEXT_CHARS = 8e3;
10198
10347
  var ROUTER_CONTEXT_HEAD_CHARS = 3e3;
@@ -10224,13 +10373,13 @@ function coerceClassifierConfidence(value) {
10224
10373
  }
10225
10374
  return CONFIDENCE_LABELS[trimmed] ?? value;
10226
10375
  }
10227
- var turnExecutionProfileSchema = z.object({
10228
- thinking_level: z.enum(TURN_THINKING_LEVELS),
10229
- confidence: z.preprocess(
10376
+ var turnExecutionProfileSchema = z2.object({
10377
+ thinking_level: z2.enum(TURN_THINKING_LEVELS),
10378
+ confidence: z2.preprocess(
10230
10379
  coerceClassifierConfidence,
10231
- z.number().min(0).max(1)
10380
+ z2.number().min(0).max(1)
10232
10381
  ),
10233
- reason: z.string().min(1)
10382
+ reason: z2.string().min(1)
10234
10383
  });
10235
10384
  var DEFAULT_THINKING_LEVEL = "medium";
10236
10385
  var THINKING_LEVEL_RANK = {
@@ -10546,6 +10695,7 @@ async function persistRunningSessionRecord(args) {
10546
10695
  conversationId: args.conversationId,
10547
10696
  cumulativeDurationMs: latestSessionRecord?.cumulativeDurationMs,
10548
10697
  cumulativeUsage: latestSessionRecord?.cumulativeUsage,
10698
+ ...args.destination ?? latestSessionRecord?.destination ? { destination: args.destination ?? latestSessionRecord?.destination } : {},
10549
10699
  sessionId: args.sessionId,
10550
10700
  sliceId: args.sliceId,
10551
10701
  state: "running",
@@ -10586,6 +10736,7 @@ async function persistCompletedSessionRecord(args) {
10586
10736
  latestSessionRecord?.cumulativeUsage,
10587
10737
  args.currentUsage
10588
10738
  ),
10739
+ ...args.destination ?? latestSessionRecord?.destination ? { destination: args.destination ?? latestSessionRecord?.destination } : {},
10589
10740
  sessionId: args.sessionId,
10590
10741
  sliceId: args.sliceId,
10591
10742
  state: "completed",
@@ -10632,6 +10783,7 @@ async function persistAuthPauseSessionRecord(args) {
10632
10783
  latestSessionRecord?.cumulativeUsage,
10633
10784
  args.currentUsage
10634
10785
  ),
10786
+ ...args.destination ?? latestSessionRecord?.destination ? { destination: args.destination ?? latestSessionRecord?.destination } : {},
10635
10787
  sessionId: args.sessionId,
10636
10788
  sliceId: nextSliceId,
10637
10789
  state: "awaiting_resume",
@@ -10688,6 +10840,9 @@ async function persistTimeoutSessionRecord(args) {
10688
10840
  conversationId: args.conversationId,
10689
10841
  cumulativeDurationMs,
10690
10842
  cumulativeUsage,
10843
+ ...args.destination ?? latestSessionRecord?.destination ? {
10844
+ destination: args.destination ?? latestSessionRecord?.destination
10845
+ } : {},
10691
10846
  sessionId: args.sessionId,
10692
10847
  sliceId: args.currentSliceId,
10693
10848
  state: "failed",
@@ -10706,6 +10861,7 @@ async function persistTimeoutSessionRecord(args) {
10706
10861
  conversationId: args.conversationId,
10707
10862
  cumulativeDurationMs,
10708
10863
  cumulativeUsage,
10864
+ ...args.destination ?? latestSessionRecord?.destination ? { destination: args.destination ?? latestSessionRecord?.destination } : {},
10709
10865
  sessionId: args.sessionId,
10710
10866
  sliceId: nextSliceId,
10711
10867
  state: "awaiting_resume",
@@ -10756,6 +10912,7 @@ async function persistYieldSessionRecord(args) {
10756
10912
  latestSessionRecord?.cumulativeUsage,
10757
10913
  args.currentUsage
10758
10914
  ),
10915
+ ...args.destination ?? latestSessionRecord?.destination ? { destination: args.destination ?? latestSessionRecord?.destination } : {},
10759
10916
  sessionId: args.sessionId,
10760
10917
  sliceId: args.currentSliceId,
10761
10918
  state: "awaiting_resume",
@@ -10971,6 +11128,7 @@ async function createMcpOAuthClientProvider(input) {
10971
11128
  provider: input.provider,
10972
11129
  userId: input.userId,
10973
11130
  conversationId: input.conversationId,
11131
+ ...input.destination ? { destination: input.destination } : {},
10974
11132
  sessionId: input.sessionId,
10975
11133
  userMessage: input.userMessage,
10976
11134
  ...input.channelId ? { channelId: input.channelId } : {},
@@ -10990,6 +11148,7 @@ async function createMcpOAuthClientProvider(input) {
10990
11148
  provider: input.provider,
10991
11149
  userId: input.userId,
10992
11150
  conversationId: input.conversationId,
11151
+ ...input.destination ? { destination: input.destination } : {},
10993
11152
  sessionId: input.sessionId,
10994
11153
  userMessage: input.userMessage,
10995
11154
  ...input.channelId ? { channelId: input.channelId } : {},
@@ -11065,6 +11224,7 @@ function createMcpAuthOrchestration(deps, abortAgent) {
11065
11224
  const provider = await createMcpOAuthClientProvider({
11066
11225
  provider: plugin.manifest.name,
11067
11226
  conversationId: deps.conversationId,
11227
+ destination: deps.destination,
11068
11228
  sessionId: deps.sessionId,
11069
11229
  userId: deps.requesterId,
11070
11230
  userMessage: deps.userMessage,
@@ -11161,7 +11321,6 @@ function createMcpAuthOrchestration(deps, abortAgent) {
11161
11321
  }
11162
11322
 
11163
11323
  // src/chat/respond.ts
11164
- var PROVIDER_RETRY_DELAYS_MS = [1e3, 2e3];
11165
11324
  var AGENT_ABORT_SETTLE_GRACE_MS = 5e3;
11166
11325
  function sleep2(ms) {
11167
11326
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -11652,6 +11811,7 @@ async function generateAssistantReply(messageText2, context = {}) {
11652
11811
  sessionId,
11653
11812
  requesterId: authRequesterId,
11654
11813
  channelId: context.correlation?.channelId,
11814
+ destination: context.destination,
11655
11815
  threadTs: context.correlation?.threadTs,
11656
11816
  toolChannelId: context.toolChannelId,
11657
11817
  userMessage: userInput,
@@ -11670,6 +11830,7 @@ async function generateAssistantReply(messageText2, context = {}) {
11670
11830
  sessionId,
11671
11831
  requesterId: authRequesterId,
11672
11832
  channelId: context.correlation?.channelId,
11833
+ destination: context.destination,
11673
11834
  threadTs: context.correlation?.threadTs,
11674
11835
  userMessage: userInput,
11675
11836
  channelConfiguration: context.channelConfiguration,
@@ -11696,8 +11857,6 @@ async function generateAssistantReply(messageText2, context = {}) {
11696
11857
  assistantUserName: botConfig.userName,
11697
11858
  modelId: botConfig.modelId
11698
11859
  });
11699
- const toolChannelId = context.toolChannelId ?? context.correlation?.channelId;
11700
- const channelCapabilities = resolveChannelCapabilities(toolChannelId);
11701
11860
  const loadableSkills = availableSkills.filter(
11702
11861
  (skill) => skill.disableModelInvocation !== true || skill.name === invokedSkill?.name
11703
11862
  );
@@ -11748,8 +11907,10 @@ async function generateAssistantReply(messageText2, context = {}) {
11748
11907
  }
11749
11908
  },
11750
11909
  {
11751
- channelId: toolChannelId,
11752
- channelCapabilities,
11910
+ channelId: context.correlation?.channelId,
11911
+ conversationId: sessionConversationId,
11912
+ deliveryChannelId: context.toolChannelId,
11913
+ destination: context.destination,
11753
11914
  requester: actorRequester,
11754
11915
  teamId: context.correlation?.teamId,
11755
11916
  messageTs: context.correlation?.messageTs,
@@ -11889,6 +12050,7 @@ async function generateAssistantReply(messageText2, context = {}) {
11889
12050
  const persisted = await persistRunningSessionRecord({
11890
12051
  channelName: context.correlation?.channelName,
11891
12052
  conversationId: sessionConversationId,
12053
+ destination: context.destination,
11892
12054
  sessionId,
11893
12055
  sliceId: currentSliceId,
11894
12056
  messages,
@@ -12154,26 +12316,24 @@ async function generateAssistantReply(messageText2, context = {}) {
12154
12316
  throw getPendingAuthPause();
12155
12317
  }
12156
12318
  const lastAssistant = outputMessages.at(-1);
12157
- const retryDelayMs = PROVIDER_RETRY_DELAYS_MS[attempt];
12158
- if (retryDelayMs === void 0 || !isRetryableProviderError(lastAssistant)) {
12319
+ const providerRetry = nextProviderRetry({
12320
+ attempt,
12321
+ lastAssistant,
12322
+ messages: agent.state.messages
12323
+ });
12324
+ if (!providerRetry) {
12159
12325
  break;
12160
12326
  }
12161
12327
  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);
12328
+ agent.state.messages = providerRetry.messages;
12329
+ await persistSafeBoundary(providerRetry.messages);
12170
12330
  logWarn(
12171
12331
  "agent_turn_provider_retry",
12172
12332
  spanContext,
12173
12333
  {},
12174
12334
  "Retrying transient provider failure"
12175
12335
  );
12176
- await sleep2(retryDelayMs);
12336
+ await sleep2(providerRetry.delayMs);
12177
12337
  run = agent.continue();
12178
12338
  }
12179
12339
  },
@@ -12203,6 +12363,7 @@ async function generateAssistantReply(messageText2, context = {}) {
12203
12363
  conversationId: sessionConversationId,
12204
12364
  currentDurationMs: Date.now() - replyStartedAtMs,
12205
12365
  currentUsage: turnUsage,
12366
+ destination: context.destination,
12206
12367
  sessionId,
12207
12368
  sliceId: currentSliceId,
12208
12369
  allMessages: agent.state.messages,
@@ -12236,6 +12397,7 @@ async function generateAssistantReply(messageText2, context = {}) {
12236
12397
  const sessionRecord = await persistYieldSessionRecord({
12237
12398
  channelName: context.correlation?.channelName,
12238
12399
  conversationId: timeoutResumeConversationId,
12400
+ destination: context.destination,
12239
12401
  sessionId: timeoutResumeSessionId,
12240
12402
  currentSliceId: timeoutResumeSliceId,
12241
12403
  currentDurationMs: Date.now() - replyStartedAtMs,
@@ -12260,6 +12422,7 @@ async function generateAssistantReply(messageText2, context = {}) {
12260
12422
  const sessionRecord = await persistTimeoutSessionRecord({
12261
12423
  channelName: context.correlation?.channelName,
12262
12424
  conversationId: timeoutResumeConversationId,
12425
+ destination: context.destination,
12263
12426
  sessionId: timeoutResumeSessionId,
12264
12427
  currentSliceId: timeoutResumeSliceId,
12265
12428
  currentDurationMs: Date.now() - replyStartedAtMs,
@@ -12303,6 +12466,7 @@ async function generateAssistantReply(messageText2, context = {}) {
12303
12466
  const sessionRecord = await persistAuthPauseSessionRecord({
12304
12467
  channelName: context.correlation?.channelName,
12305
12468
  conversationId: timeoutResumeConversationId,
12469
+ destination: context.destination,
12306
12470
  sessionId: timeoutResumeSessionId,
12307
12471
  currentSliceId: timeoutResumeSliceId,
12308
12472
  currentDurationMs: Date.now() - replyStartedAtMs,
@@ -12335,6 +12499,9 @@ async function generateAssistantReply(messageText2, context = {}) {
12335
12499
  if (isRetryableTurnError(error)) {
12336
12500
  throw error;
12337
12501
  }
12502
+ if (isProviderRetryError(error)) {
12503
+ throw error;
12504
+ }
12338
12505
  if (isTurnInputCommitLostError(error)) {
12339
12506
  throw error;
12340
12507
  }
@@ -13136,11 +13303,80 @@ async function verifyDispatchCallbackRequest(request) {
13136
13303
 
13137
13304
  // src/chat/agent-dispatch/store.ts
13138
13305
  import { createHash } from "crypto";
13306
+ import {
13307
+ agentPluginCredentialSubjectSchema,
13308
+ destinationSchema
13309
+ } from "@sentry/junior-plugin-api";
13310
+ import { z as z3 } from "zod";
13139
13311
  var DISPATCH_PREFIX = "junior:agent_dispatch";
13140
13312
  var DISPATCH_LOCK_TTL_MS = 10 * 60 * 1e3;
13141
13313
  var DISPATCH_INDEX_LOCK_TTL_MS = 1e4;
13142
13314
  var DISPATCH_INDEX_MAX_LENGTH = 1e4;
13143
13315
  var DEFAULT_MAX_ATTEMPTS = 5;
13316
+ var nonEmptyExactStringSchema = z3.string().min(1).refine(
13317
+ (value) => value === value.trim() && value.toLowerCase() !== "unknown"
13318
+ );
13319
+ var dispatchStatusSchema = z3.enum([
13320
+ "pending",
13321
+ "running",
13322
+ "awaiting_resume",
13323
+ "completed",
13324
+ "failed",
13325
+ "blocked"
13326
+ ]);
13327
+ var dispatchActorSchema = z3.object({
13328
+ type: z3.literal("system"),
13329
+ id: nonEmptyExactStringSchema
13330
+ }).strict();
13331
+ var credentialSubjectBindingSchema = z3.object({
13332
+ type: z3.literal("slack-direct-conversation"),
13333
+ teamId: z3.string().min(1),
13334
+ channelId: z3.string().min(1),
13335
+ signature: z3.string().min(1)
13336
+ }).strict();
13337
+ var boundCredentialSubjectSchema = agentPluginCredentialSubjectSchema.extend({
13338
+ binding: credentialSubjectBindingSchema
13339
+ }).strict();
13340
+ var dispatchRecordSchema = z3.object({
13341
+ actor: dispatchActorSchema,
13342
+ attempt: z3.number().int().nonnegative(),
13343
+ createdAtMs: z3.number().finite(),
13344
+ credentialSubject: boundCredentialSubjectSchema.optional(),
13345
+ destination: destinationSchema,
13346
+ errorMessage: z3.string().optional(),
13347
+ id: nonEmptyExactStringSchema,
13348
+ idempotencyKey: z3.string().min(1),
13349
+ input: z3.string().min(1),
13350
+ lastCallbackAtMs: z3.number().finite().optional(),
13351
+ leaseExpiresAtMs: z3.number().finite().optional(),
13352
+ maxAttempts: z3.number().int().positive(),
13353
+ metadata: z3.record(z3.string(), z3.string()).optional(),
13354
+ plugin: nonEmptyExactStringSchema,
13355
+ resultMessageTs: z3.string().optional(),
13356
+ status: dispatchStatusSchema,
13357
+ updatedAtMs: z3.number().finite(),
13358
+ version: z3.number().int().positive()
13359
+ }).strict().superRefine((record, ctx) => {
13360
+ const subject = record.credentialSubject;
13361
+ if (!subject) {
13362
+ return;
13363
+ }
13364
+ if (!record.destination.channelId.startsWith("D")) {
13365
+ ctx.addIssue({
13366
+ code: z3.ZodIssueCode.custom,
13367
+ message: "Dispatch credentialSubject requires a private direct Slack destination",
13368
+ path: ["credentialSubject"]
13369
+ });
13370
+ return;
13371
+ }
13372
+ if (subject.binding.teamId !== record.destination.teamId || subject.binding.channelId !== record.destination.channelId) {
13373
+ ctx.addIssue({
13374
+ code: z3.ZodIssueCode.custom,
13375
+ message: "Dispatch credentialSubject binding must match destination",
13376
+ path: ["credentialSubject", "binding"]
13377
+ });
13378
+ }
13379
+ });
13144
13380
  function getDispatchStorageKey(id) {
13145
13381
  return `${DISPATCH_PREFIX}:record:${id}`;
13146
13382
  }
@@ -13166,8 +13402,12 @@ function buildDispatchId(plugin, idempotencyKey) {
13166
13402
  const digest = createHash("sha256").update(plugin).update("\0").update(idempotencyKey).digest("hex").slice(0, 32);
13167
13403
  return `dispatch_${digest}`;
13168
13404
  }
13405
+ function parseDispatchRecord(value) {
13406
+ const parsed = dispatchRecordSchema.safeParse(value);
13407
+ return parsed.success ? parsed.data : void 0;
13408
+ }
13169
13409
  function getDispatchDestinationLockId(destination) {
13170
- return `slack:${destination.teamId}:${destination.channelId}`;
13410
+ return destinationKey(destination);
13171
13411
  }
13172
13412
  function getDispatchConversationId(dispatch) {
13173
13413
  return `agent-dispatch:${dispatch.id}`;
@@ -13234,22 +13474,28 @@ async function syncIncompleteDispatchIndex(state, record) {
13234
13474
  });
13235
13475
  }
13236
13476
  async function putRecord(state, record) {
13477
+ const next = parseDispatchRecord(record);
13478
+ if (!next) {
13479
+ throw new Error("Dispatch record is invalid.");
13480
+ }
13237
13481
  await state.set(
13238
- getDispatchStorageKey(record.id),
13239
- record,
13482
+ getDispatchStorageKey(next.id),
13483
+ next,
13240
13484
  JUNIOR_THREAD_STATE_TTL_MS
13241
13485
  );
13242
- await syncIncompleteDispatchIndex(state, record);
13486
+ await syncIncompleteDispatchIndex(state, next);
13243
13487
  }
13244
13488
  async function getDispatchRecord(id) {
13245
13489
  const state = getStateAdapter();
13246
13490
  await state.connect();
13247
- return await state.get(getDispatchStorageKey(id)) ?? void 0;
13491
+ return parseDispatchRecord(await state.get(getDispatchStorageKey(id)));
13248
13492
  }
13249
13493
  async function createOrGetDispatch(args) {
13250
13494
  const id = buildDispatchId(args.plugin, args.options.idempotencyKey);
13251
13495
  return await withDispatchLock(id, async (state) => {
13252
- const existing = await state.get(getDispatchStorageKey(id)) ?? void 0;
13496
+ const existing = parseDispatchRecord(
13497
+ await state.get(getDispatchStorageKey(id))
13498
+ );
13253
13499
  if (existing) {
13254
13500
  return { record: existing, status: "already_exists" };
13255
13501
  }
@@ -13344,9 +13590,12 @@ async function persistRuntimePatch(args) {
13344
13590
  }
13345
13591
  async function markDispatch(args) {
13346
13592
  return await withDispatchLock(args.dispatch.id, async (state) => {
13347
- const current = await state.get(
13348
- getDispatchStorageKey(args.dispatch.id)
13349
- ) ?? args.dispatch;
13593
+ const current = parseDispatchRecord(
13594
+ await state.get(getDispatchStorageKey(args.dispatch.id))
13595
+ );
13596
+ if (!current) {
13597
+ throw new Error("Dispatch record is missing or invalid.");
13598
+ }
13350
13599
  return await updateDispatchRecord(state, {
13351
13600
  ...current,
13352
13601
  status: args.status,
@@ -13372,7 +13621,9 @@ async function runAgentDispatchSlice(callback, deps = {}) {
13372
13621
  const scheduleCallback = deps.scheduleCallback ?? scheduleDispatchCallback;
13373
13622
  const nowMs = Date.now();
13374
13623
  const claimedDispatch = await withDispatchLock(callback.id, async (state) => {
13375
- const current = await state.get(getDispatchStorageKey(callback.id)) ?? void 0;
13624
+ const current = parseDispatchRecord(
13625
+ await state.get(getDispatchStorageKey(callback.id))
13626
+ );
13376
13627
  if (!current || !canClaimDispatch(current, nowMs) || current.version !== callback.expectedVersion) {
13377
13628
  return void 0;
13378
13629
  }
@@ -13407,10 +13658,10 @@ async function runAgentDispatchSlice(callback, deps = {}) {
13407
13658
  const startedDispatch = await withDispatchLock(
13408
13659
  dispatch.id,
13409
13660
  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) {
13661
+ const current = parseDispatchRecord(
13662
+ await state.get(getDispatchStorageKey(dispatch.id))
13663
+ );
13664
+ if (!current || current.status !== "running" || current.version !== dispatch.version || current.attempt >= current.maxAttempts) {
13414
13665
  return void 0;
13415
13666
  }
13416
13667
  return await updateDispatchRecord(state, {
@@ -13462,6 +13713,7 @@ async function runAgentDispatchSlice(callback, deps = {}) {
13462
13713
  conversationContext,
13463
13714
  artifactState: artifacts,
13464
13715
  piMessages: conversation.piMessages,
13716
+ destination: dispatch.destination,
13465
13717
  correlation: {
13466
13718
  conversationId,
13467
13719
  threadId: conversationId,
@@ -13729,14 +13981,16 @@ function normalizeMessage(value) {
13729
13981
  const conversationId = toOptionalString(value.conversationId);
13730
13982
  const inboundMessageId = toOptionalString(value.inboundMessageId);
13731
13983
  const source = normalizeSource(value.source);
13984
+ const destination = parseDestination(value.destination);
13732
13985
  const createdAtMs = toOptionalNumber(value.createdAtMs);
13733
13986
  const receivedAtMs = toOptionalNumber(value.receivedAtMs);
13734
13987
  const input = normalizeInput(value.input);
13735
- if (!conversationId || !inboundMessageId || !source || typeof createdAtMs !== "number" || typeof receivedAtMs !== "number" || !input) {
13988
+ if (!conversationId || !destination || !inboundMessageId || !source || typeof createdAtMs !== "number" || typeof receivedAtMs !== "number" || !input) {
13736
13989
  return void 0;
13737
13990
  }
13738
13991
  return {
13739
13992
  conversationId,
13993
+ destination,
13740
13994
  inboundMessageId,
13741
13995
  source,
13742
13996
  createdAtMs,
@@ -13768,14 +14022,16 @@ function normalizeWorkState(conversationId, value) {
13768
14022
  return void 0;
13769
14023
  }
13770
14024
  const storedConversationId = toOptionalString(value.conversationId);
14025
+ const destination = parseDestination(value.destination);
13771
14026
  const updatedAtMs = toOptionalNumber(value.updatedAtMs);
13772
- if (storedConversationId !== conversationId || typeof updatedAtMs !== "number") {
14027
+ if (storedConversationId !== conversationId || !destination || typeof updatedAtMs !== "number") {
13773
14028
  return void 0;
13774
14029
  }
13775
14030
  const messages = Array.isArray(value.messages) ? value.messages.map(normalizeMessage).filter((message) => Boolean(message)).filter((message) => message.conversationId === conversationId).sort(compareMessages) : [];
13776
14031
  return {
13777
14032
  schemaVersion: CONVERSATION_WORK_SCHEMA_VERSION,
13778
14033
  conversationId,
14034
+ destination,
13779
14035
  messages,
13780
14036
  needsRun: value.needsRun === true,
13781
14037
  updatedAtMs,
@@ -13787,6 +14043,7 @@ function emptyWorkState(args) {
13787
14043
  return {
13788
14044
  schemaVersion: CONVERSATION_WORK_SCHEMA_VERSION,
13789
14045
  conversationId: args.conversationId,
14046
+ destination: args.destination,
13790
14047
  messages: [],
13791
14048
  needsRun: false,
13792
14049
  updatedAtMs: args.nowMs
@@ -13900,10 +14157,15 @@ async function withConversationMutation(args, callback) {
13900
14157
  }
13901
14158
  }
13902
14159
  async function readWorkState(state, conversationId) {
13903
- return normalizeWorkState(
13904
- conversationId,
13905
- await state.get(stateKey(conversationId))
13906
- );
14160
+ const raw = await state.get(stateKey(conversationId));
14161
+ if (raw == null) {
14162
+ return void 0;
14163
+ }
14164
+ const work = normalizeWorkState(conversationId, raw);
14165
+ if (!work) {
14166
+ throw new Error(`Conversation work state is invalid for ${conversationId}`);
14167
+ }
14168
+ return work;
13907
14169
  }
13908
14170
  async function writeWorkState(state, work) {
13909
14171
  await state.set(
@@ -13920,6 +14182,14 @@ async function writeWorkState(state, work) {
13920
14182
  function hasRunnableWork(state) {
13921
14183
  return state.needsRun || pendingMessages(state).length > 0;
13922
14184
  }
14185
+ function assertSameConversationDestination(args) {
14186
+ if (sameDestination(args.current, args.next)) {
14187
+ return;
14188
+ }
14189
+ throw new Error(
14190
+ `Conversation work destination changed for ${args.conversationId}`
14191
+ );
14192
+ }
13923
14193
  async function getConversationWorkState(args) {
13924
14194
  const state = await getConnectedState(args.state);
13925
14195
  return await readWorkState(state, args.conversationId);
@@ -13937,8 +14207,14 @@ async function appendInboundMessage(args) {
13937
14207
  async (state) => {
13938
14208
  const current = await readWorkState(state, args.message.conversationId) ?? emptyWorkState({
13939
14209
  conversationId: args.message.conversationId,
14210
+ destination: args.message.destination,
13940
14211
  nowMs
13941
14212
  });
14213
+ assertSameConversationDestination({
14214
+ conversationId: args.message.conversationId,
14215
+ current: current.destination,
14216
+ next: args.message.destination
14217
+ });
13942
14218
  const existing = current.messages.find(
13943
14219
  (message) => message.inboundMessageId === args.message.inboundMessageId
13944
14220
  );
@@ -13981,7 +14257,10 @@ async function appendAndEnqueueInboundMessage(args) {
13981
14257
  idempotencyKey = duplicateInboundNudgeIdempotencyKey(args.message, nowMs);
13982
14258
  }
13983
14259
  const queueResult = await args.queue.send(
13984
- { conversationId: args.message.conversationId },
14260
+ {
14261
+ conversationId: args.message.conversationId,
14262
+ destination: args.message.destination
14263
+ },
13985
14264
  { idempotencyKey }
13986
14265
  );
13987
14266
  await markConversationWorkEnqueued({
@@ -13998,8 +14277,16 @@ async function requestConversationWork(args) {
13998
14277
  const nowMs = args.nowMs ?? now();
13999
14278
  return await withConversationMutation(args, async (state) => {
14000
14279
  const existing = await readWorkState(state, args.conversationId);
14280
+ if (existing) {
14281
+ assertSameConversationDestination({
14282
+ conversationId: args.conversationId,
14283
+ current: existing.destination,
14284
+ next: args.destination
14285
+ });
14286
+ }
14001
14287
  const current = existing ?? emptyWorkState({
14002
14288
  conversationId: args.conversationId,
14289
+ destination: args.destination,
14003
14290
  nowMs
14004
14291
  });
14005
14292
  await writeWorkState(state, {
@@ -14155,6 +14442,11 @@ async function requestConversationContinuation(args) {
14155
14442
  if (!current || current.lease?.leaseToken !== args.leaseToken) {
14156
14443
  return false;
14157
14444
  }
14445
+ assertSameConversationDestination({
14446
+ conversationId: args.conversationId,
14447
+ current: current.destination,
14448
+ next: args.destination
14449
+ });
14158
14450
  await writeWorkState(state, {
14159
14451
  ...current,
14160
14452
  needsRun: true,
@@ -14232,8 +14524,12 @@ async function getAwaitingTurnContinuationRequest(args) {
14232
14524
  if (!sessionRecord || sessionRecord.state !== "awaiting_resume" || sessionRecord.resumeReason !== "timeout" && sessionRecord.resumeReason !== "yield" || sessionRecord.resumeReason === "timeout" && sessionRecord.sliceId < 2) {
14233
14525
  return void 0;
14234
14526
  }
14527
+ if (!sessionRecord.destination) {
14528
+ return void 0;
14529
+ }
14235
14530
  return {
14236
14531
  conversationId: args.conversationId,
14532
+ destination: sessionRecord.destination,
14237
14533
  sessionId: args.sessionId,
14238
14534
  expectedVersion: sessionRecord.version
14239
14535
  };
@@ -14261,12 +14557,17 @@ function parseTurnTimeoutResumeRequest(value) {
14261
14557
  return void 0;
14262
14558
  }
14263
14559
  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") {
14560
+ const destination = parseDestination(record.destination);
14561
+ let expectedVersion = record.expectedVersion;
14562
+ if (typeof expectedVersion !== "number") {
14563
+ expectedVersion = record.expectedCheckpointVersion;
14564
+ }
14565
+ if (typeof record.conversationId !== "string" || typeof record.sessionId !== "string" || typeof expectedVersion !== "number" || !destination) {
14266
14566
  return void 0;
14267
14567
  }
14268
14568
  return {
14269
14569
  conversationId: record.conversationId,
14570
+ destination,
14270
14571
  sessionId: record.sessionId,
14271
14572
  expectedVersion
14272
14573
  };
@@ -14275,12 +14576,16 @@ async function scheduleTurnTimeoutResume(request, options = {}) {
14275
14576
  const nowMs = options.nowMs ?? Date.now();
14276
14577
  await requestConversationWork({
14277
14578
  conversationId: request.conversationId,
14579
+ destination: request.destination,
14278
14580
  nowMs,
14279
14581
  state: options.state
14280
14582
  });
14281
14583
  const queue = options.queue ?? getVercelConversationWorkQueue();
14282
14584
  await queue.send(
14283
- { conversationId: request.conversationId },
14585
+ {
14586
+ conversationId: request.conversationId,
14587
+ destination: request.destination
14588
+ },
14284
14589
  {
14285
14590
  idempotencyKey: [
14286
14591
  "timeout",
@@ -14326,7 +14631,10 @@ function heartbeatIdempotencyKey(reason, conversationId, nowMs) {
14326
14631
  }
14327
14632
  async function sendRecoveryNudge(args) {
14328
14633
  await args.queue.send(
14329
- { conversationId: args.conversationId },
14634
+ {
14635
+ conversationId: args.conversationId,
14636
+ destination: args.destination
14637
+ },
14330
14638
  { idempotencyKey: args.idempotencyKey }
14331
14639
  );
14332
14640
  await markConversationWorkEnqueued({
@@ -14364,6 +14672,7 @@ async function recoverConversationWork(args) {
14364
14672
  }
14365
14673
  await sendRecoveryNudge({
14366
14674
  conversationId,
14675
+ destination: work.destination,
14367
14676
  idempotencyKey: heartbeatIdempotencyKey(
14368
14677
  "lease",
14369
14678
  conversationId,
@@ -14390,6 +14699,7 @@ async function recoverConversationWork(args) {
14390
14699
  }
14391
14700
  await sendRecoveryNudge({
14392
14701
  conversationId,
14702
+ destination: work.destination,
14393
14703
  idempotencyKey: heartbeatIdempotencyKey(
14394
14704
  "pending",
14395
14705
  conversationId,
@@ -14419,78 +14729,92 @@ async function recoverConversationWork(args) {
14419
14729
  return result;
14420
14730
  }
14421
14731
 
14422
- // src/chat/slack/ids.ts
14423
- function isSlackTeamId(value) {
14424
- return /^T[A-Z0-9]+$/.test(value);
14732
+ // src/chat/agent-dispatch/validation.ts
14733
+ import {
14734
+ dispatchOptionsSchema
14735
+ } from "@sentry/junior-plugin-api";
14736
+ function hasIssueAtPath(issues, path9) {
14737
+ return issues.some(
14738
+ (issue) => issue.path.length === path9.length && issue.path.every((value, index) => value === path9[index])
14739
+ );
14425
14740
  }
14426
- function isSlackConversationId(value) {
14427
- return /^(C|G|D)[A-Z0-9]+$/.test(value);
14741
+ function hasIssueUnderPath(issues, path9) {
14742
+ return issues.some(
14743
+ (issue) => path9.every((value, index) => issue.path[index] === value)
14744
+ );
14428
14745
  }
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");
14746
+ function dispatchOptionsErrorMessage(issues) {
14747
+ if (hasIssueAtPath(issues, [])) {
14748
+ const unknownKeys = issues.some(
14749
+ (issue) => issue.code === "unrecognized_keys"
14750
+ );
14751
+ return unknownKeys ? "Dispatch options must not include unknown fields" : "Dispatch options are required";
14439
14752
  }
14440
- if (options.idempotencyKey.length > MAX_IDEMPOTENCY_KEY_LENGTH) {
14441
- throw new Error("Dispatch idempotencyKey exceeds the maximum length");
14753
+ if (issues.some(
14754
+ (issue) => issue.code === "unrecognized_keys" && issue.path[0] === "destination"
14755
+ )) {
14756
+ return "Dispatch destination must not include unknown fields";
14757
+ }
14758
+ if (issues.some(
14759
+ (issue) => issue.code === "unrecognized_keys" && issue.path[0] === "credentialSubject"
14760
+ )) {
14761
+ return "Dispatch credentialSubject binding is runtime-owned";
14442
14762
  }
14443
- if (options.destination.platform !== "slack") {
14444
- throw new Error("Dispatch destination platform must be slack");
14763
+ if (hasIssueAtPath(issues, ["destination"])) {
14764
+ return "Dispatch destination platform must be slack";
14445
14765
  }
14446
- if (!isSlackTeamId(options.destination.teamId)) {
14447
- throw new Error("Dispatch destination teamId must be a Slack team id");
14766
+ if (hasIssueUnderPath(issues, ["destination", "teamId"])) {
14767
+ return "Dispatch destination teamId must be a Slack team id";
14448
14768
  }
14449
- if (!isSlackConversationId(options.destination.channelId)) {
14450
- throw new Error(
14451
- "Dispatch destination channelId must be a Slack channel id"
14769
+ if (hasIssueUnderPath(issues, ["destination", "channelId"])) {
14770
+ return "Dispatch destination channelId must be a Slack channel id";
14771
+ }
14772
+ if (hasIssueUnderPath(issues, ["idempotencyKey"])) {
14773
+ const tooLong = issues.some(
14774
+ (issue) => issue.path[0] === "idempotencyKey" && issue.code === "too_big"
14452
14775
  );
14776
+ return tooLong ? "Dispatch idempotencyKey exceeds the maximum length" : "Dispatch idempotencyKey is required";
14453
14777
  }
14454
- if (!options.input.trim()) {
14455
- throw new Error("Dispatch input is required");
14778
+ if (hasIssueUnderPath(issues, ["input"])) {
14779
+ const tooLong = issues.some(
14780
+ (issue) => issue.path[0] === "input" && issue.code === "too_big"
14781
+ );
14782
+ return tooLong ? "Dispatch input exceeds the maximum length" : "Dispatch input is required";
14456
14783
  }
14457
- if (options.input.length > MAX_DISPATCH_INPUT_LENGTH) {
14458
- throw new Error("Dispatch input exceeds the maximum length");
14784
+ if (hasIssueUnderPath(issues, ["credentialSubject", "userId"])) {
14785
+ return "Dispatch credentialSubject userId is required";
14459
14786
  }
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)) {
14787
+ if (hasIssueUnderPath(issues, ["credentialSubject", "allowedWhen"])) {
14788
+ return "Dispatch credentialSubject allowedWhen must be private-direct-conversation";
14789
+ }
14790
+ if (hasIssueUnderPath(issues, ["credentialSubject"])) {
14791
+ return "Dispatch credentialSubject type must be user";
14792
+ }
14793
+ const metadataIssue = issues.find(
14794
+ (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")
14795
+ );
14796
+ if (metadataIssue) {
14797
+ return metadataIssue.message;
14798
+ }
14799
+ if (hasIssueUnderPath(issues, ["metadata"])) {
14800
+ return "Dispatch metadata values must be strings";
14801
+ }
14802
+ return "Dispatch options are invalid";
14803
+ }
14804
+ function validateDispatchOptions(options) {
14805
+ const parsed = dispatchOptionsSchema.safeParse(options);
14806
+ if (!parsed.success) {
14807
+ throw new Error(dispatchOptionsErrorMessage(parsed.error.issues));
14808
+ }
14809
+ const candidate = parsed.data;
14810
+ const { credentialSubject, destination } = candidate;
14811
+ if (credentialSubject !== void 0) {
14812
+ if (!isDmChannel(destination.channelId)) {
14473
14813
  throw new Error(
14474
14814
  "Dispatch credentialSubject requires a private direct Slack destination"
14475
14815
  );
14476
14816
  }
14477
14817
  }
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
14818
  }
14495
14819
  async function verifyDispatchCredentialSubjectAccess(options) {
14496
14820
  if (!options.credentialSubject) {
@@ -14604,8 +14928,8 @@ function isStaleDispatch(args) {
14604
14928
  }
14605
14929
  async function failDispatch(args) {
14606
14930
  await withDispatchLock(args.record.id, async (state) => {
14607
- const current = await state.get(
14608
- getDispatchStorageKey(args.record.id)
14931
+ const current = parseDispatchRecord(
14932
+ await state.get(getDispatchStorageKey(args.record.id))
14609
14933
  ) ?? args.record;
14610
14934
  if (isTerminalDispatchStatus(current.status)) {
14611
14935
  return;
@@ -14737,7 +15061,7 @@ async function recoverStaleDispatches(args) {
14737
15061
  }
14738
15062
  return recovered;
14739
15063
  }
14740
- async function runTrustedPluginHeartbeats(args) {
15064
+ async function runPluginHeartbeats(args) {
14741
15065
  let count = 0;
14742
15066
  for (const plugin of getAgentPlugins()) {
14743
15067
  if (count >= (args.limit ?? DEFAULT_PLUGIN_LIMIT)) {
@@ -14763,7 +15087,7 @@ async function runTrustedPluginHeartbeats(args) {
14763
15087
  );
14764
15088
  if (typeof result?.dispatchCount === "number" && result.dispatchCount > 0) {
14765
15089
  logInfo(
14766
- "trusted_plugin_heartbeat_dispatched",
15090
+ "plugin_heartbeat_dispatched",
14767
15091
  {},
14768
15092
  {
14769
15093
  "app.dispatch.count": result.dispatchCount,
@@ -14775,10 +15099,10 @@ async function runTrustedPluginHeartbeats(args) {
14775
15099
  } catch (error) {
14776
15100
  logException(
14777
15101
  error,
14778
- "trusted_plugin_heartbeat_failed",
15102
+ "plugin_heartbeat_failed",
14779
15103
  {},
14780
15104
  { "app.plugin.name": plugin.name },
14781
- "Trusted plugin heartbeat failed"
15105
+ "Plugin heartbeat failed"
14782
15106
  );
14783
15107
  }
14784
15108
  }
@@ -14793,7 +15117,7 @@ async function runHeartbeat(args) {
14793
15117
  nowMs: args.nowMs
14794
15118
  });
14795
15119
  await recoverStaleDispatches({ nowMs: args.nowMs });
14796
- await runTrustedPluginHeartbeats({ nowMs: args.nowMs });
15120
+ await runPluginHeartbeats({ nowMs: args.nowMs });
14797
15121
  }
14798
15122
 
14799
15123
  // src/handlers/heartbeat.ts
@@ -15325,10 +15649,6 @@ function getWorkspaceTeamId() {
15325
15649
  }
15326
15650
 
15327
15651
  // 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
15652
  function escapeRegExp2(value) {
15333
15653
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
15334
15654
  }
@@ -15404,14 +15724,6 @@ function getMessageTs(message) {
15404
15724
  const rawRecord = raw;
15405
15725
  return toOptionalString(rawRecord.ts) ?? toOptionalString(rawRecord.event_ts) ?? toOptionalString(rawRecord.message?.ts);
15406
15726
  }
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
15727
 
15416
15728
  // src/chat/runtime/processing-reaction.ts
15417
15729
  var noProcessingReaction = {
@@ -16114,9 +16426,10 @@ async function persistFailedReplyState(channelId, threadTs, sessionId, expectedV
16114
16426
  }
16115
16427
  async function resumeAuthorizedMcpTurn(args) {
16116
16428
  const { authSession, provider } = args;
16117
- if (!authSession.channelId || !authSession.threadTs) {
16429
+ if (!authSession.channelId || !authSession.destination || !authSession.threadTs) {
16118
16430
  return;
16119
16431
  }
16432
+ const destination = authSession.destination;
16120
16433
  const threadId = `slack:${authSession.channelId}:${authSession.threadTs}`;
16121
16434
  const currentState = await getPersistedThreadState(threadId);
16122
16435
  const conversation = coerceThreadConversationState(currentState);
@@ -16225,6 +16538,7 @@ async function resumeAuthorizedMcpTurn(args) {
16225
16538
  actor: { type: "user", userId: authSession.userId }
16226
16539
  },
16227
16540
  requester,
16541
+ destination,
16228
16542
  correlation: {
16229
16543
  conversationId: authSession.conversationId,
16230
16544
  turnId: lockedSessionId,
@@ -16313,6 +16627,7 @@ async function resumeAuthorizedMcpTurn(args) {
16313
16627
  }
16314
16628
  await scheduleTurnTimeoutResume({
16315
16629
  conversationId: authSession.conversationId,
16630
+ destination,
16316
16631
  sessionId: lockedSessionId,
16317
16632
  expectedVersion: version
16318
16633
  });
@@ -16410,13 +16725,23 @@ async function buildSkillsSummaryText() {
16410
16725
  }
16411
16726
  return lines.join("\n");
16412
16727
  }
16413
- async function hasConnectedAccount(userId, plugin, userTokenStore) {
16414
- if (plugin.manifest.credentials?.type === "oauth-bearer") {
16728
+ function accountLabel(account) {
16729
+ const label = account.label ?? account.id;
16730
+ return account.url ? `<${account.url}|${label}>` : label;
16731
+ }
16732
+ function connectedAccountText(plugin, account) {
16733
+ return account ? `*${plugin.manifest.name}*
16734
+ Connected as ${accountLabel(account)}` : `*${plugin.manifest.name}*
16735
+ ${plugin.manifest.description}`;
16736
+ }
16737
+ async function connectedOAuthTokens(userId, plugin, userTokenStore) {
16738
+ if (plugin.manifest.oauth || plugin.manifest.credentials) {
16415
16739
  const stored = await userTokenStore.get(userId, plugin.manifest.name);
16416
- return Boolean(
16417
- stored && hasRequiredOAuthScope(stored.scope, plugin.manifest.oauth?.scope)
16418
- );
16740
+ return stored && hasRequiredOAuthScope(stored.scope, plugin.manifest.oauth?.scope) ? stored : void 0;
16419
16741
  }
16742
+ return void 0;
16743
+ }
16744
+ async function hasConnectedMcpAccount(userId, plugin) {
16420
16745
  if (plugin.manifest.mcp) {
16421
16746
  return Boolean(
16422
16747
  (await getMcpStoredOAuthCredentials(userId, plugin.manifest.name))?.tokens
@@ -16431,13 +16756,13 @@ async function buildHomeView(userId, userTokenStore) {
16431
16756
  const providers = getPluginProviders();
16432
16757
  const connectedSections = [];
16433
16758
  for (const plugin of providers) {
16434
- if (!await hasConnectedAccount(userId, plugin, userTokenStore)) continue;
16759
+ const tokens = await connectedOAuthTokens(userId, plugin, userTokenStore);
16760
+ if (!tokens && !await hasConnectedMcpAccount(userId, plugin)) continue;
16435
16761
  connectedSections.push({
16436
16762
  type: "section",
16437
16763
  text: {
16438
16764
  type: "mrkdwn",
16439
- text: `*${plugin.manifest.name}*
16440
- ${plugin.manifest.description}`
16765
+ text: connectedAccountText(plugin, tokens?.account)
16441
16766
  },
16442
16767
  accessory: {
16443
16768
  type: "button",
@@ -16582,31 +16907,20 @@ async function persistFailedOAuthReplyState(args) {
16582
16907
  });
16583
16908
  }
16584
16909
  async function resumeOAuthSessionRecordTurn(stored) {
16585
- if (!stored.resumeConversationId || !stored.resumeSessionId || !stored.channelId || !stored.threadTs) {
16910
+ if (!stored.resumeConversationId || !stored.resumeSessionId || !stored.channelId || !stored.destination || !stored.threadTs) {
16586
16911
  return false;
16587
16912
  }
16588
- const sessionRecord = await getAgentTurnSessionRecord(
16589
- stored.resumeConversationId,
16590
- stored.resumeSessionId
16591
- );
16592
- if (!sessionRecord) {
16593
- return false;
16594
- }
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
- }
16601
- const currentState = await getPersistedThreadState(
16602
- stored.resumeConversationId
16913
+ const destination = stored.destination;
16914
+ const currentState = await getPersistedThreadState(
16915
+ stored.resumeConversationId
16603
16916
  );
16604
16917
  const conversation = coerceThreadConversationState(currentState);
16605
16918
  const pendingAuth = getConversationPendingAuth({
16606
16919
  conversation,
16607
16920
  kind: "plugin",
16608
16921
  provider: stored.provider,
16609
- requesterId: stored.userId
16922
+ requesterId: stored.userId,
16923
+ ...stored.scope ? { scope: stored.scope } : {}
16610
16924
  });
16611
16925
  const resolvedSessionId = pendingAuth?.sessionId ?? stored.resumeSessionId;
16612
16926
  const userMessage2 = resolvedSessionId ? getTurnUserMessage(conversation, resolvedSessionId) : void 0;
@@ -16623,32 +16937,34 @@ async function resumeOAuthSessionRecordTurn(stored) {
16623
16937
  });
16624
16938
  return true;
16625
16939
  }
16626
- } else {
16627
- if (!userMessage2?.author?.userId) {
16628
- return false;
16629
- }
16630
- if (conversation.processing.activeTurnId !== stored.resumeSessionId) {
16631
- return true;
16632
- }
16633
16940
  }
16634
- if (!userMessage2?.author?.userId || !resolvedSessionId) {
16941
+ const sessionRecord = await getAgentTurnSessionRecord(
16942
+ stored.resumeConversationId,
16943
+ resolvedSessionId
16944
+ );
16945
+ if (!sessionRecord) {
16946
+ return false;
16947
+ }
16948
+ if (sessionRecord.state === "completed" || sessionRecord.state === "failed" || sessionRecord.state === "abandoned") {
16949
+ return true;
16950
+ }
16951
+ if (sessionRecord.state !== "awaiting_resume" || sessionRecord.resumeReason !== "auth") {
16952
+ return true;
16953
+ }
16954
+ if (!userMessage2?.author?.userId) {
16635
16955
  return false;
16636
16956
  }
16957
+ if (!pendingAuth && conversation.processing.activeTurnId !== stored.resumeSessionId) {
16958
+ return true;
16959
+ }
16637
16960
  await resumeSlackTurn({
16638
- messageText: stored.pendingMessage ?? userMessage2.text,
16961
+ messageText: pendingAuth ? userMessage2.text : stored.pendingMessage ?? userMessage2.text,
16639
16962
  channelId: stored.channelId,
16640
16963
  threadTs: stored.threadTs,
16641
16964
  messageTs: getTurnUserSlackMessageTs(userMessage2),
16642
16965
  lockKey: stored.resumeConversationId,
16643
16966
  initialText: "",
16644
16967
  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
16968
  const lockedState = await getPersistedThreadState(
16653
16969
  stored.resumeConversationId
16654
16970
  );
@@ -16658,12 +16974,20 @@ async function resumeOAuthSessionRecordTurn(stored) {
16658
16974
  conversation: lockedConversation,
16659
16975
  kind: "plugin",
16660
16976
  provider: stored.provider,
16661
- requesterId: stored.userId
16977
+ requesterId: stored.userId,
16978
+ ...stored.scope ? { scope: stored.scope } : {}
16662
16979
  });
16663
16980
  const lockedSessionId = lockedPendingAuth?.sessionId ?? stored.resumeSessionId;
16664
16981
  if (lockedSessionId !== resolvedSessionId) {
16665
16982
  return false;
16666
16983
  }
16984
+ const lockedSessionRecord = await getAgentTurnSessionRecord(
16985
+ stored.resumeConversationId,
16986
+ lockedSessionId
16987
+ );
16988
+ if (!lockedSessionRecord || lockedSessionRecord.state !== "awaiting_resume" || lockedSessionRecord.resumeReason !== "auth") {
16989
+ return false;
16990
+ }
16667
16991
  if (lockedPendingAuth) {
16668
16992
  if (!isPendingAuthLatestRequest(lockedConversation, lockedPendingAuth)) {
16669
16993
  clearPendingAuth(lockedConversation, lockedPendingAuth.sessionId);
@@ -16711,7 +17035,7 @@ async function resumeOAuthSessionRecordTurn(stored) {
16711
17035
  lockedUserMessage.author.userId
16712
17036
  );
16713
17037
  return {
16714
- messageText: stored.pendingMessage ?? lockedUserMessage.text,
17038
+ messageText: lockedPendingAuth ? lockedUserMessage.text : stored.pendingMessage ?? lockedUserMessage.text,
16715
17039
  messageTs: getTurnUserSlackMessageTs(lockedUserMessage),
16716
17040
  replyContext: {
16717
17041
  credentialContext: {
@@ -16721,6 +17045,7 @@ async function resumeOAuthSessionRecordTurn(stored) {
16721
17045
  }
16722
17046
  },
16723
17047
  requester,
17048
+ destination,
16724
17049
  correlation: {
16725
17050
  conversationId: stored.resumeConversationId,
16726
17051
  turnId: lockedSessionId,
@@ -16797,6 +17122,7 @@ async function resumeOAuthSessionRecordTurn(stored) {
16797
17122
  }
16798
17123
  await scheduleTurnTimeoutResume({
16799
17124
  conversationId: stored.resumeConversationId,
17125
+ destination,
16800
17126
  sessionId: lockedSessionId,
16801
17127
  expectedVersion: version
16802
17128
  });
@@ -16807,7 +17133,9 @@ async function resumeOAuthSessionRecordTurn(stored) {
16807
17133
  return true;
16808
17134
  }
16809
17135
  async function resumePendingOAuthMessage(stored) {
16810
- if (!stored.pendingMessage || !stored.channelId || !stored.threadTs) return;
17136
+ if (!stored.pendingMessage || !stored.channelId || !stored.destination || !stored.threadTs) {
17137
+ return;
17138
+ }
16811
17139
  const threadId = `slack:${stored.channelId}:${stored.threadTs}`;
16812
17140
  const conversation = coerceThreadConversationState(
16813
17141
  await getPersistedThreadState(threadId)
@@ -16828,6 +17156,13 @@ async function resumePendingOAuthMessage(stored) {
16828
17156
  actor: { type: "user", userId: stored.userId }
16829
17157
  },
16830
17158
  requester,
17159
+ destination: stored.destination,
17160
+ correlation: {
17161
+ conversationId: threadId,
17162
+ channelId: stored.channelId,
17163
+ threadTs: stored.threadTs,
17164
+ requesterId: stored.userId
17165
+ },
16831
17166
  conversationContext,
16832
17167
  piMessages: conversation.piMessages,
16833
17168
  configuration: stored.configuration
@@ -16887,7 +17222,7 @@ async function GET4(request, provider, waitUntil) {
16887
17222
  }
16888
17223
  const stateAdapter = getStateAdapter();
16889
17224
  const stateKey2 = `oauth-state:${state}`;
16890
- const stored = await stateAdapter.get(stateKey2);
17225
+ const stored = parseOAuthStatePayload(await stateAdapter.get(stateKey2));
16891
17226
  if (!stored) {
16892
17227
  return htmlErrorResponse(
16893
17228
  "Link expired",
@@ -16921,6 +17256,7 @@ async function GET4(request, provider, waitUntil) {
16921
17256
  );
16922
17257
  }
16923
17258
  const redirectUri = `${baseUrl}${providerConfig.callbackPath}`;
17259
+ const requestedScope = stored.scope ?? providerConfig.scope;
16924
17260
  let tokenResponse;
16925
17261
  try {
16926
17262
  const tokenRequest = buildOAuthTokenRequest({
@@ -16953,13 +17289,12 @@ async function GET4(request, provider, waitUntil) {
16953
17289
  500
16954
17290
  );
16955
17291
  }
16956
- const tokenData = await tokenResponse.json();
16957
17292
  let parsedTokenResponse;
16958
17293
  try {
16959
- parsedTokenResponse = parseOAuthTokenResponse(
16960
- tokenData,
16961
- providerConfig.scope
16962
- );
17294
+ const tokenData = await tokenResponse.json();
17295
+ parsedTokenResponse = parseOAuthTokenResponse(tokenData, requestedScope, {
17296
+ treatEmptyScopeAsUnreported: providerConfig.treatEmptyScopeAsUnreported
17297
+ });
16963
17298
  } catch {
16964
17299
  return htmlErrorResponse(
16965
17300
  "Connection failed",
@@ -16967,7 +17302,7 @@ async function GET4(request, provider, waitUntil) {
16967
17302
  500
16968
17303
  );
16969
17304
  }
16970
- if (!hasRequiredOAuthScope(parsedTokenResponse.scope, providerConfig.scope)) {
17305
+ if (!hasRequiredOAuthScope(parsedTokenResponse.scope, requestedScope)) {
16971
17306
  return htmlErrorResponse(
16972
17307
  "Connection failed",
16973
17308
  `The ${providerLabel} authorization did not grant the access Junior requires. Return to Slack and ask Junior to connect your ${providerLabel} account again.`,
@@ -16975,7 +17310,23 @@ async function GET4(request, provider, waitUntil) {
16975
17310
  );
16976
17311
  }
16977
17312
  const userTokenStore = createUserTokenStore();
16978
- await userTokenStore.set(stored.userId, provider, parsedTokenResponse);
17313
+ let account;
17314
+ try {
17315
+ account = await resolvePluginOAuthAccount({
17316
+ provider,
17317
+ tokens: parsedTokenResponse
17318
+ });
17319
+ } catch {
17320
+ return htmlErrorResponse(
17321
+ "Connection failed",
17322
+ `Junior could not verify the connected ${providerLabel} account. Please try again.`,
17323
+ 500
17324
+ );
17325
+ }
17326
+ await userTokenStore.set(stored.userId, provider, {
17327
+ ...parsedTokenResponse,
17328
+ ...account ? { account } : {}
17329
+ });
16979
17330
  waitUntil(async () => {
16980
17331
  try {
16981
17332
  await publishAppHomeView(getSlackClient(), stored.userId, userTokenStore);
@@ -17023,6 +17374,148 @@ async function GET4(request, provider, waitUntil) {
17023
17374
  });
17024
17375
  }
17025
17376
 
17377
+ // src/chat/sandbox/egress-credentials.ts
17378
+ var HTTP_READ_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
17379
+ var SandboxEgressCredentialNeededError = class extends Error {
17380
+ authorization;
17381
+ grant;
17382
+ provider;
17383
+ constructor(input) {
17384
+ super(input.message);
17385
+ this.name = "SandboxEgressCredentialNeededError";
17386
+ this.authorization = input.authorization;
17387
+ this.grant = input.grant;
17388
+ this.provider = input.provider;
17389
+ }
17390
+ };
17391
+ function defaultGrantForProvider(input) {
17392
+ const access = HTTP_READ_METHODS.has(
17393
+ input.method.toUpperCase()
17394
+ ) ? "read" : "write";
17395
+ return {
17396
+ source: "broker",
17397
+ grant: {
17398
+ name: "default",
17399
+ access,
17400
+ reason: `sandbox-egress:${input.provider}:${access}`
17401
+ }
17402
+ };
17403
+ }
17404
+ function oauthAuthorizationForProvider(provider) {
17405
+ const oauth = getPluginOAuthConfig(provider);
17406
+ return oauth ? {
17407
+ type: "oauth",
17408
+ provider,
17409
+ ...oauth.scope ? { scope: oauth.scope } : {}
17410
+ } : void 0;
17411
+ }
17412
+ function credentialSubjectFromContext(context) {
17413
+ return "subject" in context.credentials && context.credentials.subject ? { type: "user", userId: context.credentials.subject.userId } : void 0;
17414
+ }
17415
+ function assertLeaseTransformsOwnedByProvider(provider, lease) {
17416
+ for (const transform of lease.headerTransforms) {
17417
+ if (resolveSandboxEgressProviderForHost(transform.domain) !== provider) {
17418
+ throw new Error(
17419
+ `Credential lease for ${provider} included header transform for unowned domain ${transform.domain}`
17420
+ );
17421
+ }
17422
+ }
17423
+ }
17424
+ async function selectSandboxEgressGrant(input) {
17425
+ if (!hasEgressCredentialHooks(input.provider)) {
17426
+ return defaultGrantForProvider(input);
17427
+ }
17428
+ const pluginGrant = await selectPluginGrant({
17429
+ provider: input.provider,
17430
+ method: input.method,
17431
+ upstreamUrl: input.upstreamUrl
17432
+ });
17433
+ if (!pluginGrant) {
17434
+ throw new Error(
17435
+ `Plugin "${input.provider}" grantForEgress must return a grant for sandbox egress`
17436
+ );
17437
+ }
17438
+ return { source: "plugin", grant: pluginGrant };
17439
+ }
17440
+ function authorizationForSandboxEgressGrant(provider, selection) {
17441
+ return selection.source === "broker" ? oauthAuthorizationForProvider(provider) : void 0;
17442
+ }
17443
+ async function sandboxEgressCredentialLease(provider, selection, context) {
17444
+ const { grant } = selection;
17445
+ const cached = await getSandboxEgressCredentialLease(
17446
+ provider,
17447
+ grant.name,
17448
+ context
17449
+ );
17450
+ if (cached) {
17451
+ if (selection.source === "plugin" && cached.grant.access !== grant.access) {
17452
+ throw new Error(
17453
+ `Cached credential lease for ${provider}/${grant.name} has ${cached.grant.access} access, but ${grant.access} was selected`
17454
+ );
17455
+ }
17456
+ return {
17457
+ ...cached,
17458
+ grant
17459
+ };
17460
+ }
17461
+ let lease;
17462
+ if (selection.source === "plugin") {
17463
+ const credentialSubject = credentialSubjectFromContext(context);
17464
+ const pluginResult = await issuePluginCredential({
17465
+ provider,
17466
+ grant,
17467
+ actor: context.credentials.actor,
17468
+ ...credentialSubject ? { credentialSubject } : {},
17469
+ userTokenStore: createUserTokenStore()
17470
+ });
17471
+ if (pluginResult.type === "needed") {
17472
+ throw new SandboxEgressCredentialNeededError({
17473
+ provider,
17474
+ grant,
17475
+ authorization: pluginResult.authorization,
17476
+ message: pluginResult.message
17477
+ });
17478
+ }
17479
+ if (pluginResult.type === "unavailable") {
17480
+ throw new CredentialUnavailableError(provider, pluginResult.message);
17481
+ }
17482
+ lease = pluginResult.lease;
17483
+ } else {
17484
+ lease = await issueProviderCredentialLease({
17485
+ context: context.credentials,
17486
+ provider,
17487
+ reason: grant.reason ?? `sandbox-egress:${provider}:default`
17488
+ });
17489
+ }
17490
+ const headerTransforms = lease.headerTransforms ?? [];
17491
+ if (headerTransforms.length === 0) {
17492
+ throw new Error(
17493
+ `Credential lease for ${provider} did not include header transforms`
17494
+ );
17495
+ }
17496
+ const leaseExpiresAtMs = Date.parse(lease.expiresAt);
17497
+ if (!Number.isFinite(leaseExpiresAtMs) || leaseExpiresAtMs <= Date.now()) {
17498
+ throw new Error(`Credential lease for ${provider} is expired`);
17499
+ }
17500
+ const authorization = selection.source === "broker" ? oauthAuthorizationForProvider(provider) : lease.authorization;
17501
+ const cachedLease = {
17502
+ provider,
17503
+ grant,
17504
+ ...lease.account ? { account: lease.account } : {},
17505
+ ...authorization ? { authorization } : {},
17506
+ expiresAt: lease.expiresAt,
17507
+ headerTransforms
17508
+ };
17509
+ assertLeaseTransformsOwnedByProvider(provider, cachedLease);
17510
+ await setSandboxEgressCredentialLease(context, cachedLease);
17511
+ return cachedLease;
17512
+ }
17513
+ function hasSandboxEgressLeaseTransformForHost(lease, host) {
17514
+ return lease.headerTransforms.some(
17515
+ (transform) => matchesSandboxEgressDomain(host, transform.domain)
17516
+ );
17517
+ }
17518
+
17026
17519
  // src/chat/sandbox/egress-oidc.ts
17027
17520
  import {
17028
17521
  createRemoteJWKSet,
@@ -17126,6 +17619,19 @@ var UPSTREAM_PERMISSION_REJECTION_STATUS = 403;
17126
17619
  function jsonError(message, status) {
17127
17620
  return Response.json({ error: message }, { status });
17128
17621
  }
17622
+ function authRequiredResponse(input) {
17623
+ return new Response(
17624
+ `junior-auth-required provider=${input.provider} grant=${input.grant.name} access=${input.grant.access} 401 unauthorized
17625
+ ${input.message}`,
17626
+ {
17627
+ status: 401,
17628
+ headers: {
17629
+ "content-type": "text/plain; charset=utf-8",
17630
+ "cache-control": "no-store"
17631
+ }
17632
+ }
17633
+ );
17634
+ }
17129
17635
  function shouldLogSandboxEgressInfo() {
17130
17636
  const environment = (process.env.SENTRY_ENVIRONMENT ?? process.env.VERCEL_ENV ?? process.env.NODE_ENV ?? "").trim().toLowerCase();
17131
17637
  return environment !== "production";
@@ -17133,7 +17639,10 @@ function shouldLogSandboxEgressInfo() {
17133
17639
  function egressAttributes(input) {
17134
17640
  return {
17135
17641
  ...input.egressId ? { "app.sandbox.egress_id": input.egressId } : {},
17136
- ...input.provider ? { "app.credential.provider": input.provider } : {},
17642
+ ...input.provider ? { "app.provider.name": input.provider } : {},
17643
+ ...input.grantName ? { "app.grant.name": input.grantName } : {},
17644
+ ...input.grantAccess ? { "app.grant.access": input.grantAccess } : {},
17645
+ ...input.grantReason ? { "app.grant.reason": input.grantReason } : {},
17137
17646
  ...input.host ? { "server.address": input.host } : {},
17138
17647
  ...input.method ? { "http.request.method": input.method } : {},
17139
17648
  ...input.path ? { "url.path": input.path } : {},
@@ -17169,9 +17678,42 @@ function routingAttributes(request, upstreamUrl) {
17169
17678
  };
17170
17679
  if (upstreamUrl) {
17171
17680
  attributes["app.sandbox.egress.upstream_path"] = upstreamUrl.pathname;
17681
+ const gitService = upstreamUrl.searchParams.get("service");
17682
+ if (upstreamUrl.hostname.toLowerCase() === "github.com" && (gitService === "git-upload-pack" || gitService === "git-receive-pack")) {
17683
+ attributes["app.sandbox.egress.git_service"] = gitService;
17684
+ }
17172
17685
  }
17173
17686
  return attributes;
17174
17687
  }
17688
+ function displayedUpstreamPath(upstreamUrl) {
17689
+ const gitService = upstreamUrl.searchParams.get("service");
17690
+ if (upstreamUrl.hostname.toLowerCase() === "github.com" && (gitService === "git-upload-pack" || gitService === "git-receive-pack")) {
17691
+ return `${upstreamUrl.pathname}?service=${gitService}`;
17692
+ }
17693
+ return upstreamUrl.pathname;
17694
+ }
17695
+ function upstreamPermissionAttributes(provider, upstream) {
17696
+ if (provider !== "github") {
17697
+ return {};
17698
+ }
17699
+ return {
17700
+ "app.github.accepted_permissions": upstream.headers.get("x-accepted-github-permissions") ?? void 0,
17701
+ "app.github.sso": upstream.headers.get("x-github-sso") ?? void 0
17702
+ };
17703
+ }
17704
+ function githubPermissionHeaders(upstream) {
17705
+ const acceptedPermissions = upstream.headers.get(
17706
+ "x-accepted-github-permissions"
17707
+ );
17708
+ const sso = upstream.headers.get("x-github-sso");
17709
+ return {
17710
+ ...acceptedPermissions ? { acceptedPermissions } : {},
17711
+ ...sso ? { sso } : {}
17712
+ };
17713
+ }
17714
+ function permissionDeniedMessage(provider, grant) {
17715
+ return `${provider} returned HTTP 403 after Junior injected the ${grant.name} grant. Junior forwarded the request; this is not a local runtime block.`;
17716
+ }
17175
17717
  function logSandboxEgressUpstreamRequest(input) {
17176
17718
  if (!shouldLogSandboxEgressInfo()) {
17177
17719
  return;
@@ -17182,6 +17724,9 @@ function logSandboxEgressUpstreamRequest(input) {
17182
17724
  {
17183
17725
  ...egressAttributes({
17184
17726
  egressId: input.egressId,
17727
+ grantAccess: input.grantAccess,
17728
+ grantName: input.grantName,
17729
+ grantReason: input.grantReason,
17185
17730
  host: input.upstreamUrl.hostname,
17186
17731
  method: input.request.method,
17187
17732
  path: input.upstreamUrl.pathname,
@@ -17191,7 +17736,7 @@ function logSandboxEgressUpstreamRequest(input) {
17191
17736
  ...routingAttributes(input.request, input.upstreamUrl),
17192
17737
  "app.sandbox.egress.upstream_ok": input.upstream.ok
17193
17738
  },
17194
- `Sandbox egress ${input.request.method} ${input.upstreamUrl.hostname}${input.upstreamUrl.pathname} -> ${input.upstream.status}`
17739
+ `Sandbox egress ${input.request.method} ${input.upstreamUrl.hostname}${displayedUpstreamPath(input.upstreamUrl)} -> ${input.upstream.status}`
17195
17740
  );
17196
17741
  }
17197
17742
  function normalizeHost(value) {
@@ -17306,35 +17851,6 @@ function responseHeaders(upstream) {
17306
17851
  });
17307
17852
  return headers;
17308
17853
  }
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
17854
  function isSandboxEgressForwardedRequest(request) {
17339
17855
  return Boolean(
17340
17856
  request.headers.get(OIDC_TOKEN_HEADER)?.trim() && request.headers.get(FORWARDED_HOST_HEADER)?.trim() && request.headers.get(FORWARDED_SCHEME_HEADER)?.trim()
@@ -17440,17 +17956,72 @@ async function proxySandboxEgressRequest(request, deps = {}) {
17440
17956
  403
17441
17957
  );
17442
17958
  }
17959
+ const grantSelection = await selectSandboxEgressGrant({
17960
+ provider,
17961
+ method: request.method,
17962
+ upstreamUrl
17963
+ });
17443
17964
  let lease;
17444
17965
  try {
17445
- lease = await credentialLease(provider, credentialContext);
17966
+ lease = await sandboxEgressCredentialLease(
17967
+ provider,
17968
+ grantSelection,
17969
+ credentialContext
17970
+ );
17446
17971
  } catch (error) {
17972
+ if (error instanceof SandboxEgressCredentialNeededError) {
17973
+ await setSandboxEgressAuthRequiredSignal(credentialContext, {
17974
+ provider: error.provider,
17975
+ grant: error.grant,
17976
+ ...error.authorization ? { authorization: error.authorization } : {},
17977
+ message: error.message
17978
+ });
17979
+ logWarn(
17980
+ "sandbox_egress_credential_needed",
17981
+ {},
17982
+ {
17983
+ ...egressAttributes({
17984
+ egressId: activeEgressId,
17985
+ grantAccess: error.grant.access,
17986
+ grantName: error.grant.name,
17987
+ grantReason: error.grant.reason,
17988
+ host: upstreamUrl.hostname,
17989
+ method: request.method,
17990
+ path: upstreamUrl.pathname,
17991
+ provider: error.provider,
17992
+ status: 401
17993
+ }),
17994
+ ...routingAttributes(request, upstreamUrl)
17995
+ },
17996
+ "Sandbox egress grant needs user authorization before issuing a credential lease"
17997
+ );
17998
+ return authRequiredResponse({
17999
+ provider: error.provider,
18000
+ grant: error.grant,
18001
+ message: error.message
18002
+ });
18003
+ }
17447
18004
  if (error instanceof CredentialUnavailableError) {
18005
+ const failedGrant = grantSelection.grant;
18006
+ const authorization = authorizationForSandboxEgressGrant(
18007
+ error.provider,
18008
+ grantSelection
18009
+ );
18010
+ await setSandboxEgressAuthRequiredSignal(credentialContext, {
18011
+ provider: error.provider,
18012
+ grant: failedGrant,
18013
+ ...authorization ? { authorization } : {},
18014
+ message: error.message
18015
+ });
17448
18016
  logWarn(
17449
18017
  "sandbox_egress_credential_unavailable",
17450
18018
  {},
17451
18019
  {
17452
18020
  ...egressAttributes({
17453
18021
  egressId: activeEgressId,
18022
+ grantAccess: failedGrant.access,
18023
+ grantName: failedGrant.name,
18024
+ grantReason: failedGrant.reason,
17454
18025
  host: upstreamUrl.hostname,
17455
18026
  method: request.method,
17456
18027
  path: upstreamUrl.pathname,
@@ -17459,26 +18030,26 @@ async function proxySandboxEgressRequest(request, deps = {}) {
17459
18030
  }),
17460
18031
  ...routingAttributes(request, upstreamUrl)
17461
18032
  },
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
- }
18033
+ "Sandbox egress credential lease is unavailable for selected grant"
17471
18034
  );
18035
+ return authRequiredResponse({
18036
+ provider: error.provider,
18037
+ grant: failedGrant,
18038
+ message: error.message
18039
+ });
17472
18040
  }
17473
18041
  throw error;
17474
18042
  }
17475
- if (!hasTransformForHost(lease, upstreamUrl.hostname)) {
18043
+ if (!hasSandboxEgressLeaseTransformForHost(lease, upstreamUrl.hostname)) {
17476
18044
  logWarn(
17477
18045
  "sandbox_egress_transform_missing",
17478
18046
  {},
17479
18047
  {
17480
18048
  ...egressAttributes({
17481
18049
  egressId: activeEgressId,
18050
+ grantAccess: lease.grant.access,
18051
+ grantName: lease.grant.name,
18052
+ grantReason: lease.grant.reason,
17482
18053
  host: upstreamUrl.hostname,
17483
18054
  method: request.method,
17484
18055
  path: upstreamUrl.pathname,
@@ -17494,9 +18065,9 @@ ${error.message}`,
17494
18065
  );
17495
18066
  return jsonError("Credential lease does not cover forwarded host", 403);
17496
18067
  }
17497
- const body = await requestBodyBytes(request);
17498
18068
  const fetchImpl = deps.fetch ?? fetch;
17499
18069
  const headers = requestHeaders(request, lease, upstreamUrl.hostname);
18070
+ const body = await requestBodyBytes(request);
17500
18071
  const intercepted = await deps.interceptHttp?.({
17501
18072
  provider,
17502
18073
  request: new Request(upstreamUrl, {
@@ -17517,6 +18088,9 @@ ${error.message}`,
17517
18088
  });
17518
18089
  logSandboxEgressUpstreamRequest({
17519
18090
  egressId: activeEgressId,
18091
+ grantAccess: lease.grant.access,
18092
+ grantName: lease.grant.name,
18093
+ grantReason: lease.grant.reason,
17520
18094
  provider,
17521
18095
  request,
17522
18096
  upstream,
@@ -17529,6 +18103,9 @@ ${error.message}`,
17529
18103
  {
17530
18104
  ...egressAttributes({
17531
18105
  egressId: activeEgressId,
18106
+ grantAccess: lease.grant.access,
18107
+ grantName: lease.grant.name,
18108
+ grantReason: lease.grant.reason,
17532
18109
  host: upstreamUrl.hostname,
17533
18110
  method: request.method,
17534
18111
  path: upstreamUrl.pathname,
@@ -17536,6 +18113,7 @@ ${error.message}`,
17536
18113
  status: upstream.status
17537
18114
  }),
17538
18115
  ...routingAttributes(request, upstreamUrl),
18116
+ ...upstreamPermissionAttributes(provider, upstream),
17539
18117
  "error.type": `http_${upstream.status}`
17540
18118
  },
17541
18119
  `Sandbox egress upstream returned HTTP ${upstream.status}`
@@ -17548,6 +18126,9 @@ ${error.message}`,
17548
18126
  {
17549
18127
  ...egressAttributes({
17550
18128
  egressId: activeEgressId,
18129
+ grantAccess: lease.grant.access,
18130
+ grantName: lease.grant.name,
18131
+ grantReason: lease.grant.reason,
17551
18132
  host: upstreamUrl.hostname,
17552
18133
  method: request.method,
17553
18134
  path: upstreamUrl.pathname,
@@ -17555,27 +18136,49 @@ ${error.message}`,
17555
18136
  status: upstream.status
17556
18137
  }),
17557
18138
  ...routingAttributes(request, upstreamUrl),
18139
+ ...upstreamPermissionAttributes(provider, upstream),
17558
18140
  ...upstream.status === UPSTREAM_TOKEN_REJECTION_STATUS ? {
17559
18141
  "app.sandbox.egress.www_authenticate": upstream.headers.get("www-authenticate") ?? void 0
17560
18142
  } : {}
17561
18143
  },
17562
18144
  upstream.status === UPSTREAM_TOKEN_REJECTION_STATUS ? "Sandbox egress upstream auth rejected injected credential" : "Sandbox egress upstream permission denied"
17563
18145
  );
17564
- await clearSandboxEgressCredentialLease(provider, credentialContext);
17565
18146
  if (upstream.status === UPSTREAM_TOKEN_REJECTION_STATUS) {
18147
+ await clearSandboxEgressCredentialLease(
18148
+ provider,
18149
+ lease.grant.name,
18150
+ credentialContext
18151
+ );
18152
+ await setSandboxEgressAuthRequiredSignal(credentialContext, {
18153
+ provider,
18154
+ grant: lease.grant,
18155
+ ...lease.authorization ? { authorization: lease.authorization } : {},
18156
+ message: `Provider rejected the injected ${provider} credential.`
18157
+ });
17566
18158
  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
- }
18159
+ return authRequiredResponse({
18160
+ provider,
18161
+ grant: lease.grant,
18162
+ message: `Provider rejected the injected ${provider} credential.
18163
+ `
18164
+ });
18165
+ } else {
18166
+ await clearSandboxEgressCredentialLease(
18167
+ provider,
18168
+ lease.grant.name,
18169
+ credentialContext
17578
18170
  );
18171
+ await setSandboxEgressPermissionDeniedSignal(credentialContext, {
18172
+ provider,
18173
+ grant: lease.grant,
18174
+ ...lease.account ? { account: lease.account } : {},
18175
+ message: permissionDeniedMessage(provider, lease.grant),
18176
+ source: "upstream",
18177
+ status: UPSTREAM_PERMISSION_REJECTION_STATUS,
18178
+ upstreamHost: upstreamUrl.hostname,
18179
+ upstreamPath: displayedUpstreamPath(upstreamUrl),
18180
+ ...provider === "github" ? githubPermissionHeaders(upstream) : {}
18181
+ });
17579
18182
  }
17580
18183
  }
17581
18184
  return new Response(upstream.body, {
@@ -17720,6 +18323,7 @@ async function resumeTimedOutTurn(payload, options = {}) {
17720
18323
  }
17721
18324
  },
17722
18325
  requester,
18326
+ destination: payload.destination,
17723
18327
  correlation: {
17724
18328
  conversationId: payload.conversationId,
17725
18329
  turnId: payload.sessionId,
@@ -17787,6 +18391,7 @@ async function resumeTimedOutTurn(payload, options = {}) {
17787
18391
  }
17788
18392
  await scheduleTurnTimeoutResume2({
17789
18393
  conversationId: payload.conversationId,
18394
+ destination: payload.destination,
17790
18395
  sessionId: payload.sessionId,
17791
18396
  expectedVersion: version
17792
18397
  });
@@ -17864,14 +18469,14 @@ async function POST2(request, waitUntil, options = {}) {
17864
18469
  }
17865
18470
 
17866
18471
  // 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(
18472
+ import { z as z4 } from "zod";
18473
+ var replyDecisionSchema = z4.object({
18474
+ should_reply: z4.boolean().describe("Whether Junior should respond to this thread message."),
18475
+ should_unsubscribe: z4.boolean().optional().describe(
17871
18476
  "Whether Junior should unsubscribe from this thread because the user clearly asked it to stop participating."
17872
18477
  ),
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.")
18478
+ confidence: z4.number().min(0).max(1).describe("Classifier confidence from 0 to 1."),
18479
+ reason: z4.string().optional().describe("Short reason for the decision.")
17875
18480
  });
17876
18481
  var ROUTER_CONFIDENCE_THRESHOLD = 0.8;
17877
18482
  var ROUTER_CLASSIFIER_MAX_TOKENS = 240;
@@ -18181,6 +18786,9 @@ async function decideSubscribedThreadReply(args) {
18181
18786
  reasonDetail: reason
18182
18787
  };
18183
18788
  } catch (error) {
18789
+ if (isProviderRetryError(error)) {
18790
+ throw error;
18791
+ }
18184
18792
  args.logClassifierFailure(error, args.input);
18185
18793
  return {
18186
18794
  shouldReply: false,
@@ -18265,6 +18873,9 @@ async function ensureSlackMessageActorIdentity(message, lookupSlackUser2) {
18265
18873
 
18266
18874
  // src/chat/runtime/slack-runtime.ts
18267
18875
  var THREAD_OPTOUT_ACK = "Understood. I'll stay out of this thread unless someone @mentions me again.";
18876
+ function shouldRethrowTurnControlError(error) {
18877
+ return isCooperativeTurnYieldError(error) || isTurnInputCommitLostError(error) || isProviderRetryError(error);
18878
+ }
18268
18879
  async function maybeHandleThreadOptOutDecision(args) {
18269
18880
  if (!args.decision?.shouldUnsubscribe) {
18270
18881
  return false;
@@ -18294,7 +18905,7 @@ function getQueuedMessagesFromSlackMessages(messages, options) {
18294
18905
  );
18295
18906
  }
18296
18907
  function createSteeringMessageDrain(hooks, options) {
18297
- if (!hooks?.drainSteeringMessages) {
18908
+ if (!hooks.drainSteeringMessages) {
18298
18909
  return void 0;
18299
18910
  }
18300
18911
  return async (inject) => {
@@ -18332,7 +18943,7 @@ function createSlackTurnRuntime(deps) {
18332
18943
  if (shouldKeepProcessingReactionForToolInvocation(invocation)) {
18333
18944
  processingReaction.keep();
18334
18945
  }
18335
- hooks?.onToolInvocation?.(invocation);
18946
+ hooks.onToolInvocation?.(invocation);
18336
18947
  };
18337
18948
  };
18338
18949
  const stopProcessingReactions = async (processingReactions) => {
@@ -18450,7 +19061,7 @@ function createSlackTurnRuntime(deps) {
18450
19061
  );
18451
19062
  await deps.withSpan("chat.turn", "chat.turn", context, async () => {
18452
19063
  await thread.subscribe();
18453
- const queuedMessages = getQueuedMessages(hooks?.messageContext, {
19064
+ const queuedMessages = getQueuedMessages(hooks.messageContext, {
18454
19065
  explicitMention: true,
18455
19066
  stripLeadingBotMention: deps.stripLeadingBotMention
18456
19067
  });
@@ -18467,7 +19078,7 @@ function createSlackTurnRuntime(deps) {
18467
19078
  );
18468
19079
  };
18469
19080
  const onInputCommitted = async () => {
18470
- await hooks?.onInputCommitted?.();
19081
+ await hooks.onInputCommitted?.();
18471
19082
  await startQueuedProcessingReactions();
18472
19083
  };
18473
19084
  const drainSteeringMessages = createSteeringMessageDrain(hooks, {
@@ -18483,18 +19094,19 @@ function createSlackTurnRuntime(deps) {
18483
19094
  });
18484
19095
  await deps.replyToThread(thread, message, {
18485
19096
  explicitMention: true,
18486
- beforeFirstResponsePost: hooks?.beforeFirstResponsePost,
19097
+ beforeFirstResponsePost: hooks.beforeFirstResponsePost,
19098
+ destination: hooks.destination,
18487
19099
  queuedMessages,
18488
19100
  onInputCommitted,
18489
19101
  onToolInvocation: toolInvocationHook,
18490
19102
  onTurnCompleted,
18491
19103
  drainSteeringMessages,
18492
- onTurnStatePersisted: hooks?.onTurnStatePersisted,
18493
- shouldYield: hooks?.shouldYield
19104
+ onTurnStatePersisted: hooks.onTurnStatePersisted,
19105
+ shouldYield: hooks.shouldYield
18494
19106
  });
18495
19107
  });
18496
19108
  } catch (error) {
18497
- if (isCooperativeTurnYieldError(error) || isTurnInputCommitLostError(error)) {
19109
+ if (shouldRethrowTurnControlError(error)) {
18498
19110
  throw error;
18499
19111
  }
18500
19112
  const errorContext = logContext({
@@ -18526,7 +19138,7 @@ function createSlackTurnRuntime(deps) {
18526
19138
  "Sentry did not return an event ID for mention_handler_failed"
18527
19139
  );
18528
19140
  }
18529
- await hooks?.beforeFirstResponsePost?.();
19141
+ await hooks.beforeFirstResponsePost?.();
18530
19142
  await postFallbackErrorReplyWithLogging({
18531
19143
  thread,
18532
19144
  errorContext,
@@ -18580,7 +19192,7 @@ function createSlackTurnRuntime(deps) {
18580
19192
  channelId,
18581
19193
  runId
18582
19194
  };
18583
- const queuedMessages = getQueuedMessages(hooks?.messageContext, {
19195
+ const queuedMessages = getQueuedMessages(hooks.messageContext, {
18584
19196
  explicitMention: Boolean(message.isMention),
18585
19197
  stripLeadingBotMention: deps.stripLeadingBotMention
18586
19198
  });
@@ -18639,7 +19251,7 @@ function createSlackTurnRuntime(deps) {
18639
19251
  if (await maybeHandleThreadOptOutDecision({
18640
19252
  thread,
18641
19253
  decision,
18642
- beforeFirstResponsePost: hooks?.beforeFirstResponsePost
19254
+ beforeFirstResponsePost: hooks.beforeFirstResponsePost
18643
19255
  })) {
18644
19256
  await skipSubscribedMessage({
18645
19257
  thread,
@@ -18679,7 +19291,7 @@ function createSlackTurnRuntime(deps) {
18679
19291
  );
18680
19292
  };
18681
19293
  const onInputCommitted = async () => {
18682
- await hooks?.onInputCommitted?.();
19294
+ await hooks.onInputCommitted?.();
18683
19295
  await startQueuedProcessingReactions();
18684
19296
  };
18685
19297
  const toolInvocationHook = createToolInvocationHook(
@@ -18688,19 +19300,20 @@ function createSlackTurnRuntime(deps) {
18688
19300
  );
18689
19301
  await deps.replyToThread(thread, message, {
18690
19302
  explicitMention: Boolean(message.isMention),
19303
+ destination: hooks.destination,
18691
19304
  preparedState,
18692
- beforeFirstResponsePost: hooks?.beforeFirstResponsePost,
19305
+ beforeFirstResponsePost: hooks.beforeFirstResponsePost,
18693
19306
  queuedMessages,
18694
19307
  onInputCommitted,
18695
19308
  onToolInvocation: toolInvocationHook,
18696
19309
  onTurnCompleted,
18697
19310
  drainSteeringMessages,
18698
- onTurnStatePersisted: hooks?.onTurnStatePersisted,
18699
- shouldYield: hooks?.shouldYield
19311
+ onTurnStatePersisted: hooks.onTurnStatePersisted,
19312
+ shouldYield: hooks.shouldYield
18700
19313
  });
18701
19314
  });
18702
19315
  } catch (error) {
18703
- if (isCooperativeTurnYieldError(error) || isTurnInputCommitLostError(error)) {
19316
+ if (shouldRethrowTurnControlError(error)) {
18704
19317
  throw error;
18705
19318
  }
18706
19319
  const errorContext = logContext({
@@ -18732,7 +19345,7 @@ function createSlackTurnRuntime(deps) {
18732
19345
  "Sentry did not return an event ID for subscribed_message_handler_failed"
18733
19346
  );
18734
19347
  }
18735
- await hooks?.beforeFirstResponsePost?.();
19348
+ await hooks.beforeFirstResponsePost?.();
18736
19349
  await postFallbackErrorReplyWithLogging({
18737
19350
  thread,
18738
19351
  errorContext,
@@ -19857,7 +20470,7 @@ async function loadPiMessagesForTurn(args) {
19857
20470
  return { piMessages: fallback };
19858
20471
  }
19859
20472
  function createReplyToThread(deps) {
19860
- return async function replyToThread(thread, message, options = {}) {
20473
+ return async function replyToThread(thread, message, options) {
19861
20474
  if (message.author.isMe) {
19862
20475
  return;
19863
20476
  }
@@ -19872,7 +20485,8 @@ function createReplyToThread(deps) {
19872
20485
  const threadTs = getThreadTs(threadId);
19873
20486
  const assistantThreadContext = getAssistantThreadContext(message);
19874
20487
  const messageTs = getMessageTs(message);
19875
- const teamId = getTeamId(message);
20488
+ const destination = options.destination;
20489
+ const teamId = destination.teamId;
19876
20490
  const runId = getRunId(thread, message);
19877
20491
  const conversationId = threadId ?? runId;
19878
20492
  await withSpan(
@@ -20089,6 +20703,7 @@ function createReplyToThread(deps) {
20089
20703
  state: "running",
20090
20704
  surface: "slack",
20091
20705
  requester,
20706
+ destination,
20092
20707
  traceId: getActiveTraceId()
20093
20708
  }).catch((error) => {
20094
20709
  logException(
@@ -20269,6 +20884,7 @@ function createReplyToThread(deps) {
20269
20884
  omittedImageAttachmentCount,
20270
20885
  userAttachments,
20271
20886
  slackConversation,
20887
+ destination,
20272
20888
  surface: "slack",
20273
20889
  turnDeadlineAtMs: getTurnRequestDeadline()?.deadlineAtMs,
20274
20890
  correlation: {
@@ -20427,6 +21043,7 @@ function createReplyToThread(deps) {
20427
21043
  state: "completed",
20428
21044
  conversationTitle: titleUpdateResult?.title,
20429
21045
  requester,
21046
+ destination,
20430
21047
  traceId: getActiveTraceId()
20431
21048
  });
20432
21049
  }
@@ -20467,10 +21084,11 @@ function createReplyToThread(deps) {
20467
21084
  const conversationIdForResume = error.metadata?.conversationId;
20468
21085
  const sessionIdForResume = error.metadata?.sessionId;
20469
21086
  const version = error.metadata?.version;
20470
- if (conversationIdForResume && sessionIdForResume && typeof version === "number") {
21087
+ if (conversationIdForResume && sessionIdForResume && typeof version === "number" && destination) {
20471
21088
  try {
20472
21089
  await deps.services.scheduleTurnTimeoutResume({
20473
21090
  conversationId: conversationIdForResume,
21091
+ destination,
20474
21092
  sessionId: sessionIdForResume,
20475
21093
  expectedVersion: version
20476
21094
  });
@@ -20578,6 +21196,7 @@ function createReplyToThread(deps) {
20578
21196
  startedAtMs: message.metadata.dateSent.getTime(),
20579
21197
  state: "failed",
20580
21198
  requester,
21199
+ destination,
20581
21200
  traceId: getActiveTraceId()
20582
21201
  });
20583
21202
  const sessionRecord = await getAgentTurnSessionRecord(
@@ -21016,7 +21635,7 @@ function nonEmptyString(value) {
21016
21635
  return trimmed || void 0;
21017
21636
  }
21018
21637
 
21019
- // src/chat/queue/thread-message-dispatcher.ts
21638
+ // src/chat/slack/attachment-fetchers.ts
21020
21639
  function rehydrateAttachmentFetchers(message, downloadPrivateSlackFile2 = downloadPrivateSlackFile) {
21021
21640
  for (const attachment of message.attachments) {
21022
21641
  if (!attachment.fetchData && attachment.url) {
@@ -21316,6 +21935,7 @@ function createSlackConversationWorker(options) {
21316
21935
  try {
21317
21936
  if (route === "mention") {
21318
21937
  await options.runtime.handleNewMention(thread, latestMessage, {
21938
+ destination: context.destination,
21319
21939
  messageContext,
21320
21940
  drainSteeringMessages,
21321
21941
  onInputCommitted,
@@ -21324,6 +21944,7 @@ function createSlackConversationWorker(options) {
21324
21944
  return;
21325
21945
  }
21326
21946
  await options.runtime.handleSubscribedMessage(thread, latestMessage, {
21947
+ destination: context.destination,
21327
21948
  messageContext,
21328
21949
  drainSteeringMessages,
21329
21950
  onInputCommitted,
@@ -21348,8 +21969,16 @@ function createSlackConversationWorker(options) {
21348
21969
  }
21349
21970
  function buildSlackInboundMessage(args) {
21350
21971
  const authorId = requireSlackAuthorId(args.message);
21972
+ const destination = createSlackDestination({
21973
+ channelId: args.thread.channelId,
21974
+ teamId: args.installation?.teamId
21975
+ });
21976
+ if (!destination) {
21977
+ throw new Error("Slack inbound message requires destination context");
21978
+ }
21351
21979
  return {
21352
21980
  conversationId: args.conversationId,
21981
+ destination,
21353
21982
  inboundMessageId: [
21354
21983
  "slack",
21355
21984
  args.installation?.teamId ?? args.installation?.enterpriseId ?? "unknown",
@@ -22256,7 +22885,11 @@ async function POST3(request, platform, waitUntil) {
22256
22885
  }
22257
22886
 
22258
22887
  // src/chat/task-execution/vercel-callback.ts
22259
- import { handleCallback } from "@vercel/queue";
22888
+ import {
22889
+ handleCallback,
22890
+ QueueClient,
22891
+ registerDevConsumer
22892
+ } from "@vercel/queue";
22260
22893
 
22261
22894
  // src/chat/task-execution/worker.ts
22262
22895
  var CONVERSATION_WORK_DEFER_DELAY_MS = 15e3;
@@ -22269,7 +22902,10 @@ function nudgeIdempotencyKey(reason, conversationId, nowMs) {
22269
22902
  }
22270
22903
  async function sendWakeNudge(args) {
22271
22904
  await args.options.queue.send(
22272
- { conversationId: args.conversationId },
22905
+ {
22906
+ conversationId: args.conversationId,
22907
+ destination: args.destination
22908
+ },
22273
22909
  {
22274
22910
  delayMs: args.delayMs,
22275
22911
  idempotencyKey: args.idempotencyKey
@@ -22284,6 +22920,7 @@ async function sendWakeNudge(args) {
22284
22920
  async function requestLostLeaseRecovery(args) {
22285
22921
  const continuationMarked = await requestConversationContinuation({
22286
22922
  conversationId: args.conversationId,
22923
+ destination: args.destination,
22287
22924
  leaseToken: args.leaseToken,
22288
22925
  nowMs: args.nowMs,
22289
22926
  state: args.options.state
@@ -22302,6 +22939,7 @@ async function requestLostLeaseRecovery(args) {
22302
22939
  }
22303
22940
  await sendWakeNudge({
22304
22941
  conversationId: args.conversationId,
22942
+ destination: args.destination,
22305
22943
  idempotencyKey: nudgeIdempotencyKey(
22306
22944
  "lost_lease",
22307
22945
  args.conversationId,
@@ -22345,7 +22983,8 @@ function startLeaseCheckIn(args) {
22345
22983
  timer.unref?.();
22346
22984
  return timer;
22347
22985
  }
22348
- async function processConversationWork(conversationId, options) {
22986
+ async function processConversationWork(message, options) {
22987
+ const conversationId = message.conversationId;
22349
22988
  const initial = await getConversationWorkState({
22350
22989
  conversationId,
22351
22990
  state: options.state
@@ -22353,6 +22992,12 @@ async function processConversationWork(conversationId, options) {
22353
22992
  if (!initial || countPendingConversationMessages(initial) === 0 && !initial.needsRun && !initial.lease) {
22354
22993
  return { status: "no_work" };
22355
22994
  }
22995
+ if (!sameDestination(initial.destination, message.destination)) {
22996
+ throw new Error(
22997
+ `Conversation work queue destination changed for ${conversationId}`
22998
+ );
22999
+ }
23000
+ const destination = initial.destination;
22356
23001
  const lease = await startConversationWork({
22357
23002
  conversationId,
22358
23003
  nowMs: now2(options),
@@ -22365,6 +23010,7 @@ async function processConversationWork(conversationId, options) {
22365
23010
  const nudgeNowMs = now2(options);
22366
23011
  await sendWakeNudge({
22367
23012
  conversationId,
23013
+ destination,
22368
23014
  delayMs: CONVERSATION_WORK_DEFER_DELAY_MS,
22369
23015
  idempotencyKey: nudgeIdempotencyKey("active", conversationId, nudgeNowMs),
22370
23016
  nowMs: nudgeNowMs,
@@ -22403,6 +23049,7 @@ async function processConversationWork(conversationId, options) {
22403
23049
  );
22404
23050
  const workerContext = {
22405
23051
  conversationId,
23052
+ destination,
22406
23053
  leaseToken: lease.leaseToken,
22407
23054
  shouldYield: () => leaseLost || now2(options) >= softYieldDeadlineMs,
22408
23055
  checkIn: async () => {
@@ -22430,6 +23077,7 @@ async function processConversationWork(conversationId, options) {
22430
23077
  if (result.status === "lost_lease") {
22431
23078
  await requestLostLeaseRecovery({
22432
23079
  conversationId,
23080
+ destination,
22433
23081
  leaseToken: lease.leaseToken,
22434
23082
  nowMs: now2(options),
22435
23083
  options
@@ -22439,6 +23087,7 @@ async function processConversationWork(conversationId, options) {
22439
23087
  if (leaseLost) {
22440
23088
  await requestLostLeaseRecovery({
22441
23089
  conversationId,
23090
+ destination,
22442
23091
  leaseToken: lease.leaseToken,
22443
23092
  nowMs: now2(options),
22444
23093
  options
@@ -22449,6 +23098,7 @@ async function processConversationWork(conversationId, options) {
22449
23098
  const yieldNowMs = now2(options);
22450
23099
  const continuationMarked = await requestConversationContinuation({
22451
23100
  conversationId,
23101
+ destination,
22452
23102
  leaseToken: lease.leaseToken,
22453
23103
  nowMs: yieldNowMs,
22454
23104
  state: options.state
@@ -22458,6 +23108,7 @@ async function processConversationWork(conversationId, options) {
22458
23108
  }
22459
23109
  await sendWakeNudge({
22460
23110
  conversationId,
23111
+ destination,
22461
23112
  idempotencyKey: nudgeIdempotencyKey(
22462
23113
  "yield",
22463
23114
  conversationId,
@@ -22496,6 +23147,7 @@ async function processConversationWork(conversationId, options) {
22496
23147
  const nudgeNowMs = now2(options);
22497
23148
  await sendWakeNudge({
22498
23149
  conversationId,
23150
+ destination,
22499
23151
  idempotencyKey: nudgeIdempotencyKey(
22500
23152
  "pending",
22501
23153
  conversationId,
@@ -22520,6 +23172,7 @@ async function processConversationWork(conversationId, options) {
22520
23172
  try {
22521
23173
  const continuationMarked = await requestConversationContinuation({
22522
23174
  conversationId,
23175
+ destination,
22523
23176
  leaseToken: lease.leaseToken,
22524
23177
  nowMs: errorNowMs,
22525
23178
  state: options.state
@@ -22527,6 +23180,7 @@ async function processConversationWork(conversationId, options) {
22527
23180
  if (continuationMarked) {
22528
23181
  await sendWakeNudge({
22529
23182
  conversationId,
23183
+ destination,
22530
23184
  idempotencyKey: nudgeIdempotencyKey(
22531
23185
  "error",
22532
23186
  conversationId,
@@ -22561,15 +23215,17 @@ async function processConversationWork(conversationId, options) {
22561
23215
  "Conversation work release failed after runner error"
22562
23216
  );
22563
23217
  }
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
- );
23218
+ if (!isProviderRetryError(error)) {
23219
+ logException(
23220
+ error,
23221
+ "conversation_work_failed",
23222
+ { conversationId },
23223
+ {
23224
+ "app.worker.elapsed_ms": now2(options) - startedAtMs
23225
+ },
23226
+ "Conversation work failed"
23227
+ );
23228
+ }
22573
23229
  throw error;
22574
23230
  } finally {
22575
23231
  clearInterval(timer);
@@ -22578,12 +23234,19 @@ async function processConversationWork(conversationId, options) {
22578
23234
 
22579
23235
  // src/chat/task-execution/vercel-callback.ts
22580
23236
  var CONVERSATION_WORK_VISIBILITY_TIMEOUT_BUFFER_SECONDS = 30;
23237
+ var CONVERSATION_WORK_DEV_CONSUMER_GROUP = "junior_conversation_work_dev";
22581
23238
  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");
23239
+ const destination = parseDestination(
23240
+ message?.destination
23241
+ );
23242
+ if (!message || typeof message !== "object" || typeof message.conversationId !== "string" || !message.conversationId.trim() || !destination) {
23243
+ throw new Error(
23244
+ "Conversation queue message is missing destination context"
23245
+ );
22584
23246
  }
22585
23247
  return {
22586
- conversationId: message.conversationId
23248
+ conversationId: message.conversationId,
23249
+ destination
22587
23250
  };
22588
23251
  }
22589
23252
  function resolveConversationWorkVisibilityTimeoutSeconds(functionMaxDurationSeconds = getChatConfig().functionMaxDurationSeconds) {
@@ -22591,7 +23254,7 @@ function resolveConversationWorkVisibilityTimeoutSeconds(functionMaxDurationSeco
22591
23254
  }
22592
23255
  async function processConversationQueueMessage(message, options) {
22593
23256
  const parsed = parseConversationQueueMessage(message);
22594
- return await processConversationWork(parsed.conversationId, {
23257
+ return await processConversationWork(parsed, {
22595
23258
  checkInIntervalMs: options.checkInIntervalMs,
22596
23259
  nowMs: options.nowMs,
22597
23260
  queue: options.queue ?? getVercelConversationWorkQueue(),
@@ -22600,22 +23263,35 @@ async function processConversationQueueMessage(message, options) {
22600
23263
  state: options.state
22601
23264
  });
22602
23265
  }
23266
+ async function handleConversationQueueMessage(message, options) {
23267
+ const verified = verifySignedConversationQueueMessage(message);
23268
+ if (!verified) {
23269
+ throw new Error("Unauthorized conversation queue message");
23270
+ }
23271
+ await runWithTurnRequestDeadline(
23272
+ () => processConversationQueueMessage(verified, options)
23273
+ );
23274
+ }
22603
23275
  function createVercelConversationWorkCallback(options) {
22604
23276
  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
- },
23277
+ (message) => handleConversationQueueMessage(message, options),
22614
23278
  {
22615
23279
  visibilityTimeoutSeconds: options.visibilityTimeoutSeconds ?? resolveConversationWorkVisibilityTimeoutSeconds()
22616
23280
  }
22617
23281
  );
22618
23282
  }
23283
+ function registerVercelConversationWorkDevConsumer(options) {
23284
+ if (process.env.NODE_ENV !== "development") {
23285
+ return void 0;
23286
+ }
23287
+ return registerDevConsumer({
23288
+ client: new QueueClient(),
23289
+ consumerGroup: CONVERSATION_WORK_DEV_CONSUMER_GROUP,
23290
+ handler: (message) => handleConversationQueueMessage(message, options),
23291
+ topic: resolveConversationWorkQueueTopic(options),
23292
+ visibilityTimeoutSeconds: options.visibilityTimeoutSeconds ?? resolveConversationWorkVisibilityTimeoutSeconds()
23293
+ });
23294
+ }
22619
23295
 
22620
23296
  // src/app.ts
22621
23297
  async function defaultWaitUntil() {
@@ -22638,7 +23314,7 @@ async function resolveVirtualConfig() {
22638
23314
  return {
22639
23315
  pluginSet: mod.pluginSet,
22640
23316
  plugins: mod.plugins,
22641
- trustedPluginRegistrations: mod.trustedPluginRegistrations ?? []
23317
+ pluginHookRegistrations: mod.pluginHookRegistrations ?? []
22642
23318
  };
22643
23319
  } catch (error) {
22644
23320
  if (!isMissingVirtualConfig(error)) {
@@ -22707,20 +23383,20 @@ function validateBuildIncludesPluginPackages(pluginConfig, virtualConfig) {
22707
23383
  `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
23384
  );
22709
23385
  }
22710
- function validateBuildIncludesTrustedRegistrations(trustedRegistrations, virtualConfig) {
22711
- const bundledTrustedRegistrations = virtualConfig?.trustedPluginRegistrations ?? [];
22712
- if (bundledTrustedRegistrations.length === 0) {
23386
+ function validateBuildIncludesPluginHookRegistrations(hookRegistrations, virtualConfig) {
23387
+ const bundledHookRegistrations = virtualConfig?.pluginHookRegistrations ?? [];
23388
+ if (bundledHookRegistrations.length === 0) {
22713
23389
  return;
22714
23390
  }
22715
- const registered = new Set(trustedRegistrations.map((plugin) => plugin.name));
22716
- const missing = bundledTrustedRegistrations.filter(
23391
+ const registered = new Set(hookRegistrations.map((plugin) => plugin.name));
23392
+ const missing = bundledHookRegistrations.filter(
22717
23393
  (pluginName) => !registered.has(pluginName)
22718
23394
  );
22719
23395
  if (missing.length === 0) {
22720
23396
  return;
22721
23397
  }
22722
23398
  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 }).`
23399
+ `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
23400
  );
22725
23401
  }
22726
23402
  function validatePluginRegistrations(registrations) {
@@ -22736,6 +23412,51 @@ function validatePluginRegistrations(registrations) {
22736
23412
  }
22737
23413
  }
22738
23414
  }
23415
+ function validatePluginEgressCredentialHooks(registrations) {
23416
+ const plugins = new Map(
23417
+ registrations.map((registration) => [registration.name, registration])
23418
+ );
23419
+ for (const provider of getPluginProviders()) {
23420
+ const hooks = plugins.get(provider.manifest.name)?.hooks;
23421
+ const hasGrantHook = Boolean(hooks?.grantForEgress);
23422
+ const hasIssueHook = Boolean(hooks?.issueCredential);
23423
+ const hasGenericCredentials = Boolean(
23424
+ provider.manifest.credentials || provider.manifest.apiHeaders
23425
+ );
23426
+ const hasDomains = Boolean(provider.manifest.domains?.length);
23427
+ const hasHookManagedOAuth = Boolean(
23428
+ provider.manifest.oauth && !provider.manifest.credentials
23429
+ );
23430
+ if (!hasGrantHook && !hasIssueHook) {
23431
+ if (hasDomains && !hasGenericCredentials) {
23432
+ throw new Error(
23433
+ `Plugin "${provider.manifest.name}" manifest.domains requires egress credential hooks when no generic credentials or apiHeaders are configured.`
23434
+ );
23435
+ }
23436
+ if (hasHookManagedOAuth) {
23437
+ throw new Error(
23438
+ `Plugin "${provider.manifest.name}" manifest.oauth without oauth-bearer credentials requires egress credential hooks.`
23439
+ );
23440
+ }
23441
+ continue;
23442
+ }
23443
+ if (!hasGrantHook || !hasIssueHook) {
23444
+ throw new Error(
23445
+ `Plugin "${provider.manifest.name}" egress credential hooks must include both grantForEgress and issueCredential.`
23446
+ );
23447
+ }
23448
+ if (hasGenericCredentials) {
23449
+ throw new Error(
23450
+ `Plugin "${provider.manifest.name}" egress credential hooks must use manifest.domains instead of generic credentials or apiHeaders.`
23451
+ );
23452
+ }
23453
+ if (!hasDomains) {
23454
+ throw new Error(
23455
+ `Plugin "${provider.manifest.name}" egress credential hooks require manifest.domains to list sandbox egress hosts.`
23456
+ );
23457
+ }
23458
+ }
23459
+ }
22739
23460
  function mountAgentPluginRoutes(app, routes) {
22740
23461
  for (const route of routes) {
22741
23462
  const handler = (c) => route.handler(c.req.raw);
@@ -22753,12 +23474,12 @@ function mountAgentPluginRoutes(app, routes) {
22753
23474
  async function createApp(options) {
22754
23475
  const virtualConfig = await resolveVirtualConfig();
22755
23476
  const configuredPlugins = options?.plugins ?? virtualConfig?.pluginSet;
22756
- const agentPlugins = trustedPluginRegistrationsFromPluginSet(configuredPlugins);
23477
+ const agentPlugins = pluginHookRegistrationsFromPluginSet(configuredPlugins);
22757
23478
  const pluginConfig = configuredPlugins ? pluginCatalogConfigFromPluginSet(configuredPlugins) : virtualConfig?.plugins ?? resolveEnvPluginCatalogConfig();
22758
23479
  if (configuredPlugins) {
22759
23480
  validateBuildIncludesPluginPackages(pluginConfig, virtualConfig);
22760
23481
  }
22761
- validateBuildIncludesTrustedRegistrations(agentPlugins, virtualConfig);
23482
+ validateBuildIncludesPluginHookRegistrations(agentPlugins, virtualConfig);
22762
23483
  validateAgentPlugins(agentPlugins);
22763
23484
  const shouldValidatePluginCatalog = hasConfiguredPluginCatalog(pluginConfig) || Boolean(configuredPlugins?.registrations.length) || Boolean(Object.keys(options?.configDefaults ?? {}).length);
22764
23485
  const previousPluginCatalogConfig = setPluginCatalogConfig(pluginConfig);
@@ -22774,6 +23495,9 @@ async function createApp(options) {
22774
23495
  if (shouldValidatePluginCatalog) {
22775
23496
  getPluginCatalogSignature();
22776
23497
  validatePluginRegistrations(configuredPlugins?.registrations ?? []);
23498
+ validatePluginEgressCredentialHooks(
23499
+ configuredPlugins?.registrations ?? []
23500
+ );
22777
23501
  }
22778
23502
  agentPluginRoutes = getAgentPluginRoutes();
22779
23503
  } catch (error) {
@@ -22811,9 +23535,17 @@ async function createApp(options) {
22811
23535
  return POST(c.req.raw, waitUntil);
22812
23536
  });
22813
23537
  let agentContinuePOST;
23538
+ let conversationWorkOptions;
23539
+ const getConversationWorkOptions = () => {
23540
+ conversationWorkOptions ??= options?.conversationWork ?? getProductionConversationWorkOptions();
23541
+ return conversationWorkOptions;
23542
+ };
23543
+ if (process.env.NODE_ENV === "development") {
23544
+ registerVercelConversationWorkDevConsumer(getConversationWorkOptions());
23545
+ }
22814
23546
  app.post("/api/internal/agent/continue", (c) => {
22815
23547
  agentContinuePOST ??= createVercelConversationWorkCallback(
22816
- options?.conversationWork ?? getProductionConversationWorkOptions()
23548
+ getConversationWorkOptions()
22817
23549
  );
22818
23550
  return agentContinuePOST(c.req.raw);
22819
23551
  });