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