@sentry/junior 0.67.3 → 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 (66) hide show
  1. package/dist/app.js +1465 -814
  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/slack/footer.d.ts +2 -7
  37. package/dist/chat/state/conversation.d.ts +1 -0
  38. package/dist/chat/state/turn-session.d.ts +4 -0
  39. package/dist/chat/task-execution/queue.d.ts +2 -0
  40. package/dist/chat/task-execution/store.d.ts +5 -0
  41. package/dist/chat/task-execution/vercel-callback.d.ts +4 -0
  42. package/dist/chat/task-execution/vercel-queue.d.ts +2 -0
  43. package/dist/chat/task-execution/worker.d.ts +4 -2
  44. package/dist/chat/tools/slack/context.d.ts +3 -0
  45. package/dist/chat/tools/types.d.ts +21 -2
  46. package/dist/chunk-76YMBKW7.js +326 -0
  47. package/dist/{chunk-PIVOJIUD.js → chunk-B5HKWWQB.js} +9 -5
  48. package/dist/chunk-BBXYXOJW.js +1858 -0
  49. package/dist/{chunk-V47RLIO2.js → chunk-GT67ZWZQ.js} +4 -4
  50. package/dist/{chunk-75UZ4JLC.js → chunk-IGLNC5H6.js} +21 -9
  51. package/dist/{chunk-EBVQXCD2.js → chunk-JS4HURDT.js} +362 -280
  52. package/dist/{chunk-UQQSW7QB.js → chunk-N3MORKTH.js} +74 -331
  53. package/dist/chunk-R62YWUNO.js +264 -0
  54. package/dist/{chunk-OIIXZOOC.js → chunk-UXG6TU2U.js} +311 -2015
  55. package/dist/cli/check.js +4 -4
  56. package/dist/cli/snapshot-warmup.js +5 -4
  57. package/dist/nitro.d.ts +1 -1
  58. package/dist/nitro.js +21 -19
  59. package/dist/plugins.d.ts +2 -2
  60. package/dist/reporting.d.ts +2 -2
  61. package/dist/reporting.js +13 -11
  62. package/package.json +6 -4
  63. package/dist/chat/plugins/auth/github-app-broker.d.ts +0 -4
  64. package/dist/chat/plugins/github-permissions.d.ts +0 -11
  65. package/dist/chat/queue/thread-message-dispatcher.d.ts +0 -33
  66. 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
  }
@@ -12762,51 +12929,6 @@ function escapeSlackMrkdwn(text) {
12762
12929
  function escapeSlackLinkUrl(url) {
12763
12930
  return url.replaceAll("&", "&amp;").replaceAll("<", "%3C").replaceAll(">", "%3E");
12764
12931
  }
12765
- function formatSlackTokenCount(value) {
12766
- if (value >= 1e6) {
12767
- const millions = value / 1e6;
12768
- return `${parseFloat(millions.toFixed(2))}m`;
12769
- }
12770
- if (value >= 1e3) {
12771
- const thousands = value / 1e3;
12772
- return `${parseFloat(thousands.toFixed(1))}k`;
12773
- }
12774
- return `${value}`;
12775
- }
12776
- function formatSlackDuration(durationMs) {
12777
- if (durationMs < 1e3) {
12778
- return `${durationMs}ms`;
12779
- }
12780
- const totalSeconds = Math.round(durationMs / 1e3);
12781
- if (totalSeconds < 10) {
12782
- const precise = durationMs / 1e3;
12783
- return `${precise.toFixed(1).replace(/\.0$/, "")}s`;
12784
- }
12785
- if (totalSeconds < 60) {
12786
- return `${totalSeconds}s`;
12787
- }
12788
- const minutes = Math.floor(totalSeconds / 60);
12789
- const seconds = totalSeconds % 60;
12790
- if (seconds === 0) {
12791
- return `${minutes}m`;
12792
- }
12793
- return `${minutes}m${seconds}s`;
12794
- }
12795
- function resolveTotalTokens(usage) {
12796
- if (!usage) {
12797
- return void 0;
12798
- }
12799
- const components = [
12800
- usage.inputTokens,
12801
- usage.outputTokens,
12802
- usage.cachedInputTokens,
12803
- usage.cacheCreationTokens
12804
- ].filter((value) => value !== void 0);
12805
- if (components.length > 0) {
12806
- return components.reduce((sum, value) => sum + value, 0);
12807
- }
12808
- return usage.totalTokens;
12809
- }
12810
12932
  function buildSlackReplyFooter(args) {
12811
12933
  const items = [];
12812
12934
  const conversationId = args.conversationId?.trim();
@@ -12821,26 +12943,6 @@ function buildSlackReplyFooter(args) {
12821
12943
  }
12822
12944
  items.push(idItem);
12823
12945
  }
12824
- const totalTokens = resolveTotalTokens(args.usage);
12825
- if (totalTokens !== void 0) {
12826
- items.push({
12827
- label: "Tokens",
12828
- value: formatSlackTokenCount(totalTokens)
12829
- });
12830
- }
12831
- if (typeof args.durationMs === "number" && Number.isFinite(args.durationMs)) {
12832
- const durationMs = Math.max(0, Math.floor(args.durationMs));
12833
- items.push({
12834
- label: "Time",
12835
- value: formatSlackDuration(durationMs)
12836
- });
12837
- }
12838
- if (args.thinkingLevel) {
12839
- items.push({
12840
- label: "Thinking",
12841
- value: args.thinkingLevel
12842
- });
12843
- }
12844
12946
  return items.length > 0 ? { items } : void 0;
12845
12947
  }
12846
12948
  function buildSlackReplyBlocks(text, footer) {
@@ -13201,11 +13303,80 @@ async function verifyDispatchCallbackRequest(request) {
13201
13303
 
13202
13304
  // src/chat/agent-dispatch/store.ts
13203
13305
  import { createHash } from "crypto";
13306
+ import {
13307
+ agentPluginCredentialSubjectSchema,
13308
+ destinationSchema
13309
+ } from "@sentry/junior-plugin-api";
13310
+ import { z as z3 } from "zod";
13204
13311
  var DISPATCH_PREFIX = "junior:agent_dispatch";
13205
13312
  var DISPATCH_LOCK_TTL_MS = 10 * 60 * 1e3;
13206
13313
  var DISPATCH_INDEX_LOCK_TTL_MS = 1e4;
13207
13314
  var DISPATCH_INDEX_MAX_LENGTH = 1e4;
13208
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
+ });
13209
13380
  function getDispatchStorageKey(id) {
13210
13381
  return `${DISPATCH_PREFIX}:record:${id}`;
13211
13382
  }
@@ -13231,8 +13402,12 @@ function buildDispatchId(plugin, idempotencyKey) {
13231
13402
  const digest = createHash("sha256").update(plugin).update("\0").update(idempotencyKey).digest("hex").slice(0, 32);
13232
13403
  return `dispatch_${digest}`;
13233
13404
  }
13405
+ function parseDispatchRecord(value) {
13406
+ const parsed = dispatchRecordSchema.safeParse(value);
13407
+ return parsed.success ? parsed.data : void 0;
13408
+ }
13234
13409
  function getDispatchDestinationLockId(destination) {
13235
- return `slack:${destination.teamId}:${destination.channelId}`;
13410
+ return destinationKey(destination);
13236
13411
  }
13237
13412
  function getDispatchConversationId(dispatch) {
13238
13413
  return `agent-dispatch:${dispatch.id}`;
@@ -13299,22 +13474,28 @@ async function syncIncompleteDispatchIndex(state, record) {
13299
13474
  });
13300
13475
  }
13301
13476
  async function putRecord(state, record) {
13477
+ const next = parseDispatchRecord(record);
13478
+ if (!next) {
13479
+ throw new Error("Dispatch record is invalid.");
13480
+ }
13302
13481
  await state.set(
13303
- getDispatchStorageKey(record.id),
13304
- record,
13482
+ getDispatchStorageKey(next.id),
13483
+ next,
13305
13484
  JUNIOR_THREAD_STATE_TTL_MS
13306
13485
  );
13307
- await syncIncompleteDispatchIndex(state, record);
13486
+ await syncIncompleteDispatchIndex(state, next);
13308
13487
  }
13309
13488
  async function getDispatchRecord(id) {
13310
13489
  const state = getStateAdapter();
13311
13490
  await state.connect();
13312
- return await state.get(getDispatchStorageKey(id)) ?? void 0;
13491
+ return parseDispatchRecord(await state.get(getDispatchStorageKey(id)));
13313
13492
  }
13314
13493
  async function createOrGetDispatch(args) {
13315
13494
  const id = buildDispatchId(args.plugin, args.options.idempotencyKey);
13316
13495
  return await withDispatchLock(id, async (state) => {
13317
- const existing = await state.get(getDispatchStorageKey(id)) ?? void 0;
13496
+ const existing = parseDispatchRecord(
13497
+ await state.get(getDispatchStorageKey(id))
13498
+ );
13318
13499
  if (existing) {
13319
13500
  return { record: existing, status: "already_exists" };
13320
13501
  }
@@ -13409,9 +13590,12 @@ async function persistRuntimePatch(args) {
13409
13590
  }
13410
13591
  async function markDispatch(args) {
13411
13592
  return await withDispatchLock(args.dispatch.id, async (state) => {
13412
- const current = await state.get(
13413
- getDispatchStorageKey(args.dispatch.id)
13414
- ) ?? 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
+ }
13415
13599
  return await updateDispatchRecord(state, {
13416
13600
  ...current,
13417
13601
  status: args.status,
@@ -13437,7 +13621,9 @@ async function runAgentDispatchSlice(callback, deps = {}) {
13437
13621
  const scheduleCallback = deps.scheduleCallback ?? scheduleDispatchCallback;
13438
13622
  const nowMs = Date.now();
13439
13623
  const claimedDispatch = await withDispatchLock(callback.id, async (state) => {
13440
- const current = await state.get(getDispatchStorageKey(callback.id)) ?? void 0;
13624
+ const current = parseDispatchRecord(
13625
+ await state.get(getDispatchStorageKey(callback.id))
13626
+ );
13441
13627
  if (!current || !canClaimDispatch(current, nowMs) || current.version !== callback.expectedVersion) {
13442
13628
  return void 0;
13443
13629
  }
@@ -13472,10 +13658,10 @@ async function runAgentDispatchSlice(callback, deps = {}) {
13472
13658
  const startedDispatch = await withDispatchLock(
13473
13659
  dispatch.id,
13474
13660
  async (state) => {
13475
- const current = await state.get(
13476
- getDispatchStorageKey(dispatch.id)
13477
- ) ?? dispatch;
13478
- 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) {
13479
13665
  return void 0;
13480
13666
  }
13481
13667
  return await updateDispatchRecord(state, {
@@ -13527,6 +13713,7 @@ async function runAgentDispatchSlice(callback, deps = {}) {
13527
13713
  conversationContext,
13528
13714
  artifactState: artifacts,
13529
13715
  piMessages: conversation.piMessages,
13716
+ destination: dispatch.destination,
13530
13717
  correlation: {
13531
13718
  conversationId,
13532
13719
  threadId: conversationId,
@@ -13585,10 +13772,7 @@ async function runAgentDispatchSlice(callback, deps = {}) {
13585
13772
  channelId: dispatch.destination.channelId,
13586
13773
  posts: planSlackReplyPosts({ reply: deliveryReply }),
13587
13774
  footer: buildSlackReplyFooter({
13588
- conversationId,
13589
- durationMs: deliveryReply.diagnostics.durationMs,
13590
- thinkingLevel: deliveryReply.diagnostics.thinkingLevel,
13591
- usage: deliveryReply.diagnostics.usage
13775
+ conversationId
13592
13776
  }),
13593
13777
  fileUploadFailureMode: "strict"
13594
13778
  });
@@ -13797,14 +13981,16 @@ function normalizeMessage(value) {
13797
13981
  const conversationId = toOptionalString(value.conversationId);
13798
13982
  const inboundMessageId = toOptionalString(value.inboundMessageId);
13799
13983
  const source = normalizeSource(value.source);
13984
+ const destination = parseDestination(value.destination);
13800
13985
  const createdAtMs = toOptionalNumber(value.createdAtMs);
13801
13986
  const receivedAtMs = toOptionalNumber(value.receivedAtMs);
13802
13987
  const input = normalizeInput(value.input);
13803
- if (!conversationId || !inboundMessageId || !source || typeof createdAtMs !== "number" || typeof receivedAtMs !== "number" || !input) {
13988
+ if (!conversationId || !destination || !inboundMessageId || !source || typeof createdAtMs !== "number" || typeof receivedAtMs !== "number" || !input) {
13804
13989
  return void 0;
13805
13990
  }
13806
13991
  return {
13807
13992
  conversationId,
13993
+ destination,
13808
13994
  inboundMessageId,
13809
13995
  source,
13810
13996
  createdAtMs,
@@ -13836,14 +14022,16 @@ function normalizeWorkState(conversationId, value) {
13836
14022
  return void 0;
13837
14023
  }
13838
14024
  const storedConversationId = toOptionalString(value.conversationId);
14025
+ const destination = parseDestination(value.destination);
13839
14026
  const updatedAtMs = toOptionalNumber(value.updatedAtMs);
13840
- if (storedConversationId !== conversationId || typeof updatedAtMs !== "number") {
14027
+ if (storedConversationId !== conversationId || !destination || typeof updatedAtMs !== "number") {
13841
14028
  return void 0;
13842
14029
  }
13843
14030
  const messages = Array.isArray(value.messages) ? value.messages.map(normalizeMessage).filter((message) => Boolean(message)).filter((message) => message.conversationId === conversationId).sort(compareMessages) : [];
13844
14031
  return {
13845
14032
  schemaVersion: CONVERSATION_WORK_SCHEMA_VERSION,
13846
14033
  conversationId,
14034
+ destination,
13847
14035
  messages,
13848
14036
  needsRun: value.needsRun === true,
13849
14037
  updatedAtMs,
@@ -13855,6 +14043,7 @@ function emptyWorkState(args) {
13855
14043
  return {
13856
14044
  schemaVersion: CONVERSATION_WORK_SCHEMA_VERSION,
13857
14045
  conversationId: args.conversationId,
14046
+ destination: args.destination,
13858
14047
  messages: [],
13859
14048
  needsRun: false,
13860
14049
  updatedAtMs: args.nowMs
@@ -13968,10 +14157,15 @@ async function withConversationMutation(args, callback) {
13968
14157
  }
13969
14158
  }
13970
14159
  async function readWorkState(state, conversationId) {
13971
- return normalizeWorkState(
13972
- conversationId,
13973
- await state.get(stateKey(conversationId))
13974
- );
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;
13975
14169
  }
13976
14170
  async function writeWorkState(state, work) {
13977
14171
  await state.set(
@@ -13988,6 +14182,14 @@ async function writeWorkState(state, work) {
13988
14182
  function hasRunnableWork(state) {
13989
14183
  return state.needsRun || pendingMessages(state).length > 0;
13990
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
+ }
13991
14193
  async function getConversationWorkState(args) {
13992
14194
  const state = await getConnectedState(args.state);
13993
14195
  return await readWorkState(state, args.conversationId);
@@ -14005,8 +14207,14 @@ async function appendInboundMessage(args) {
14005
14207
  async (state) => {
14006
14208
  const current = await readWorkState(state, args.message.conversationId) ?? emptyWorkState({
14007
14209
  conversationId: args.message.conversationId,
14210
+ destination: args.message.destination,
14008
14211
  nowMs
14009
14212
  });
14213
+ assertSameConversationDestination({
14214
+ conversationId: args.message.conversationId,
14215
+ current: current.destination,
14216
+ next: args.message.destination
14217
+ });
14010
14218
  const existing = current.messages.find(
14011
14219
  (message) => message.inboundMessageId === args.message.inboundMessageId
14012
14220
  );
@@ -14049,7 +14257,10 @@ async function appendAndEnqueueInboundMessage(args) {
14049
14257
  idempotencyKey = duplicateInboundNudgeIdempotencyKey(args.message, nowMs);
14050
14258
  }
14051
14259
  const queueResult = await args.queue.send(
14052
- { conversationId: args.message.conversationId },
14260
+ {
14261
+ conversationId: args.message.conversationId,
14262
+ destination: args.message.destination
14263
+ },
14053
14264
  { idempotencyKey }
14054
14265
  );
14055
14266
  await markConversationWorkEnqueued({
@@ -14066,8 +14277,16 @@ async function requestConversationWork(args) {
14066
14277
  const nowMs = args.nowMs ?? now();
14067
14278
  return await withConversationMutation(args, async (state) => {
14068
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
+ }
14069
14287
  const current = existing ?? emptyWorkState({
14070
14288
  conversationId: args.conversationId,
14289
+ destination: args.destination,
14071
14290
  nowMs
14072
14291
  });
14073
14292
  await writeWorkState(state, {
@@ -14223,6 +14442,11 @@ async function requestConversationContinuation(args) {
14223
14442
  if (!current || current.lease?.leaseToken !== args.leaseToken) {
14224
14443
  return false;
14225
14444
  }
14445
+ assertSameConversationDestination({
14446
+ conversationId: args.conversationId,
14447
+ current: current.destination,
14448
+ next: args.destination
14449
+ });
14226
14450
  await writeWorkState(state, {
14227
14451
  ...current,
14228
14452
  needsRun: true,
@@ -14300,8 +14524,12 @@ async function getAwaitingTurnContinuationRequest(args) {
14300
14524
  if (!sessionRecord || sessionRecord.state !== "awaiting_resume" || sessionRecord.resumeReason !== "timeout" && sessionRecord.resumeReason !== "yield" || sessionRecord.resumeReason === "timeout" && sessionRecord.sliceId < 2) {
14301
14525
  return void 0;
14302
14526
  }
14527
+ if (!sessionRecord.destination) {
14528
+ return void 0;
14529
+ }
14303
14530
  return {
14304
14531
  conversationId: args.conversationId,
14532
+ destination: sessionRecord.destination,
14305
14533
  sessionId: args.sessionId,
14306
14534
  expectedVersion: sessionRecord.version
14307
14535
  };
@@ -14329,12 +14557,17 @@ function parseTurnTimeoutResumeRequest(value) {
14329
14557
  return void 0;
14330
14558
  }
14331
14559
  const record = value;
14332
- const expectedVersion = typeof record.expectedVersion === "number" ? record.expectedVersion : record.expectedCheckpointVersion;
14333
- 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) {
14334
14566
  return void 0;
14335
14567
  }
14336
14568
  return {
14337
14569
  conversationId: record.conversationId,
14570
+ destination,
14338
14571
  sessionId: record.sessionId,
14339
14572
  expectedVersion
14340
14573
  };
@@ -14343,12 +14576,16 @@ async function scheduleTurnTimeoutResume(request, options = {}) {
14343
14576
  const nowMs = options.nowMs ?? Date.now();
14344
14577
  await requestConversationWork({
14345
14578
  conversationId: request.conversationId,
14579
+ destination: request.destination,
14346
14580
  nowMs,
14347
14581
  state: options.state
14348
14582
  });
14349
14583
  const queue = options.queue ?? getVercelConversationWorkQueue();
14350
14584
  await queue.send(
14351
- { conversationId: request.conversationId },
14585
+ {
14586
+ conversationId: request.conversationId,
14587
+ destination: request.destination
14588
+ },
14352
14589
  {
14353
14590
  idempotencyKey: [
14354
14591
  "timeout",
@@ -14394,7 +14631,10 @@ function heartbeatIdempotencyKey(reason, conversationId, nowMs) {
14394
14631
  }
14395
14632
  async function sendRecoveryNudge(args) {
14396
14633
  await args.queue.send(
14397
- { conversationId: args.conversationId },
14634
+ {
14635
+ conversationId: args.conversationId,
14636
+ destination: args.destination
14637
+ },
14398
14638
  { idempotencyKey: args.idempotencyKey }
14399
14639
  );
14400
14640
  await markConversationWorkEnqueued({
@@ -14432,6 +14672,7 @@ async function recoverConversationWork(args) {
14432
14672
  }
14433
14673
  await sendRecoveryNudge({
14434
14674
  conversationId,
14675
+ destination: work.destination,
14435
14676
  idempotencyKey: heartbeatIdempotencyKey(
14436
14677
  "lease",
14437
14678
  conversationId,
@@ -14458,6 +14699,7 @@ async function recoverConversationWork(args) {
14458
14699
  }
14459
14700
  await sendRecoveryNudge({
14460
14701
  conversationId,
14702
+ destination: work.destination,
14461
14703
  idempotencyKey: heartbeatIdempotencyKey(
14462
14704
  "pending",
14463
14705
  conversationId,
@@ -14487,78 +14729,92 @@ async function recoverConversationWork(args) {
14487
14729
  return result;
14488
14730
  }
14489
14731
 
14490
- // src/chat/slack/ids.ts
14491
- function isSlackTeamId(value) {
14492
- 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
+ );
14493
14740
  }
14494
- function isSlackConversationId(value) {
14495
- 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
+ );
14496
14745
  }
14497
-
14498
- // src/chat/agent-dispatch/validation.ts
14499
- var MAX_DISPATCH_INPUT_LENGTH = 32e3;
14500
- var MAX_IDEMPOTENCY_KEY_LENGTH = 512;
14501
- var MAX_METADATA_KEYS = 20;
14502
- var MAX_METADATA_KEY_LENGTH = 128;
14503
- var MAX_METADATA_VALUE_LENGTH = 512;
14504
- function validateDispatchOptions(options) {
14505
- if (!options.idempotencyKey.trim()) {
14506
- 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";
14752
+ }
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";
14507
14762
  }
14508
- if (options.idempotencyKey.length > MAX_IDEMPOTENCY_KEY_LENGTH) {
14509
- throw new Error("Dispatch idempotencyKey exceeds the maximum length");
14763
+ if (hasIssueAtPath(issues, ["destination"])) {
14764
+ return "Dispatch destination platform must be slack";
14510
14765
  }
14511
- if (options.destination.platform !== "slack") {
14512
- throw new Error("Dispatch destination platform must be slack");
14766
+ if (hasIssueUnderPath(issues, ["destination", "teamId"])) {
14767
+ return "Dispatch destination teamId must be a Slack team id";
14513
14768
  }
14514
- if (!isSlackTeamId(options.destination.teamId)) {
14515
- throw new Error("Dispatch destination teamId must be a Slack team id");
14769
+ if (hasIssueUnderPath(issues, ["destination", "channelId"])) {
14770
+ return "Dispatch destination channelId must be a Slack channel id";
14516
14771
  }
14517
- if (!isSlackConversationId(options.destination.channelId)) {
14518
- throw new Error(
14519
- "Dispatch destination channelId must be a Slack channel id"
14772
+ if (hasIssueUnderPath(issues, ["idempotencyKey"])) {
14773
+ const tooLong = issues.some(
14774
+ (issue) => issue.path[0] === "idempotencyKey" && issue.code === "too_big"
14520
14775
  );
14776
+ return tooLong ? "Dispatch idempotencyKey exceeds the maximum length" : "Dispatch idempotencyKey is required";
14521
14777
  }
14522
- if (!options.input.trim()) {
14523
- 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";
14524
14783
  }
14525
- if (options.input.length > MAX_DISPATCH_INPUT_LENGTH) {
14526
- throw new Error("Dispatch input exceeds the maximum length");
14784
+ if (hasIssueUnderPath(issues, ["credentialSubject", "userId"])) {
14785
+ return "Dispatch credentialSubject userId is required";
14527
14786
  }
14528
- if (options.credentialSubject) {
14529
- if (options.credentialSubject.type !== "user") {
14530
- throw new Error("Dispatch credentialSubject type must be user");
14531
- }
14532
- if (!isActorUserId(options.credentialSubject.userId)) {
14533
- throw new Error("Dispatch credentialSubject userId is required");
14534
- }
14535
- if (options.credentialSubject.allowedWhen !== "private-direct-conversation") {
14536
- throw new Error(
14537
- "Dispatch credentialSubject allowedWhen must be private-direct-conversation"
14538
- );
14539
- }
14540
- 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)) {
14541
14813
  throw new Error(
14542
14814
  "Dispatch credentialSubject requires a private direct Slack destination"
14543
14815
  );
14544
14816
  }
14545
14817
  }
14546
- const metadata = options.metadata ?? {};
14547
- const entries = Object.entries(metadata);
14548
- if (entries.length > MAX_METADATA_KEYS) {
14549
- throw new Error("Dispatch metadata has too many keys");
14550
- }
14551
- for (const [key, value] of entries) {
14552
- if (!key.trim() || typeof value !== "string") {
14553
- throw new Error("Dispatch metadata values must be strings");
14554
- }
14555
- if (key.length > MAX_METADATA_KEY_LENGTH) {
14556
- throw new Error("Dispatch metadata key exceeds the maximum length");
14557
- }
14558
- if (value.length > MAX_METADATA_VALUE_LENGTH) {
14559
- throw new Error("Dispatch metadata value exceeds the maximum length");
14560
- }
14561
- }
14562
14818
  }
14563
14819
  async function verifyDispatchCredentialSubjectAccess(options) {
14564
14820
  if (!options.credentialSubject) {
@@ -14672,8 +14928,8 @@ function isStaleDispatch(args) {
14672
14928
  }
14673
14929
  async function failDispatch(args) {
14674
14930
  await withDispatchLock(args.record.id, async (state) => {
14675
- const current = await state.get(
14676
- getDispatchStorageKey(args.record.id)
14931
+ const current = parseDispatchRecord(
14932
+ await state.get(getDispatchStorageKey(args.record.id))
14677
14933
  ) ?? args.record;
14678
14934
  if (isTerminalDispatchStatus(current.status)) {
14679
14935
  return;
@@ -14805,7 +15061,7 @@ async function recoverStaleDispatches(args) {
14805
15061
  }
14806
15062
  return recovered;
14807
15063
  }
14808
- async function runTrustedPluginHeartbeats(args) {
15064
+ async function runPluginHeartbeats(args) {
14809
15065
  let count = 0;
14810
15066
  for (const plugin of getAgentPlugins()) {
14811
15067
  if (count >= (args.limit ?? DEFAULT_PLUGIN_LIMIT)) {
@@ -14831,7 +15087,7 @@ async function runTrustedPluginHeartbeats(args) {
14831
15087
  );
14832
15088
  if (typeof result?.dispatchCount === "number" && result.dispatchCount > 0) {
14833
15089
  logInfo(
14834
- "trusted_plugin_heartbeat_dispatched",
15090
+ "plugin_heartbeat_dispatched",
14835
15091
  {},
14836
15092
  {
14837
15093
  "app.dispatch.count": result.dispatchCount,
@@ -14843,10 +15099,10 @@ async function runTrustedPluginHeartbeats(args) {
14843
15099
  } catch (error) {
14844
15100
  logException(
14845
15101
  error,
14846
- "trusted_plugin_heartbeat_failed",
15102
+ "plugin_heartbeat_failed",
14847
15103
  {},
14848
15104
  { "app.plugin.name": plugin.name },
14849
- "Trusted plugin heartbeat failed"
15105
+ "Plugin heartbeat failed"
14850
15106
  );
14851
15107
  }
14852
15108
  }
@@ -14861,7 +15117,7 @@ async function runHeartbeat(args) {
14861
15117
  nowMs: args.nowMs
14862
15118
  });
14863
15119
  await recoverStaleDispatches({ nowMs: args.nowMs });
14864
- await runTrustedPluginHeartbeats({ nowMs: args.nowMs });
15120
+ await runPluginHeartbeats({ nowMs: args.nowMs });
14865
15121
  }
14866
15122
 
14867
15123
  // src/handlers/heartbeat.ts
@@ -15393,10 +15649,6 @@ function getWorkspaceTeamId() {
15393
15649
  }
15394
15650
 
15395
15651
  // src/chat/runtime/thread-context.ts
15396
- function toSlackTeamId(value) {
15397
- const candidate = toOptionalString(value);
15398
- return candidate && isSlackTeamId(candidate) ? candidate : void 0;
15399
- }
15400
15652
  function escapeRegExp2(value) {
15401
15653
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
15402
15654
  }
@@ -15472,14 +15724,6 @@ function getMessageTs(message) {
15472
15724
  const rawRecord = raw;
15473
15725
  return toOptionalString(rawRecord.ts) ?? toOptionalString(rawRecord.event_ts) ?? toOptionalString(rawRecord.message?.ts);
15474
15726
  }
15475
- function getTeamId(message) {
15476
- const raw = message.raw;
15477
- if (!raw || typeof raw !== "object") {
15478
- return void 0;
15479
- }
15480
- const rawRecord = raw;
15481
- return toSlackTeamId(rawRecord.team_id) ?? toSlackTeamId(rawRecord.team) ?? toSlackTeamId(getWorkspaceTeamId()) ?? toSlackTeamId(rawRecord.user_team);
15482
- }
15483
15727
 
15484
15728
  // src/chat/runtime/processing-reaction.ts
15485
15729
  var noProcessingReaction = {
@@ -15821,10 +16065,6 @@ async function resumeSlackTurn(args) {
15821
16065
  status.start();
15822
16066
  const generateReply = runArgs.generateReply ?? generateAssistantReply;
15823
16067
  const replyContext = createResumeReplyContext(runArgs, status);
15824
- const priorSessionRecord = replyContext.correlation?.conversationId && replyContext.correlation?.turnId ? await getAgentTurnSessionRecord(
15825
- replyContext.correlation.conversationId,
15826
- replyContext.correlation.turnId
15827
- ) : void 0;
15828
16068
  const replyPromise = generateReply(runArgs.messageText, replyContext);
15829
16069
  const replyTimeoutMs = resolveReplyTimeoutMs(runArgs.replyTimeoutMs);
15830
16070
  let reply = typeof replyTimeoutMs === "number" ? await Promise.race([
@@ -15847,13 +16087,7 @@ async function resumeSlackTurn(args) {
15847
16087
  });
15848
16088
  await status.stop();
15849
16089
  const footer = buildSlackReplyFooter({
15850
- conversationId: runArgs.replyContext?.correlation?.conversationId ?? lockKey,
15851
- durationMs: typeof priorSessionRecord?.cumulativeDurationMs === "number" || typeof reply.diagnostics.durationMs === "number" ? (priorSessionRecord?.cumulativeDurationMs ?? 0) + (reply.diagnostics.durationMs ?? 0) : void 0,
15852
- thinkingLevel: reply.diagnostics.thinkingLevel,
15853
- usage: addAgentTurnUsage(
15854
- priorSessionRecord?.cumulativeUsage,
15855
- reply.diagnostics.usage
15856
- ) ?? reply.diagnostics.usage
16090
+ conversationId: runArgs.replyContext?.correlation?.conversationId ?? lockKey
15857
16091
  });
15858
16092
  await postSlackApiReplyPosts({
15859
16093
  channelId: runArgs.channelId,
@@ -16192,9 +16426,10 @@ async function persistFailedReplyState(channelId, threadTs, sessionId, expectedV
16192
16426
  }
16193
16427
  async function resumeAuthorizedMcpTurn(args) {
16194
16428
  const { authSession, provider } = args;
16195
- if (!authSession.channelId || !authSession.threadTs) {
16429
+ if (!authSession.channelId || !authSession.destination || !authSession.threadTs) {
16196
16430
  return;
16197
16431
  }
16432
+ const destination = authSession.destination;
16198
16433
  const threadId = `slack:${authSession.channelId}:${authSession.threadTs}`;
16199
16434
  const currentState = await getPersistedThreadState(threadId);
16200
16435
  const conversation = coerceThreadConversationState(currentState);
@@ -16303,6 +16538,7 @@ async function resumeAuthorizedMcpTurn(args) {
16303
16538
  actor: { type: "user", userId: authSession.userId }
16304
16539
  },
16305
16540
  requester,
16541
+ destination,
16306
16542
  correlation: {
16307
16543
  conversationId: authSession.conversationId,
16308
16544
  turnId: lockedSessionId,
@@ -16391,6 +16627,7 @@ async function resumeAuthorizedMcpTurn(args) {
16391
16627
  }
16392
16628
  await scheduleTurnTimeoutResume({
16393
16629
  conversationId: authSession.conversationId,
16630
+ destination,
16394
16631
  sessionId: lockedSessionId,
16395
16632
  expectedVersion: version
16396
16633
  });
@@ -16488,13 +16725,23 @@ async function buildSkillsSummaryText() {
16488
16725
  }
16489
16726
  return lines.join("\n");
16490
16727
  }
16491
- async function hasConnectedAccount(userId, plugin, userTokenStore) {
16492
- 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) {
16493
16739
  const stored = await userTokenStore.get(userId, plugin.manifest.name);
16494
- return Boolean(
16495
- stored && hasRequiredOAuthScope(stored.scope, plugin.manifest.oauth?.scope)
16496
- );
16740
+ return stored && hasRequiredOAuthScope(stored.scope, plugin.manifest.oauth?.scope) ? stored : void 0;
16497
16741
  }
16742
+ return void 0;
16743
+ }
16744
+ async function hasConnectedMcpAccount(userId, plugin) {
16498
16745
  if (plugin.manifest.mcp) {
16499
16746
  return Boolean(
16500
16747
  (await getMcpStoredOAuthCredentials(userId, plugin.manifest.name))?.tokens
@@ -16509,13 +16756,13 @@ async function buildHomeView(userId, userTokenStore) {
16509
16756
  const providers = getPluginProviders();
16510
16757
  const connectedSections = [];
16511
16758
  for (const plugin of providers) {
16512
- if (!await hasConnectedAccount(userId, plugin, userTokenStore)) continue;
16759
+ const tokens = await connectedOAuthTokens(userId, plugin, userTokenStore);
16760
+ if (!tokens && !await hasConnectedMcpAccount(userId, plugin)) continue;
16513
16761
  connectedSections.push({
16514
16762
  type: "section",
16515
16763
  text: {
16516
16764
  type: "mrkdwn",
16517
- text: `*${plugin.manifest.name}*
16518
- ${plugin.manifest.description}`
16765
+ text: connectedAccountText(plugin, tokens?.account)
16519
16766
  },
16520
16767
  accessory: {
16521
16768
  type: "button",
@@ -16660,22 +16907,10 @@ async function persistFailedOAuthReplyState(args) {
16660
16907
  });
16661
16908
  }
16662
16909
  async function resumeOAuthSessionRecordTurn(stored) {
16663
- if (!stored.resumeConversationId || !stored.resumeSessionId || !stored.channelId || !stored.threadTs) {
16664
- return false;
16665
- }
16666
- const sessionRecord = await getAgentTurnSessionRecord(
16667
- stored.resumeConversationId,
16668
- stored.resumeSessionId
16669
- );
16670
- if (!sessionRecord) {
16910
+ if (!stored.resumeConversationId || !stored.resumeSessionId || !stored.channelId || !stored.destination || !stored.threadTs) {
16671
16911
  return false;
16672
16912
  }
16673
- if (sessionRecord.state === "completed" || sessionRecord.state === "failed" || sessionRecord.state === "abandoned") {
16674
- return true;
16675
- }
16676
- if (sessionRecord.state !== "awaiting_resume" || sessionRecord.resumeReason !== "auth") {
16677
- return true;
16678
- }
16913
+ const destination = stored.destination;
16679
16914
  const currentState = await getPersistedThreadState(
16680
16915
  stored.resumeConversationId
16681
16916
  );
@@ -16684,7 +16919,8 @@ async function resumeOAuthSessionRecordTurn(stored) {
16684
16919
  conversation,
16685
16920
  kind: "plugin",
16686
16921
  provider: stored.provider,
16687
- requesterId: stored.userId
16922
+ requesterId: stored.userId,
16923
+ ...stored.scope ? { scope: stored.scope } : {}
16688
16924
  });
16689
16925
  const resolvedSessionId = pendingAuth?.sessionId ?? stored.resumeSessionId;
16690
16926
  const userMessage2 = resolvedSessionId ? getTurnUserMessage(conversation, resolvedSessionId) : void 0;
@@ -16701,32 +16937,34 @@ async function resumeOAuthSessionRecordTurn(stored) {
16701
16937
  });
16702
16938
  return true;
16703
16939
  }
16704
- } else {
16705
- if (!userMessage2?.author?.userId) {
16706
- return false;
16707
- }
16708
- if (conversation.processing.activeTurnId !== stored.resumeSessionId) {
16709
- return true;
16710
- }
16711
16940
  }
16712
- if (!userMessage2?.author?.userId || !resolvedSessionId) {
16941
+ const sessionRecord = await getAgentTurnSessionRecord(
16942
+ stored.resumeConversationId,
16943
+ resolvedSessionId
16944
+ );
16945
+ if (!sessionRecord) {
16713
16946
  return false;
16714
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) {
16955
+ return false;
16956
+ }
16957
+ if (!pendingAuth && conversation.processing.activeTurnId !== stored.resumeSessionId) {
16958
+ return true;
16959
+ }
16715
16960
  await resumeSlackTurn({
16716
- messageText: stored.pendingMessage ?? userMessage2.text,
16961
+ messageText: pendingAuth ? userMessage2.text : stored.pendingMessage ?? userMessage2.text,
16717
16962
  channelId: stored.channelId,
16718
16963
  threadTs: stored.threadTs,
16719
16964
  messageTs: getTurnUserSlackMessageTs(userMessage2),
16720
16965
  lockKey: stored.resumeConversationId,
16721
16966
  initialText: "",
16722
16967
  beforeStart: async () => {
16723
- const lockedSessionRecord = await getAgentTurnSessionRecord(
16724
- stored.resumeConversationId,
16725
- stored.resumeSessionId
16726
- );
16727
- if (!lockedSessionRecord || lockedSessionRecord.state !== "awaiting_resume" || lockedSessionRecord.resumeReason !== "auth") {
16728
- return false;
16729
- }
16730
16968
  const lockedState = await getPersistedThreadState(
16731
16969
  stored.resumeConversationId
16732
16970
  );
@@ -16736,12 +16974,20 @@ async function resumeOAuthSessionRecordTurn(stored) {
16736
16974
  conversation: lockedConversation,
16737
16975
  kind: "plugin",
16738
16976
  provider: stored.provider,
16739
- requesterId: stored.userId
16977
+ requesterId: stored.userId,
16978
+ ...stored.scope ? { scope: stored.scope } : {}
16740
16979
  });
16741
16980
  const lockedSessionId = lockedPendingAuth?.sessionId ?? stored.resumeSessionId;
16742
16981
  if (lockedSessionId !== resolvedSessionId) {
16743
16982
  return false;
16744
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
+ }
16745
16991
  if (lockedPendingAuth) {
16746
16992
  if (!isPendingAuthLatestRequest(lockedConversation, lockedPendingAuth)) {
16747
16993
  clearPendingAuth(lockedConversation, lockedPendingAuth.sessionId);
@@ -16789,7 +17035,7 @@ async function resumeOAuthSessionRecordTurn(stored) {
16789
17035
  lockedUserMessage.author.userId
16790
17036
  );
16791
17037
  return {
16792
- messageText: stored.pendingMessage ?? lockedUserMessage.text,
17038
+ messageText: lockedPendingAuth ? lockedUserMessage.text : stored.pendingMessage ?? lockedUserMessage.text,
16793
17039
  messageTs: getTurnUserSlackMessageTs(lockedUserMessage),
16794
17040
  replyContext: {
16795
17041
  credentialContext: {
@@ -16799,6 +17045,7 @@ async function resumeOAuthSessionRecordTurn(stored) {
16799
17045
  }
16800
17046
  },
16801
17047
  requester,
17048
+ destination,
16802
17049
  correlation: {
16803
17050
  conversationId: stored.resumeConversationId,
16804
17051
  turnId: lockedSessionId,
@@ -16875,6 +17122,7 @@ async function resumeOAuthSessionRecordTurn(stored) {
16875
17122
  }
16876
17123
  await scheduleTurnTimeoutResume({
16877
17124
  conversationId: stored.resumeConversationId,
17125
+ destination,
16878
17126
  sessionId: lockedSessionId,
16879
17127
  expectedVersion: version
16880
17128
  });
@@ -16885,7 +17133,9 @@ async function resumeOAuthSessionRecordTurn(stored) {
16885
17133
  return true;
16886
17134
  }
16887
17135
  async function resumePendingOAuthMessage(stored) {
16888
- if (!stored.pendingMessage || !stored.channelId || !stored.threadTs) return;
17136
+ if (!stored.pendingMessage || !stored.channelId || !stored.destination || !stored.threadTs) {
17137
+ return;
17138
+ }
16889
17139
  const threadId = `slack:${stored.channelId}:${stored.threadTs}`;
16890
17140
  const conversation = coerceThreadConversationState(
16891
17141
  await getPersistedThreadState(threadId)
@@ -16906,6 +17156,13 @@ async function resumePendingOAuthMessage(stored) {
16906
17156
  actor: { type: "user", userId: stored.userId }
16907
17157
  },
16908
17158
  requester,
17159
+ destination: stored.destination,
17160
+ correlation: {
17161
+ conversationId: threadId,
17162
+ channelId: stored.channelId,
17163
+ threadTs: stored.threadTs,
17164
+ requesterId: stored.userId
17165
+ },
16909
17166
  conversationContext,
16910
17167
  piMessages: conversation.piMessages,
16911
17168
  configuration: stored.configuration
@@ -16965,7 +17222,7 @@ async function GET4(request, provider, waitUntil) {
16965
17222
  }
16966
17223
  const stateAdapter = getStateAdapter();
16967
17224
  const stateKey2 = `oauth-state:${state}`;
16968
- const stored = await stateAdapter.get(stateKey2);
17225
+ const stored = parseOAuthStatePayload(await stateAdapter.get(stateKey2));
16969
17226
  if (!stored) {
16970
17227
  return htmlErrorResponse(
16971
17228
  "Link expired",
@@ -16999,6 +17256,7 @@ async function GET4(request, provider, waitUntil) {
16999
17256
  );
17000
17257
  }
17001
17258
  const redirectUri = `${baseUrl}${providerConfig.callbackPath}`;
17259
+ const requestedScope = stored.scope ?? providerConfig.scope;
17002
17260
  let tokenResponse;
17003
17261
  try {
17004
17262
  const tokenRequest = buildOAuthTokenRequest({
@@ -17031,13 +17289,12 @@ async function GET4(request, provider, waitUntil) {
17031
17289
  500
17032
17290
  );
17033
17291
  }
17034
- const tokenData = await tokenResponse.json();
17035
17292
  let parsedTokenResponse;
17036
17293
  try {
17037
- parsedTokenResponse = parseOAuthTokenResponse(
17038
- tokenData,
17039
- providerConfig.scope
17040
- );
17294
+ const tokenData = await tokenResponse.json();
17295
+ parsedTokenResponse = parseOAuthTokenResponse(tokenData, requestedScope, {
17296
+ treatEmptyScopeAsUnreported: providerConfig.treatEmptyScopeAsUnreported
17297
+ });
17041
17298
  } catch {
17042
17299
  return htmlErrorResponse(
17043
17300
  "Connection failed",
@@ -17045,7 +17302,7 @@ async function GET4(request, provider, waitUntil) {
17045
17302
  500
17046
17303
  );
17047
17304
  }
17048
- if (!hasRequiredOAuthScope(parsedTokenResponse.scope, providerConfig.scope)) {
17305
+ if (!hasRequiredOAuthScope(parsedTokenResponse.scope, requestedScope)) {
17049
17306
  return htmlErrorResponse(
17050
17307
  "Connection failed",
17051
17308
  `The ${providerLabel} authorization did not grant the access Junior requires. Return to Slack and ask Junior to connect your ${providerLabel} account again.`,
@@ -17053,7 +17310,23 @@ async function GET4(request, provider, waitUntil) {
17053
17310
  );
17054
17311
  }
17055
17312
  const userTokenStore = createUserTokenStore();
17056
- 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
+ });
17057
17330
  waitUntil(async () => {
17058
17331
  try {
17059
17332
  await publishAppHomeView(getSlackClient(), stored.userId, userTokenStore);
@@ -17101,6 +17374,148 @@ async function GET4(request, provider, waitUntil) {
17101
17374
  });
17102
17375
  }
17103
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
+
17104
17519
  // src/chat/sandbox/egress-oidc.ts
17105
17520
  import {
17106
17521
  createRemoteJWKSet,
@@ -17204,6 +17619,19 @@ var UPSTREAM_PERMISSION_REJECTION_STATUS = 403;
17204
17619
  function jsonError(message, status) {
17205
17620
  return Response.json({ error: message }, { status });
17206
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
+ }
17207
17635
  function shouldLogSandboxEgressInfo() {
17208
17636
  const environment = (process.env.SENTRY_ENVIRONMENT ?? process.env.VERCEL_ENV ?? process.env.NODE_ENV ?? "").trim().toLowerCase();
17209
17637
  return environment !== "production";
@@ -17211,7 +17639,10 @@ function shouldLogSandboxEgressInfo() {
17211
17639
  function egressAttributes(input) {
17212
17640
  return {
17213
17641
  ...input.egressId ? { "app.sandbox.egress_id": input.egressId } : {},
17214
- ...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 } : {},
17215
17646
  ...input.host ? { "server.address": input.host } : {},
17216
17647
  ...input.method ? { "http.request.method": input.method } : {},
17217
17648
  ...input.path ? { "url.path": input.path } : {},
@@ -17247,9 +17678,42 @@ function routingAttributes(request, upstreamUrl) {
17247
17678
  };
17248
17679
  if (upstreamUrl) {
17249
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
+ }
17250
17685
  }
17251
17686
  return attributes;
17252
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
+ }
17253
17717
  function logSandboxEgressUpstreamRequest(input) {
17254
17718
  if (!shouldLogSandboxEgressInfo()) {
17255
17719
  return;
@@ -17260,6 +17724,9 @@ function logSandboxEgressUpstreamRequest(input) {
17260
17724
  {
17261
17725
  ...egressAttributes({
17262
17726
  egressId: input.egressId,
17727
+ grantAccess: input.grantAccess,
17728
+ grantName: input.grantName,
17729
+ grantReason: input.grantReason,
17263
17730
  host: input.upstreamUrl.hostname,
17264
17731
  method: input.request.method,
17265
17732
  path: input.upstreamUrl.pathname,
@@ -17269,7 +17736,7 @@ function logSandboxEgressUpstreamRequest(input) {
17269
17736
  ...routingAttributes(input.request, input.upstreamUrl),
17270
17737
  "app.sandbox.egress.upstream_ok": input.upstream.ok
17271
17738
  },
17272
- `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}`
17273
17740
  );
17274
17741
  }
17275
17742
  function normalizeHost(value) {
@@ -17384,35 +17851,6 @@ function responseHeaders(upstream) {
17384
17851
  });
17385
17852
  return headers;
17386
17853
  }
17387
- async function credentialLease(provider, context) {
17388
- const cached = await getSandboxEgressCredentialLease(provider, context);
17389
- if (cached) {
17390
- return cached;
17391
- }
17392
- const lease = await issueProviderCredentialLease({
17393
- context: context.credentials,
17394
- provider,
17395
- reason: `sandbox-egress:${provider}`
17396
- });
17397
- const headerTransforms = lease.headerTransforms ?? [];
17398
- if (headerTransforms.length === 0) {
17399
- throw new Error(
17400
- `Credential lease for ${provider} did not include header transforms`
17401
- );
17402
- }
17403
- const cachedLease = {
17404
- provider,
17405
- expiresAt: lease.expiresAt,
17406
- headerTransforms
17407
- };
17408
- await setSandboxEgressCredentialLease(context, cachedLease);
17409
- return cachedLease;
17410
- }
17411
- function hasTransformForHost(lease, host) {
17412
- return lease.headerTransforms.some(
17413
- (transform) => matchesSandboxEgressDomain(host, transform.domain)
17414
- );
17415
- }
17416
17854
  function isSandboxEgressForwardedRequest(request) {
17417
17855
  return Boolean(
17418
17856
  request.headers.get(OIDC_TOKEN_HEADER)?.trim() && request.headers.get(FORWARDED_HOST_HEADER)?.trim() && request.headers.get(FORWARDED_SCHEME_HEADER)?.trim()
@@ -17518,17 +17956,72 @@ async function proxySandboxEgressRequest(request, deps = {}) {
17518
17956
  403
17519
17957
  );
17520
17958
  }
17959
+ const grantSelection = await selectSandboxEgressGrant({
17960
+ provider,
17961
+ method: request.method,
17962
+ upstreamUrl
17963
+ });
17521
17964
  let lease;
17522
17965
  try {
17523
- lease = await credentialLease(provider, credentialContext);
17966
+ lease = await sandboxEgressCredentialLease(
17967
+ provider,
17968
+ grantSelection,
17969
+ credentialContext
17970
+ );
17524
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
+ }
17525
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
+ });
17526
18016
  logWarn(
17527
18017
  "sandbox_egress_credential_unavailable",
17528
18018
  {},
17529
18019
  {
17530
18020
  ...egressAttributes({
17531
18021
  egressId: activeEgressId,
18022
+ grantAccess: failedGrant.access,
18023
+ grantName: failedGrant.name,
18024
+ grantReason: failedGrant.reason,
17532
18025
  host: upstreamUrl.hostname,
17533
18026
  method: request.method,
17534
18027
  path: upstreamUrl.pathname,
@@ -17537,26 +18030,26 @@ async function proxySandboxEgressRequest(request, deps = {}) {
17537
18030
  }),
17538
18031
  ...routingAttributes(request, upstreamUrl)
17539
18032
  },
17540
- "Sandbox egress provider credential is unavailable"
17541
- );
17542
- return new Response(
17543
- `junior-auth-required provider=${error.provider} 401 unauthorized
17544
- ${error.message}`,
17545
- {
17546
- status: 401,
17547
- headers: { "content-type": "text/plain; charset=utf-8" }
17548
- }
18033
+ "Sandbox egress credential lease is unavailable for selected grant"
17549
18034
  );
18035
+ return authRequiredResponse({
18036
+ provider: error.provider,
18037
+ grant: failedGrant,
18038
+ message: error.message
18039
+ });
17550
18040
  }
17551
18041
  throw error;
17552
18042
  }
17553
- if (!hasTransformForHost(lease, upstreamUrl.hostname)) {
18043
+ if (!hasSandboxEgressLeaseTransformForHost(lease, upstreamUrl.hostname)) {
17554
18044
  logWarn(
17555
18045
  "sandbox_egress_transform_missing",
17556
18046
  {},
17557
18047
  {
17558
18048
  ...egressAttributes({
17559
18049
  egressId: activeEgressId,
18050
+ grantAccess: lease.grant.access,
18051
+ grantName: lease.grant.name,
18052
+ grantReason: lease.grant.reason,
17560
18053
  host: upstreamUrl.hostname,
17561
18054
  method: request.method,
17562
18055
  path: upstreamUrl.pathname,
@@ -17572,9 +18065,9 @@ ${error.message}`,
17572
18065
  );
17573
18066
  return jsonError("Credential lease does not cover forwarded host", 403);
17574
18067
  }
17575
- const body = await requestBodyBytes(request);
17576
18068
  const fetchImpl = deps.fetch ?? fetch;
17577
18069
  const headers = requestHeaders(request, lease, upstreamUrl.hostname);
18070
+ const body = await requestBodyBytes(request);
17578
18071
  const intercepted = await deps.interceptHttp?.({
17579
18072
  provider,
17580
18073
  request: new Request(upstreamUrl, {
@@ -17595,6 +18088,9 @@ ${error.message}`,
17595
18088
  });
17596
18089
  logSandboxEgressUpstreamRequest({
17597
18090
  egressId: activeEgressId,
18091
+ grantAccess: lease.grant.access,
18092
+ grantName: lease.grant.name,
18093
+ grantReason: lease.grant.reason,
17598
18094
  provider,
17599
18095
  request,
17600
18096
  upstream,
@@ -17607,6 +18103,9 @@ ${error.message}`,
17607
18103
  {
17608
18104
  ...egressAttributes({
17609
18105
  egressId: activeEgressId,
18106
+ grantAccess: lease.grant.access,
18107
+ grantName: lease.grant.name,
18108
+ grantReason: lease.grant.reason,
17610
18109
  host: upstreamUrl.hostname,
17611
18110
  method: request.method,
17612
18111
  path: upstreamUrl.pathname,
@@ -17614,6 +18113,7 @@ ${error.message}`,
17614
18113
  status: upstream.status
17615
18114
  }),
17616
18115
  ...routingAttributes(request, upstreamUrl),
18116
+ ...upstreamPermissionAttributes(provider, upstream),
17617
18117
  "error.type": `http_${upstream.status}`
17618
18118
  },
17619
18119
  `Sandbox egress upstream returned HTTP ${upstream.status}`
@@ -17626,6 +18126,9 @@ ${error.message}`,
17626
18126
  {
17627
18127
  ...egressAttributes({
17628
18128
  egressId: activeEgressId,
18129
+ grantAccess: lease.grant.access,
18130
+ grantName: lease.grant.name,
18131
+ grantReason: lease.grant.reason,
17629
18132
  host: upstreamUrl.hostname,
17630
18133
  method: request.method,
17631
18134
  path: upstreamUrl.pathname,
@@ -17633,27 +18136,49 @@ ${error.message}`,
17633
18136
  status: upstream.status
17634
18137
  }),
17635
18138
  ...routingAttributes(request, upstreamUrl),
18139
+ ...upstreamPermissionAttributes(provider, upstream),
17636
18140
  ...upstream.status === UPSTREAM_TOKEN_REJECTION_STATUS ? {
17637
18141
  "app.sandbox.egress.www_authenticate": upstream.headers.get("www-authenticate") ?? void 0
17638
18142
  } : {}
17639
18143
  },
17640
18144
  upstream.status === UPSTREAM_TOKEN_REJECTION_STATUS ? "Sandbox egress upstream auth rejected injected credential" : "Sandbox egress upstream permission denied"
17641
18145
  );
17642
- await clearSandboxEgressCredentialLease(provider, credentialContext);
17643
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
+ });
17644
18158
  await upstream.body?.cancel().catch(() => void 0);
17645
- return new Response(
17646
- `junior-auth-required provider=${provider} 401 unauthorized
17647
- Provider rejected the injected ${provider} credential.
17648
- `,
17649
- {
17650
- status: 401,
17651
- headers: {
17652
- "content-type": "text/plain; charset=utf-8",
17653
- "cache-control": "no-store"
17654
- }
17655
- }
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
17656
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
+ });
17657
18182
  }
17658
18183
  }
17659
18184
  return new Response(upstream.body, {
@@ -17798,6 +18323,7 @@ async function resumeTimedOutTurn(payload, options = {}) {
17798
18323
  }
17799
18324
  },
17800
18325
  requester,
18326
+ destination: payload.destination,
17801
18327
  correlation: {
17802
18328
  conversationId: payload.conversationId,
17803
18329
  turnId: payload.sessionId,
@@ -17865,6 +18391,7 @@ async function resumeTimedOutTurn(payload, options = {}) {
17865
18391
  }
17866
18392
  await scheduleTurnTimeoutResume2({
17867
18393
  conversationId: payload.conversationId,
18394
+ destination: payload.destination,
17868
18395
  sessionId: payload.sessionId,
17869
18396
  expectedVersion: version
17870
18397
  });
@@ -17942,14 +18469,14 @@ async function POST2(request, waitUntil, options = {}) {
17942
18469
  }
17943
18470
 
17944
18471
  // src/chat/services/subscribed-decision.ts
17945
- import { z as z2 } from "zod";
17946
- var replyDecisionSchema = z2.object({
17947
- should_reply: z2.boolean().describe("Whether Junior should respond to this thread message."),
17948
- 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(
17949
18476
  "Whether Junior should unsubscribe from this thread because the user clearly asked it to stop participating."
17950
18477
  ),
17951
- confidence: z2.number().min(0).max(1).describe("Classifier confidence from 0 to 1."),
17952
- 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.")
17953
18480
  });
17954
18481
  var ROUTER_CONFIDENCE_THRESHOLD = 0.8;
17955
18482
  var ROUTER_CLASSIFIER_MAX_TOKENS = 240;
@@ -18259,6 +18786,9 @@ async function decideSubscribedThreadReply(args) {
18259
18786
  reasonDetail: reason
18260
18787
  };
18261
18788
  } catch (error) {
18789
+ if (isProviderRetryError(error)) {
18790
+ throw error;
18791
+ }
18262
18792
  args.logClassifierFailure(error, args.input);
18263
18793
  return {
18264
18794
  shouldReply: false,
@@ -18343,6 +18873,9 @@ async function ensureSlackMessageActorIdentity(message, lookupSlackUser2) {
18343
18873
 
18344
18874
  // src/chat/runtime/slack-runtime.ts
18345
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
+ }
18346
18879
  async function maybeHandleThreadOptOutDecision(args) {
18347
18880
  if (!args.decision?.shouldUnsubscribe) {
18348
18881
  return false;
@@ -18372,7 +18905,7 @@ function getQueuedMessagesFromSlackMessages(messages, options) {
18372
18905
  );
18373
18906
  }
18374
18907
  function createSteeringMessageDrain(hooks, options) {
18375
- if (!hooks?.drainSteeringMessages) {
18908
+ if (!hooks.drainSteeringMessages) {
18376
18909
  return void 0;
18377
18910
  }
18378
18911
  return async (inject) => {
@@ -18410,7 +18943,7 @@ function createSlackTurnRuntime(deps) {
18410
18943
  if (shouldKeepProcessingReactionForToolInvocation(invocation)) {
18411
18944
  processingReaction.keep();
18412
18945
  }
18413
- hooks?.onToolInvocation?.(invocation);
18946
+ hooks.onToolInvocation?.(invocation);
18414
18947
  };
18415
18948
  };
18416
18949
  const stopProcessingReactions = async (processingReactions) => {
@@ -18528,7 +19061,7 @@ function createSlackTurnRuntime(deps) {
18528
19061
  );
18529
19062
  await deps.withSpan("chat.turn", "chat.turn", context, async () => {
18530
19063
  await thread.subscribe();
18531
- const queuedMessages = getQueuedMessages(hooks?.messageContext, {
19064
+ const queuedMessages = getQueuedMessages(hooks.messageContext, {
18532
19065
  explicitMention: true,
18533
19066
  stripLeadingBotMention: deps.stripLeadingBotMention
18534
19067
  });
@@ -18545,7 +19078,7 @@ function createSlackTurnRuntime(deps) {
18545
19078
  );
18546
19079
  };
18547
19080
  const onInputCommitted = async () => {
18548
- await hooks?.onInputCommitted?.();
19081
+ await hooks.onInputCommitted?.();
18549
19082
  await startQueuedProcessingReactions();
18550
19083
  };
18551
19084
  const drainSteeringMessages = createSteeringMessageDrain(hooks, {
@@ -18561,18 +19094,19 @@ function createSlackTurnRuntime(deps) {
18561
19094
  });
18562
19095
  await deps.replyToThread(thread, message, {
18563
19096
  explicitMention: true,
18564
- beforeFirstResponsePost: hooks?.beforeFirstResponsePost,
19097
+ beforeFirstResponsePost: hooks.beforeFirstResponsePost,
19098
+ destination: hooks.destination,
18565
19099
  queuedMessages,
18566
19100
  onInputCommitted,
18567
19101
  onToolInvocation: toolInvocationHook,
18568
19102
  onTurnCompleted,
18569
19103
  drainSteeringMessages,
18570
- onTurnStatePersisted: hooks?.onTurnStatePersisted,
18571
- shouldYield: hooks?.shouldYield
19104
+ onTurnStatePersisted: hooks.onTurnStatePersisted,
19105
+ shouldYield: hooks.shouldYield
18572
19106
  });
18573
19107
  });
18574
19108
  } catch (error) {
18575
- if (isCooperativeTurnYieldError(error) || isTurnInputCommitLostError(error)) {
19109
+ if (shouldRethrowTurnControlError(error)) {
18576
19110
  throw error;
18577
19111
  }
18578
19112
  const errorContext = logContext({
@@ -18604,7 +19138,7 @@ function createSlackTurnRuntime(deps) {
18604
19138
  "Sentry did not return an event ID for mention_handler_failed"
18605
19139
  );
18606
19140
  }
18607
- await hooks?.beforeFirstResponsePost?.();
19141
+ await hooks.beforeFirstResponsePost?.();
18608
19142
  await postFallbackErrorReplyWithLogging({
18609
19143
  thread,
18610
19144
  errorContext,
@@ -18658,7 +19192,7 @@ function createSlackTurnRuntime(deps) {
18658
19192
  channelId,
18659
19193
  runId
18660
19194
  };
18661
- const queuedMessages = getQueuedMessages(hooks?.messageContext, {
19195
+ const queuedMessages = getQueuedMessages(hooks.messageContext, {
18662
19196
  explicitMention: Boolean(message.isMention),
18663
19197
  stripLeadingBotMention: deps.stripLeadingBotMention
18664
19198
  });
@@ -18717,7 +19251,7 @@ function createSlackTurnRuntime(deps) {
18717
19251
  if (await maybeHandleThreadOptOutDecision({
18718
19252
  thread,
18719
19253
  decision,
18720
- beforeFirstResponsePost: hooks?.beforeFirstResponsePost
19254
+ beforeFirstResponsePost: hooks.beforeFirstResponsePost
18721
19255
  })) {
18722
19256
  await skipSubscribedMessage({
18723
19257
  thread,
@@ -18757,7 +19291,7 @@ function createSlackTurnRuntime(deps) {
18757
19291
  );
18758
19292
  };
18759
19293
  const onInputCommitted = async () => {
18760
- await hooks?.onInputCommitted?.();
19294
+ await hooks.onInputCommitted?.();
18761
19295
  await startQueuedProcessingReactions();
18762
19296
  };
18763
19297
  const toolInvocationHook = createToolInvocationHook(
@@ -18766,19 +19300,20 @@ function createSlackTurnRuntime(deps) {
18766
19300
  );
18767
19301
  await deps.replyToThread(thread, message, {
18768
19302
  explicitMention: Boolean(message.isMention),
19303
+ destination: hooks.destination,
18769
19304
  preparedState,
18770
- beforeFirstResponsePost: hooks?.beforeFirstResponsePost,
19305
+ beforeFirstResponsePost: hooks.beforeFirstResponsePost,
18771
19306
  queuedMessages,
18772
19307
  onInputCommitted,
18773
19308
  onToolInvocation: toolInvocationHook,
18774
19309
  onTurnCompleted,
18775
19310
  drainSteeringMessages,
18776
- onTurnStatePersisted: hooks?.onTurnStatePersisted,
18777
- shouldYield: hooks?.shouldYield
19311
+ onTurnStatePersisted: hooks.onTurnStatePersisted,
19312
+ shouldYield: hooks.shouldYield
18778
19313
  });
18779
19314
  });
18780
19315
  } catch (error) {
18781
- if (isCooperativeTurnYieldError(error) || isTurnInputCommitLostError(error)) {
19316
+ if (shouldRethrowTurnControlError(error)) {
18782
19317
  throw error;
18783
19318
  }
18784
19319
  const errorContext = logContext({
@@ -18810,7 +19345,7 @@ function createSlackTurnRuntime(deps) {
18810
19345
  "Sentry did not return an event ID for subscribed_message_handler_failed"
18811
19346
  );
18812
19347
  }
18813
- await hooks?.beforeFirstResponsePost?.();
19348
+ await hooks.beforeFirstResponsePost?.();
18814
19349
  await postFallbackErrorReplyWithLogging({
18815
19350
  thread,
18816
19351
  errorContext,
@@ -19935,7 +20470,7 @@ async function loadPiMessagesForTurn(args) {
19935
20470
  return { piMessages: fallback };
19936
20471
  }
19937
20472
  function createReplyToThread(deps) {
19938
- return async function replyToThread(thread, message, options = {}) {
20473
+ return async function replyToThread(thread, message, options) {
19939
20474
  if (message.author.isMe) {
19940
20475
  return;
19941
20476
  }
@@ -19950,7 +20485,8 @@ function createReplyToThread(deps) {
19950
20485
  const threadTs = getThreadTs(threadId);
19951
20486
  const assistantThreadContext = getAssistantThreadContext(message);
19952
20487
  const messageTs = getMessageTs(message);
19953
- const teamId = getTeamId(message);
20488
+ const destination = options.destination;
20489
+ const teamId = destination.teamId;
19954
20490
  const runId = getRunId(thread, message);
19955
20491
  const conversationId = threadId ?? runId;
19956
20492
  await withSpan(
@@ -20167,6 +20703,7 @@ function createReplyToThread(deps) {
20167
20703
  state: "running",
20168
20704
  surface: "slack",
20169
20705
  requester,
20706
+ destination,
20170
20707
  traceId: getActiveTraceId()
20171
20708
  }).catch((error) => {
20172
20709
  logException(
@@ -20347,6 +20884,7 @@ function createReplyToThread(deps) {
20347
20884
  omittedImageAttachmentCount,
20348
20885
  userAttachments,
20349
20886
  slackConversation,
20887
+ destination,
20350
20888
  surface: "slack",
20351
20889
  turnDeadlineAtMs: getTurnRequestDeadline()?.deadlineAtMs,
20352
20890
  correlation: {
@@ -20416,10 +20954,7 @@ function createReplyToThread(deps) {
20416
20954
  );
20417
20955
  const plannedPosts = planSlackReplyPosts({ reply });
20418
20956
  const replyFooter = buildSlackReplyFooter({
20419
- conversationId,
20420
- durationMs: reply.diagnostics.durationMs,
20421
- thinkingLevel: reply.diagnostics.thinkingLevel,
20422
- usage: reply.diagnostics.usage
20957
+ conversationId
20423
20958
  });
20424
20959
  const shouldUseSlackFooter = Boolean(replyFooter) && Boolean(channelId && threadTs) && thread.adapter?.name === "slack";
20425
20960
  if (plannedPosts.length > 0) {
@@ -20508,6 +21043,7 @@ function createReplyToThread(deps) {
20508
21043
  state: "completed",
20509
21044
  conversationTitle: titleUpdateResult?.title,
20510
21045
  requester,
21046
+ destination,
20511
21047
  traceId: getActiveTraceId()
20512
21048
  });
20513
21049
  }
@@ -20548,10 +21084,11 @@ function createReplyToThread(deps) {
20548
21084
  const conversationIdForResume = error.metadata?.conversationId;
20549
21085
  const sessionIdForResume = error.metadata?.sessionId;
20550
21086
  const version = error.metadata?.version;
20551
- if (conversationIdForResume && sessionIdForResume && typeof version === "number") {
21087
+ if (conversationIdForResume && sessionIdForResume && typeof version === "number" && destination) {
20552
21088
  try {
20553
21089
  await deps.services.scheduleTurnTimeoutResume({
20554
21090
  conversationId: conversationIdForResume,
21091
+ destination,
20555
21092
  sessionId: sessionIdForResume,
20556
21093
  expectedVersion: version
20557
21094
  });
@@ -20659,6 +21196,7 @@ function createReplyToThread(deps) {
20659
21196
  startedAtMs: message.metadata.dateSent.getTime(),
20660
21197
  state: "failed",
20661
21198
  requester,
21199
+ destination,
20662
21200
  traceId: getActiveTraceId()
20663
21201
  });
20664
21202
  const sessionRecord = await getAgentTurnSessionRecord(
@@ -21097,7 +21635,7 @@ function nonEmptyString(value) {
21097
21635
  return trimmed || void 0;
21098
21636
  }
21099
21637
 
21100
- // src/chat/queue/thread-message-dispatcher.ts
21638
+ // src/chat/slack/attachment-fetchers.ts
21101
21639
  function rehydrateAttachmentFetchers(message, downloadPrivateSlackFile2 = downloadPrivateSlackFile) {
21102
21640
  for (const attachment of message.attachments) {
21103
21641
  if (!attachment.fetchData && attachment.url) {
@@ -21397,6 +21935,7 @@ function createSlackConversationWorker(options) {
21397
21935
  try {
21398
21936
  if (route === "mention") {
21399
21937
  await options.runtime.handleNewMention(thread, latestMessage, {
21938
+ destination: context.destination,
21400
21939
  messageContext,
21401
21940
  drainSteeringMessages,
21402
21941
  onInputCommitted,
@@ -21405,6 +21944,7 @@ function createSlackConversationWorker(options) {
21405
21944
  return;
21406
21945
  }
21407
21946
  await options.runtime.handleSubscribedMessage(thread, latestMessage, {
21947
+ destination: context.destination,
21408
21948
  messageContext,
21409
21949
  drainSteeringMessages,
21410
21950
  onInputCommitted,
@@ -21429,8 +21969,16 @@ function createSlackConversationWorker(options) {
21429
21969
  }
21430
21970
  function buildSlackInboundMessage(args) {
21431
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
+ }
21432
21979
  return {
21433
21980
  conversationId: args.conversationId,
21981
+ destination,
21434
21982
  inboundMessageId: [
21435
21983
  "slack",
21436
21984
  args.installation?.teamId ?? args.installation?.enterpriseId ?? "unknown",
@@ -22337,7 +22885,11 @@ async function POST3(request, platform, waitUntil) {
22337
22885
  }
22338
22886
 
22339
22887
  // src/chat/task-execution/vercel-callback.ts
22340
- import { handleCallback } from "@vercel/queue";
22888
+ import {
22889
+ handleCallback,
22890
+ QueueClient,
22891
+ registerDevConsumer
22892
+ } from "@vercel/queue";
22341
22893
 
22342
22894
  // src/chat/task-execution/worker.ts
22343
22895
  var CONVERSATION_WORK_DEFER_DELAY_MS = 15e3;
@@ -22350,7 +22902,10 @@ function nudgeIdempotencyKey(reason, conversationId, nowMs) {
22350
22902
  }
22351
22903
  async function sendWakeNudge(args) {
22352
22904
  await args.options.queue.send(
22353
- { conversationId: args.conversationId },
22905
+ {
22906
+ conversationId: args.conversationId,
22907
+ destination: args.destination
22908
+ },
22354
22909
  {
22355
22910
  delayMs: args.delayMs,
22356
22911
  idempotencyKey: args.idempotencyKey
@@ -22365,6 +22920,7 @@ async function sendWakeNudge(args) {
22365
22920
  async function requestLostLeaseRecovery(args) {
22366
22921
  const continuationMarked = await requestConversationContinuation({
22367
22922
  conversationId: args.conversationId,
22923
+ destination: args.destination,
22368
22924
  leaseToken: args.leaseToken,
22369
22925
  nowMs: args.nowMs,
22370
22926
  state: args.options.state
@@ -22383,6 +22939,7 @@ async function requestLostLeaseRecovery(args) {
22383
22939
  }
22384
22940
  await sendWakeNudge({
22385
22941
  conversationId: args.conversationId,
22942
+ destination: args.destination,
22386
22943
  idempotencyKey: nudgeIdempotencyKey(
22387
22944
  "lost_lease",
22388
22945
  args.conversationId,
@@ -22426,7 +22983,8 @@ function startLeaseCheckIn(args) {
22426
22983
  timer.unref?.();
22427
22984
  return timer;
22428
22985
  }
22429
- async function processConversationWork(conversationId, options) {
22986
+ async function processConversationWork(message, options) {
22987
+ const conversationId = message.conversationId;
22430
22988
  const initial = await getConversationWorkState({
22431
22989
  conversationId,
22432
22990
  state: options.state
@@ -22434,6 +22992,12 @@ async function processConversationWork(conversationId, options) {
22434
22992
  if (!initial || countPendingConversationMessages(initial) === 0 && !initial.needsRun && !initial.lease) {
22435
22993
  return { status: "no_work" };
22436
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;
22437
23001
  const lease = await startConversationWork({
22438
23002
  conversationId,
22439
23003
  nowMs: now2(options),
@@ -22446,6 +23010,7 @@ async function processConversationWork(conversationId, options) {
22446
23010
  const nudgeNowMs = now2(options);
22447
23011
  await sendWakeNudge({
22448
23012
  conversationId,
23013
+ destination,
22449
23014
  delayMs: CONVERSATION_WORK_DEFER_DELAY_MS,
22450
23015
  idempotencyKey: nudgeIdempotencyKey("active", conversationId, nudgeNowMs),
22451
23016
  nowMs: nudgeNowMs,
@@ -22484,6 +23049,7 @@ async function processConversationWork(conversationId, options) {
22484
23049
  );
22485
23050
  const workerContext = {
22486
23051
  conversationId,
23052
+ destination,
22487
23053
  leaseToken: lease.leaseToken,
22488
23054
  shouldYield: () => leaseLost || now2(options) >= softYieldDeadlineMs,
22489
23055
  checkIn: async () => {
@@ -22511,6 +23077,7 @@ async function processConversationWork(conversationId, options) {
22511
23077
  if (result.status === "lost_lease") {
22512
23078
  await requestLostLeaseRecovery({
22513
23079
  conversationId,
23080
+ destination,
22514
23081
  leaseToken: lease.leaseToken,
22515
23082
  nowMs: now2(options),
22516
23083
  options
@@ -22520,6 +23087,7 @@ async function processConversationWork(conversationId, options) {
22520
23087
  if (leaseLost) {
22521
23088
  await requestLostLeaseRecovery({
22522
23089
  conversationId,
23090
+ destination,
22523
23091
  leaseToken: lease.leaseToken,
22524
23092
  nowMs: now2(options),
22525
23093
  options
@@ -22530,6 +23098,7 @@ async function processConversationWork(conversationId, options) {
22530
23098
  const yieldNowMs = now2(options);
22531
23099
  const continuationMarked = await requestConversationContinuation({
22532
23100
  conversationId,
23101
+ destination,
22533
23102
  leaseToken: lease.leaseToken,
22534
23103
  nowMs: yieldNowMs,
22535
23104
  state: options.state
@@ -22539,6 +23108,7 @@ async function processConversationWork(conversationId, options) {
22539
23108
  }
22540
23109
  await sendWakeNudge({
22541
23110
  conversationId,
23111
+ destination,
22542
23112
  idempotencyKey: nudgeIdempotencyKey(
22543
23113
  "yield",
22544
23114
  conversationId,
@@ -22577,6 +23147,7 @@ async function processConversationWork(conversationId, options) {
22577
23147
  const nudgeNowMs = now2(options);
22578
23148
  await sendWakeNudge({
22579
23149
  conversationId,
23150
+ destination,
22580
23151
  idempotencyKey: nudgeIdempotencyKey(
22581
23152
  "pending",
22582
23153
  conversationId,
@@ -22601,6 +23172,7 @@ async function processConversationWork(conversationId, options) {
22601
23172
  try {
22602
23173
  const continuationMarked = await requestConversationContinuation({
22603
23174
  conversationId,
23175
+ destination,
22604
23176
  leaseToken: lease.leaseToken,
22605
23177
  nowMs: errorNowMs,
22606
23178
  state: options.state
@@ -22608,6 +23180,7 @@ async function processConversationWork(conversationId, options) {
22608
23180
  if (continuationMarked) {
22609
23181
  await sendWakeNudge({
22610
23182
  conversationId,
23183
+ destination,
22611
23184
  idempotencyKey: nudgeIdempotencyKey(
22612
23185
  "error",
22613
23186
  conversationId,
@@ -22642,15 +23215,17 @@ async function processConversationWork(conversationId, options) {
22642
23215
  "Conversation work release failed after runner error"
22643
23216
  );
22644
23217
  }
22645
- logException(
22646
- error,
22647
- "conversation_work_failed",
22648
- { conversationId },
22649
- {
22650
- "app.worker.elapsed_ms": now2(options) - startedAtMs
22651
- },
22652
- "Conversation work failed"
22653
- );
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
+ }
22654
23229
  throw error;
22655
23230
  } finally {
22656
23231
  clearInterval(timer);
@@ -22659,12 +23234,19 @@ async function processConversationWork(conversationId, options) {
22659
23234
 
22660
23235
  // src/chat/task-execution/vercel-callback.ts
22661
23236
  var CONVERSATION_WORK_VISIBILITY_TIMEOUT_BUFFER_SECONDS = 30;
23237
+ var CONVERSATION_WORK_DEV_CONSUMER_GROUP = "junior_conversation_work_dev";
22662
23238
  function parseConversationQueueMessage(message) {
22663
- if (!message || typeof message !== "object" || typeof message.conversationId !== "string" || !message.conversationId.trim()) {
22664
- 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
+ );
22665
23246
  }
22666
23247
  return {
22667
- conversationId: message.conversationId
23248
+ conversationId: message.conversationId,
23249
+ destination
22668
23250
  };
22669
23251
  }
22670
23252
  function resolveConversationWorkVisibilityTimeoutSeconds(functionMaxDurationSeconds = getChatConfig().functionMaxDurationSeconds) {
@@ -22672,7 +23254,7 @@ function resolveConversationWorkVisibilityTimeoutSeconds(functionMaxDurationSeco
22672
23254
  }
22673
23255
  async function processConversationQueueMessage(message, options) {
22674
23256
  const parsed = parseConversationQueueMessage(message);
22675
- return await processConversationWork(parsed.conversationId, {
23257
+ return await processConversationWork(parsed, {
22676
23258
  checkInIntervalMs: options.checkInIntervalMs,
22677
23259
  nowMs: options.nowMs,
22678
23260
  queue: options.queue ?? getVercelConversationWorkQueue(),
@@ -22681,22 +23263,35 @@ async function processConversationQueueMessage(message, options) {
22681
23263
  state: options.state
22682
23264
  });
22683
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
+ }
22684
23275
  function createVercelConversationWorkCallback(options) {
22685
23276
  return handleCallback(
22686
- async (message) => {
22687
- const verified = verifySignedConversationQueueMessage(message);
22688
- if (!verified) {
22689
- throw new Error("Unauthorized conversation queue message");
22690
- }
22691
- await runWithTurnRequestDeadline(
22692
- () => processConversationQueueMessage(verified, options)
22693
- );
22694
- },
23277
+ (message) => handleConversationQueueMessage(message, options),
22695
23278
  {
22696
23279
  visibilityTimeoutSeconds: options.visibilityTimeoutSeconds ?? resolveConversationWorkVisibilityTimeoutSeconds()
22697
23280
  }
22698
23281
  );
22699
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
+ }
22700
23295
 
22701
23296
  // src/app.ts
22702
23297
  async function defaultWaitUntil() {
@@ -22719,7 +23314,7 @@ async function resolveVirtualConfig() {
22719
23314
  return {
22720
23315
  pluginSet: mod.pluginSet,
22721
23316
  plugins: mod.plugins,
22722
- trustedPluginRegistrations: mod.trustedPluginRegistrations ?? []
23317
+ pluginHookRegistrations: mod.pluginHookRegistrations ?? []
22723
23318
  };
22724
23319
  } catch (error) {
22725
23320
  if (!isMissingVirtualConfig(error)) {
@@ -22788,20 +23383,20 @@ function validateBuildIncludesPluginPackages(pluginConfig, virtualConfig) {
22788
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 }).`
22789
23384
  );
22790
23385
  }
22791
- function validateBuildIncludesTrustedRegistrations(trustedRegistrations, virtualConfig) {
22792
- const bundledTrustedRegistrations = virtualConfig?.trustedPluginRegistrations ?? [];
22793
- if (bundledTrustedRegistrations.length === 0) {
23386
+ function validateBuildIncludesPluginHookRegistrations(hookRegistrations, virtualConfig) {
23387
+ const bundledHookRegistrations = virtualConfig?.pluginHookRegistrations ?? [];
23388
+ if (bundledHookRegistrations.length === 0) {
22794
23389
  return;
22795
23390
  }
22796
- const registered = new Set(trustedRegistrations.map((plugin) => plugin.name));
22797
- const missing = bundledTrustedRegistrations.filter(
23391
+ const registered = new Set(hookRegistrations.map((plugin) => plugin.name));
23392
+ const missing = bundledHookRegistrations.filter(
22798
23393
  (pluginName) => !registered.has(pluginName)
22799
23394
  );
22800
23395
  if (missing.length === 0) {
22801
23396
  return;
22802
23397
  }
22803
23398
  throw new Error(
22804
- `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 }).`
22805
23400
  );
22806
23401
  }
22807
23402
  function validatePluginRegistrations(registrations) {
@@ -22817,6 +23412,51 @@ function validatePluginRegistrations(registrations) {
22817
23412
  }
22818
23413
  }
22819
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
+ }
22820
23460
  function mountAgentPluginRoutes(app, routes) {
22821
23461
  for (const route of routes) {
22822
23462
  const handler = (c) => route.handler(c.req.raw);
@@ -22834,12 +23474,12 @@ function mountAgentPluginRoutes(app, routes) {
22834
23474
  async function createApp(options) {
22835
23475
  const virtualConfig = await resolveVirtualConfig();
22836
23476
  const configuredPlugins = options?.plugins ?? virtualConfig?.pluginSet;
22837
- const agentPlugins = trustedPluginRegistrationsFromPluginSet(configuredPlugins);
23477
+ const agentPlugins = pluginHookRegistrationsFromPluginSet(configuredPlugins);
22838
23478
  const pluginConfig = configuredPlugins ? pluginCatalogConfigFromPluginSet(configuredPlugins) : virtualConfig?.plugins ?? resolveEnvPluginCatalogConfig();
22839
23479
  if (configuredPlugins) {
22840
23480
  validateBuildIncludesPluginPackages(pluginConfig, virtualConfig);
22841
23481
  }
22842
- validateBuildIncludesTrustedRegistrations(agentPlugins, virtualConfig);
23482
+ validateBuildIncludesPluginHookRegistrations(agentPlugins, virtualConfig);
22843
23483
  validateAgentPlugins(agentPlugins);
22844
23484
  const shouldValidatePluginCatalog = hasConfiguredPluginCatalog(pluginConfig) || Boolean(configuredPlugins?.registrations.length) || Boolean(Object.keys(options?.configDefaults ?? {}).length);
22845
23485
  const previousPluginCatalogConfig = setPluginCatalogConfig(pluginConfig);
@@ -22855,6 +23495,9 @@ async function createApp(options) {
22855
23495
  if (shouldValidatePluginCatalog) {
22856
23496
  getPluginCatalogSignature();
22857
23497
  validatePluginRegistrations(configuredPlugins?.registrations ?? []);
23498
+ validatePluginEgressCredentialHooks(
23499
+ configuredPlugins?.registrations ?? []
23500
+ );
22858
23501
  }
22859
23502
  agentPluginRoutes = getAgentPluginRoutes();
22860
23503
  } catch (error) {
@@ -22892,9 +23535,17 @@ async function createApp(options) {
22892
23535
  return POST(c.req.raw, waitUntil);
22893
23536
  });
22894
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
+ }
22895
23546
  app.post("/api/internal/agent/continue", (c) => {
22896
23547
  agentContinuePOST ??= createVercelConversationWorkCallback(
22897
- options?.conversationWork ?? getProductionConversationWorkOptions()
23548
+ getConversationWorkOptions()
22898
23549
  );
22899
23550
  return agentContinuePOST(c.req.raw);
22900
23551
  });