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