@sentry/junior 0.67.3 → 0.69.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.js +1465 -814
- 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/slack/footer.d.ts +2 -7
- 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
|
}
|
|
@@ -12762,51 +12929,6 @@ function escapeSlackMrkdwn(text) {
|
|
|
12762
12929
|
function escapeSlackLinkUrl(url) {
|
|
12763
12930
|
return url.replaceAll("&", "&").replaceAll("<", "%3C").replaceAll(">", "%3E");
|
|
12764
12931
|
}
|
|
12765
|
-
function formatSlackTokenCount(value) {
|
|
12766
|
-
if (value >= 1e6) {
|
|
12767
|
-
const millions = value / 1e6;
|
|
12768
|
-
return `${parseFloat(millions.toFixed(2))}m`;
|
|
12769
|
-
}
|
|
12770
|
-
if (value >= 1e3) {
|
|
12771
|
-
const thousands = value / 1e3;
|
|
12772
|
-
return `${parseFloat(thousands.toFixed(1))}k`;
|
|
12773
|
-
}
|
|
12774
|
-
return `${value}`;
|
|
12775
|
-
}
|
|
12776
|
-
function formatSlackDuration(durationMs) {
|
|
12777
|
-
if (durationMs < 1e3) {
|
|
12778
|
-
return `${durationMs}ms`;
|
|
12779
|
-
}
|
|
12780
|
-
const totalSeconds = Math.round(durationMs / 1e3);
|
|
12781
|
-
if (totalSeconds < 10) {
|
|
12782
|
-
const precise = durationMs / 1e3;
|
|
12783
|
-
return `${precise.toFixed(1).replace(/\.0$/, "")}s`;
|
|
12784
|
-
}
|
|
12785
|
-
if (totalSeconds < 60) {
|
|
12786
|
-
return `${totalSeconds}s`;
|
|
12787
|
-
}
|
|
12788
|
-
const minutes = Math.floor(totalSeconds / 60);
|
|
12789
|
-
const seconds = totalSeconds % 60;
|
|
12790
|
-
if (seconds === 0) {
|
|
12791
|
-
return `${minutes}m`;
|
|
12792
|
-
}
|
|
12793
|
-
return `${minutes}m${seconds}s`;
|
|
12794
|
-
}
|
|
12795
|
-
function resolveTotalTokens(usage) {
|
|
12796
|
-
if (!usage) {
|
|
12797
|
-
return void 0;
|
|
12798
|
-
}
|
|
12799
|
-
const components = [
|
|
12800
|
-
usage.inputTokens,
|
|
12801
|
-
usage.outputTokens,
|
|
12802
|
-
usage.cachedInputTokens,
|
|
12803
|
-
usage.cacheCreationTokens
|
|
12804
|
-
].filter((value) => value !== void 0);
|
|
12805
|
-
if (components.length > 0) {
|
|
12806
|
-
return components.reduce((sum, value) => sum + value, 0);
|
|
12807
|
-
}
|
|
12808
|
-
return usage.totalTokens;
|
|
12809
|
-
}
|
|
12810
12932
|
function buildSlackReplyFooter(args) {
|
|
12811
12933
|
const items = [];
|
|
12812
12934
|
const conversationId = args.conversationId?.trim();
|
|
@@ -12821,26 +12943,6 @@ function buildSlackReplyFooter(args) {
|
|
|
12821
12943
|
}
|
|
12822
12944
|
items.push(idItem);
|
|
12823
12945
|
}
|
|
12824
|
-
const totalTokens = resolveTotalTokens(args.usage);
|
|
12825
|
-
if (totalTokens !== void 0) {
|
|
12826
|
-
items.push({
|
|
12827
|
-
label: "Tokens",
|
|
12828
|
-
value: formatSlackTokenCount(totalTokens)
|
|
12829
|
-
});
|
|
12830
|
-
}
|
|
12831
|
-
if (typeof args.durationMs === "number" && Number.isFinite(args.durationMs)) {
|
|
12832
|
-
const durationMs = Math.max(0, Math.floor(args.durationMs));
|
|
12833
|
-
items.push({
|
|
12834
|
-
label: "Time",
|
|
12835
|
-
value: formatSlackDuration(durationMs)
|
|
12836
|
-
});
|
|
12837
|
-
}
|
|
12838
|
-
if (args.thinkingLevel) {
|
|
12839
|
-
items.push({
|
|
12840
|
-
label: "Thinking",
|
|
12841
|
-
value: args.thinkingLevel
|
|
12842
|
-
});
|
|
12843
|
-
}
|
|
12844
12946
|
return items.length > 0 ? { items } : void 0;
|
|
12845
12947
|
}
|
|
12846
12948
|
function buildSlackReplyBlocks(text, footer) {
|
|
@@ -13201,11 +13303,80 @@ async function verifyDispatchCallbackRequest(request) {
|
|
|
13201
13303
|
|
|
13202
13304
|
// src/chat/agent-dispatch/store.ts
|
|
13203
13305
|
import { createHash } from "crypto";
|
|
13306
|
+
import {
|
|
13307
|
+
agentPluginCredentialSubjectSchema,
|
|
13308
|
+
destinationSchema
|
|
13309
|
+
} from "@sentry/junior-plugin-api";
|
|
13310
|
+
import { z as z3 } from "zod";
|
|
13204
13311
|
var DISPATCH_PREFIX = "junior:agent_dispatch";
|
|
13205
13312
|
var DISPATCH_LOCK_TTL_MS = 10 * 60 * 1e3;
|
|
13206
13313
|
var DISPATCH_INDEX_LOCK_TTL_MS = 1e4;
|
|
13207
13314
|
var DISPATCH_INDEX_MAX_LENGTH = 1e4;
|
|
13208
13315
|
var DEFAULT_MAX_ATTEMPTS = 5;
|
|
13316
|
+
var nonEmptyExactStringSchema = z3.string().min(1).refine(
|
|
13317
|
+
(value) => value === value.trim() && value.toLowerCase() !== "unknown"
|
|
13318
|
+
);
|
|
13319
|
+
var dispatchStatusSchema = z3.enum([
|
|
13320
|
+
"pending",
|
|
13321
|
+
"running",
|
|
13322
|
+
"awaiting_resume",
|
|
13323
|
+
"completed",
|
|
13324
|
+
"failed",
|
|
13325
|
+
"blocked"
|
|
13326
|
+
]);
|
|
13327
|
+
var dispatchActorSchema = z3.object({
|
|
13328
|
+
type: z3.literal("system"),
|
|
13329
|
+
id: nonEmptyExactStringSchema
|
|
13330
|
+
}).strict();
|
|
13331
|
+
var credentialSubjectBindingSchema = z3.object({
|
|
13332
|
+
type: z3.literal("slack-direct-conversation"),
|
|
13333
|
+
teamId: z3.string().min(1),
|
|
13334
|
+
channelId: z3.string().min(1),
|
|
13335
|
+
signature: z3.string().min(1)
|
|
13336
|
+
}).strict();
|
|
13337
|
+
var boundCredentialSubjectSchema = agentPluginCredentialSubjectSchema.extend({
|
|
13338
|
+
binding: credentialSubjectBindingSchema
|
|
13339
|
+
}).strict();
|
|
13340
|
+
var dispatchRecordSchema = z3.object({
|
|
13341
|
+
actor: dispatchActorSchema,
|
|
13342
|
+
attempt: z3.number().int().nonnegative(),
|
|
13343
|
+
createdAtMs: z3.number().finite(),
|
|
13344
|
+
credentialSubject: boundCredentialSubjectSchema.optional(),
|
|
13345
|
+
destination: destinationSchema,
|
|
13346
|
+
errorMessage: z3.string().optional(),
|
|
13347
|
+
id: nonEmptyExactStringSchema,
|
|
13348
|
+
idempotencyKey: z3.string().min(1),
|
|
13349
|
+
input: z3.string().min(1),
|
|
13350
|
+
lastCallbackAtMs: z3.number().finite().optional(),
|
|
13351
|
+
leaseExpiresAtMs: z3.number().finite().optional(),
|
|
13352
|
+
maxAttempts: z3.number().int().positive(),
|
|
13353
|
+
metadata: z3.record(z3.string(), z3.string()).optional(),
|
|
13354
|
+
plugin: nonEmptyExactStringSchema,
|
|
13355
|
+
resultMessageTs: z3.string().optional(),
|
|
13356
|
+
status: dispatchStatusSchema,
|
|
13357
|
+
updatedAtMs: z3.number().finite(),
|
|
13358
|
+
version: z3.number().int().positive()
|
|
13359
|
+
}).strict().superRefine((record, ctx) => {
|
|
13360
|
+
const subject = record.credentialSubject;
|
|
13361
|
+
if (!subject) {
|
|
13362
|
+
return;
|
|
13363
|
+
}
|
|
13364
|
+
if (!record.destination.channelId.startsWith("D")) {
|
|
13365
|
+
ctx.addIssue({
|
|
13366
|
+
code: z3.ZodIssueCode.custom,
|
|
13367
|
+
message: "Dispatch credentialSubject requires a private direct Slack destination",
|
|
13368
|
+
path: ["credentialSubject"]
|
|
13369
|
+
});
|
|
13370
|
+
return;
|
|
13371
|
+
}
|
|
13372
|
+
if (subject.binding.teamId !== record.destination.teamId || subject.binding.channelId !== record.destination.channelId) {
|
|
13373
|
+
ctx.addIssue({
|
|
13374
|
+
code: z3.ZodIssueCode.custom,
|
|
13375
|
+
message: "Dispatch credentialSubject binding must match destination",
|
|
13376
|
+
path: ["credentialSubject", "binding"]
|
|
13377
|
+
});
|
|
13378
|
+
}
|
|
13379
|
+
});
|
|
13209
13380
|
function getDispatchStorageKey(id) {
|
|
13210
13381
|
return `${DISPATCH_PREFIX}:record:${id}`;
|
|
13211
13382
|
}
|
|
@@ -13231,8 +13402,12 @@ function buildDispatchId(plugin, idempotencyKey) {
|
|
|
13231
13402
|
const digest = createHash("sha256").update(plugin).update("\0").update(idempotencyKey).digest("hex").slice(0, 32);
|
|
13232
13403
|
return `dispatch_${digest}`;
|
|
13233
13404
|
}
|
|
13405
|
+
function parseDispatchRecord(value) {
|
|
13406
|
+
const parsed = dispatchRecordSchema.safeParse(value);
|
|
13407
|
+
return parsed.success ? parsed.data : void 0;
|
|
13408
|
+
}
|
|
13234
13409
|
function getDispatchDestinationLockId(destination) {
|
|
13235
|
-
return
|
|
13410
|
+
return destinationKey(destination);
|
|
13236
13411
|
}
|
|
13237
13412
|
function getDispatchConversationId(dispatch) {
|
|
13238
13413
|
return `agent-dispatch:${dispatch.id}`;
|
|
@@ -13299,22 +13474,28 @@ async function syncIncompleteDispatchIndex(state, record) {
|
|
|
13299
13474
|
});
|
|
13300
13475
|
}
|
|
13301
13476
|
async function putRecord(state, record) {
|
|
13477
|
+
const next = parseDispatchRecord(record);
|
|
13478
|
+
if (!next) {
|
|
13479
|
+
throw new Error("Dispatch record is invalid.");
|
|
13480
|
+
}
|
|
13302
13481
|
await state.set(
|
|
13303
|
-
getDispatchStorageKey(
|
|
13304
|
-
|
|
13482
|
+
getDispatchStorageKey(next.id),
|
|
13483
|
+
next,
|
|
13305
13484
|
JUNIOR_THREAD_STATE_TTL_MS
|
|
13306
13485
|
);
|
|
13307
|
-
await syncIncompleteDispatchIndex(state,
|
|
13486
|
+
await syncIncompleteDispatchIndex(state, next);
|
|
13308
13487
|
}
|
|
13309
13488
|
async function getDispatchRecord(id) {
|
|
13310
13489
|
const state = getStateAdapter();
|
|
13311
13490
|
await state.connect();
|
|
13312
|
-
return await state.get(getDispatchStorageKey(id))
|
|
13491
|
+
return parseDispatchRecord(await state.get(getDispatchStorageKey(id)));
|
|
13313
13492
|
}
|
|
13314
13493
|
async function createOrGetDispatch(args) {
|
|
13315
13494
|
const id = buildDispatchId(args.plugin, args.options.idempotencyKey);
|
|
13316
13495
|
return await withDispatchLock(id, async (state) => {
|
|
13317
|
-
const existing =
|
|
13496
|
+
const existing = parseDispatchRecord(
|
|
13497
|
+
await state.get(getDispatchStorageKey(id))
|
|
13498
|
+
);
|
|
13318
13499
|
if (existing) {
|
|
13319
13500
|
return { record: existing, status: "already_exists" };
|
|
13320
13501
|
}
|
|
@@ -13409,9 +13590,12 @@ async function persistRuntimePatch(args) {
|
|
|
13409
13590
|
}
|
|
13410
13591
|
async function markDispatch(args) {
|
|
13411
13592
|
return await withDispatchLock(args.dispatch.id, async (state) => {
|
|
13412
|
-
const current =
|
|
13413
|
-
getDispatchStorageKey(args.dispatch.id)
|
|
13414
|
-
)
|
|
13593
|
+
const current = parseDispatchRecord(
|
|
13594
|
+
await state.get(getDispatchStorageKey(args.dispatch.id))
|
|
13595
|
+
);
|
|
13596
|
+
if (!current) {
|
|
13597
|
+
throw new Error("Dispatch record is missing or invalid.");
|
|
13598
|
+
}
|
|
13415
13599
|
return await updateDispatchRecord(state, {
|
|
13416
13600
|
...current,
|
|
13417
13601
|
status: args.status,
|
|
@@ -13437,7 +13621,9 @@ async function runAgentDispatchSlice(callback, deps = {}) {
|
|
|
13437
13621
|
const scheduleCallback = deps.scheduleCallback ?? scheduleDispatchCallback;
|
|
13438
13622
|
const nowMs = Date.now();
|
|
13439
13623
|
const claimedDispatch = await withDispatchLock(callback.id, async (state) => {
|
|
13440
|
-
const current =
|
|
13624
|
+
const current = parseDispatchRecord(
|
|
13625
|
+
await state.get(getDispatchStorageKey(callback.id))
|
|
13626
|
+
);
|
|
13441
13627
|
if (!current || !canClaimDispatch(current, nowMs) || current.version !== callback.expectedVersion) {
|
|
13442
13628
|
return void 0;
|
|
13443
13629
|
}
|
|
@@ -13472,10 +13658,10 @@ async function runAgentDispatchSlice(callback, deps = {}) {
|
|
|
13472
13658
|
const startedDispatch = await withDispatchLock(
|
|
13473
13659
|
dispatch.id,
|
|
13474
13660
|
async (state) => {
|
|
13475
|
-
const current =
|
|
13476
|
-
getDispatchStorageKey(dispatch.id)
|
|
13477
|
-
)
|
|
13478
|
-
if (current.status !== "running" || current.version !== dispatch.version || current.attempt >= current.maxAttempts) {
|
|
13661
|
+
const current = parseDispatchRecord(
|
|
13662
|
+
await state.get(getDispatchStorageKey(dispatch.id))
|
|
13663
|
+
);
|
|
13664
|
+
if (!current || current.status !== "running" || current.version !== dispatch.version || current.attempt >= current.maxAttempts) {
|
|
13479
13665
|
return void 0;
|
|
13480
13666
|
}
|
|
13481
13667
|
return await updateDispatchRecord(state, {
|
|
@@ -13527,6 +13713,7 @@ async function runAgentDispatchSlice(callback, deps = {}) {
|
|
|
13527
13713
|
conversationContext,
|
|
13528
13714
|
artifactState: artifacts,
|
|
13529
13715
|
piMessages: conversation.piMessages,
|
|
13716
|
+
destination: dispatch.destination,
|
|
13530
13717
|
correlation: {
|
|
13531
13718
|
conversationId,
|
|
13532
13719
|
threadId: conversationId,
|
|
@@ -13585,10 +13772,7 @@ async function runAgentDispatchSlice(callback, deps = {}) {
|
|
|
13585
13772
|
channelId: dispatch.destination.channelId,
|
|
13586
13773
|
posts: planSlackReplyPosts({ reply: deliveryReply }),
|
|
13587
13774
|
footer: buildSlackReplyFooter({
|
|
13588
|
-
conversationId
|
|
13589
|
-
durationMs: deliveryReply.diagnostics.durationMs,
|
|
13590
|
-
thinkingLevel: deliveryReply.diagnostics.thinkingLevel,
|
|
13591
|
-
usage: deliveryReply.diagnostics.usage
|
|
13775
|
+
conversationId
|
|
13592
13776
|
}),
|
|
13593
13777
|
fileUploadFailureMode: "strict"
|
|
13594
13778
|
});
|
|
@@ -13797,14 +13981,16 @@ function normalizeMessage(value) {
|
|
|
13797
13981
|
const conversationId = toOptionalString(value.conversationId);
|
|
13798
13982
|
const inboundMessageId = toOptionalString(value.inboundMessageId);
|
|
13799
13983
|
const source = normalizeSource(value.source);
|
|
13984
|
+
const destination = parseDestination(value.destination);
|
|
13800
13985
|
const createdAtMs = toOptionalNumber(value.createdAtMs);
|
|
13801
13986
|
const receivedAtMs = toOptionalNumber(value.receivedAtMs);
|
|
13802
13987
|
const input = normalizeInput(value.input);
|
|
13803
|
-
if (!conversationId || !inboundMessageId || !source || typeof createdAtMs !== "number" || typeof receivedAtMs !== "number" || !input) {
|
|
13988
|
+
if (!conversationId || !destination || !inboundMessageId || !source || typeof createdAtMs !== "number" || typeof receivedAtMs !== "number" || !input) {
|
|
13804
13989
|
return void 0;
|
|
13805
13990
|
}
|
|
13806
13991
|
return {
|
|
13807
13992
|
conversationId,
|
|
13993
|
+
destination,
|
|
13808
13994
|
inboundMessageId,
|
|
13809
13995
|
source,
|
|
13810
13996
|
createdAtMs,
|
|
@@ -13836,14 +14022,16 @@ function normalizeWorkState(conversationId, value) {
|
|
|
13836
14022
|
return void 0;
|
|
13837
14023
|
}
|
|
13838
14024
|
const storedConversationId = toOptionalString(value.conversationId);
|
|
14025
|
+
const destination = parseDestination(value.destination);
|
|
13839
14026
|
const updatedAtMs = toOptionalNumber(value.updatedAtMs);
|
|
13840
|
-
if (storedConversationId !== conversationId || typeof updatedAtMs !== "number") {
|
|
14027
|
+
if (storedConversationId !== conversationId || !destination || typeof updatedAtMs !== "number") {
|
|
13841
14028
|
return void 0;
|
|
13842
14029
|
}
|
|
13843
14030
|
const messages = Array.isArray(value.messages) ? value.messages.map(normalizeMessage).filter((message) => Boolean(message)).filter((message) => message.conversationId === conversationId).sort(compareMessages) : [];
|
|
13844
14031
|
return {
|
|
13845
14032
|
schemaVersion: CONVERSATION_WORK_SCHEMA_VERSION,
|
|
13846
14033
|
conversationId,
|
|
14034
|
+
destination,
|
|
13847
14035
|
messages,
|
|
13848
14036
|
needsRun: value.needsRun === true,
|
|
13849
14037
|
updatedAtMs,
|
|
@@ -13855,6 +14043,7 @@ function emptyWorkState(args) {
|
|
|
13855
14043
|
return {
|
|
13856
14044
|
schemaVersion: CONVERSATION_WORK_SCHEMA_VERSION,
|
|
13857
14045
|
conversationId: args.conversationId,
|
|
14046
|
+
destination: args.destination,
|
|
13858
14047
|
messages: [],
|
|
13859
14048
|
needsRun: false,
|
|
13860
14049
|
updatedAtMs: args.nowMs
|
|
@@ -13968,10 +14157,15 @@ async function withConversationMutation(args, callback) {
|
|
|
13968
14157
|
}
|
|
13969
14158
|
}
|
|
13970
14159
|
async function readWorkState(state, conversationId) {
|
|
13971
|
-
|
|
13972
|
-
|
|
13973
|
-
|
|
13974
|
-
|
|
14160
|
+
const raw = await state.get(stateKey(conversationId));
|
|
14161
|
+
if (raw == null) {
|
|
14162
|
+
return void 0;
|
|
14163
|
+
}
|
|
14164
|
+
const work = normalizeWorkState(conversationId, raw);
|
|
14165
|
+
if (!work) {
|
|
14166
|
+
throw new Error(`Conversation work state is invalid for ${conversationId}`);
|
|
14167
|
+
}
|
|
14168
|
+
return work;
|
|
13975
14169
|
}
|
|
13976
14170
|
async function writeWorkState(state, work) {
|
|
13977
14171
|
await state.set(
|
|
@@ -13988,6 +14182,14 @@ async function writeWorkState(state, work) {
|
|
|
13988
14182
|
function hasRunnableWork(state) {
|
|
13989
14183
|
return state.needsRun || pendingMessages(state).length > 0;
|
|
13990
14184
|
}
|
|
14185
|
+
function assertSameConversationDestination(args) {
|
|
14186
|
+
if (sameDestination(args.current, args.next)) {
|
|
14187
|
+
return;
|
|
14188
|
+
}
|
|
14189
|
+
throw new Error(
|
|
14190
|
+
`Conversation work destination changed for ${args.conversationId}`
|
|
14191
|
+
);
|
|
14192
|
+
}
|
|
13991
14193
|
async function getConversationWorkState(args) {
|
|
13992
14194
|
const state = await getConnectedState(args.state);
|
|
13993
14195
|
return await readWorkState(state, args.conversationId);
|
|
@@ -14005,8 +14207,14 @@ async function appendInboundMessage(args) {
|
|
|
14005
14207
|
async (state) => {
|
|
14006
14208
|
const current = await readWorkState(state, args.message.conversationId) ?? emptyWorkState({
|
|
14007
14209
|
conversationId: args.message.conversationId,
|
|
14210
|
+
destination: args.message.destination,
|
|
14008
14211
|
nowMs
|
|
14009
14212
|
});
|
|
14213
|
+
assertSameConversationDestination({
|
|
14214
|
+
conversationId: args.message.conversationId,
|
|
14215
|
+
current: current.destination,
|
|
14216
|
+
next: args.message.destination
|
|
14217
|
+
});
|
|
14010
14218
|
const existing = current.messages.find(
|
|
14011
14219
|
(message) => message.inboundMessageId === args.message.inboundMessageId
|
|
14012
14220
|
);
|
|
@@ -14049,7 +14257,10 @@ async function appendAndEnqueueInboundMessage(args) {
|
|
|
14049
14257
|
idempotencyKey = duplicateInboundNudgeIdempotencyKey(args.message, nowMs);
|
|
14050
14258
|
}
|
|
14051
14259
|
const queueResult = await args.queue.send(
|
|
14052
|
-
{
|
|
14260
|
+
{
|
|
14261
|
+
conversationId: args.message.conversationId,
|
|
14262
|
+
destination: args.message.destination
|
|
14263
|
+
},
|
|
14053
14264
|
{ idempotencyKey }
|
|
14054
14265
|
);
|
|
14055
14266
|
await markConversationWorkEnqueued({
|
|
@@ -14066,8 +14277,16 @@ async function requestConversationWork(args) {
|
|
|
14066
14277
|
const nowMs = args.nowMs ?? now();
|
|
14067
14278
|
return await withConversationMutation(args, async (state) => {
|
|
14068
14279
|
const existing = await readWorkState(state, args.conversationId);
|
|
14280
|
+
if (existing) {
|
|
14281
|
+
assertSameConversationDestination({
|
|
14282
|
+
conversationId: args.conversationId,
|
|
14283
|
+
current: existing.destination,
|
|
14284
|
+
next: args.destination
|
|
14285
|
+
});
|
|
14286
|
+
}
|
|
14069
14287
|
const current = existing ?? emptyWorkState({
|
|
14070
14288
|
conversationId: args.conversationId,
|
|
14289
|
+
destination: args.destination,
|
|
14071
14290
|
nowMs
|
|
14072
14291
|
});
|
|
14073
14292
|
await writeWorkState(state, {
|
|
@@ -14223,6 +14442,11 @@ async function requestConversationContinuation(args) {
|
|
|
14223
14442
|
if (!current || current.lease?.leaseToken !== args.leaseToken) {
|
|
14224
14443
|
return false;
|
|
14225
14444
|
}
|
|
14445
|
+
assertSameConversationDestination({
|
|
14446
|
+
conversationId: args.conversationId,
|
|
14447
|
+
current: current.destination,
|
|
14448
|
+
next: args.destination
|
|
14449
|
+
});
|
|
14226
14450
|
await writeWorkState(state, {
|
|
14227
14451
|
...current,
|
|
14228
14452
|
needsRun: true,
|
|
@@ -14300,8 +14524,12 @@ async function getAwaitingTurnContinuationRequest(args) {
|
|
|
14300
14524
|
if (!sessionRecord || sessionRecord.state !== "awaiting_resume" || sessionRecord.resumeReason !== "timeout" && sessionRecord.resumeReason !== "yield" || sessionRecord.resumeReason === "timeout" && sessionRecord.sliceId < 2) {
|
|
14301
14525
|
return void 0;
|
|
14302
14526
|
}
|
|
14527
|
+
if (!sessionRecord.destination) {
|
|
14528
|
+
return void 0;
|
|
14529
|
+
}
|
|
14303
14530
|
return {
|
|
14304
14531
|
conversationId: args.conversationId,
|
|
14532
|
+
destination: sessionRecord.destination,
|
|
14305
14533
|
sessionId: args.sessionId,
|
|
14306
14534
|
expectedVersion: sessionRecord.version
|
|
14307
14535
|
};
|
|
@@ -14329,12 +14557,17 @@ function parseTurnTimeoutResumeRequest(value) {
|
|
|
14329
14557
|
return void 0;
|
|
14330
14558
|
}
|
|
14331
14559
|
const record = value;
|
|
14332
|
-
const
|
|
14333
|
-
|
|
14560
|
+
const destination = parseDestination(record.destination);
|
|
14561
|
+
let expectedVersion = record.expectedVersion;
|
|
14562
|
+
if (typeof expectedVersion !== "number") {
|
|
14563
|
+
expectedVersion = record.expectedCheckpointVersion;
|
|
14564
|
+
}
|
|
14565
|
+
if (typeof record.conversationId !== "string" || typeof record.sessionId !== "string" || typeof expectedVersion !== "number" || !destination) {
|
|
14334
14566
|
return void 0;
|
|
14335
14567
|
}
|
|
14336
14568
|
return {
|
|
14337
14569
|
conversationId: record.conversationId,
|
|
14570
|
+
destination,
|
|
14338
14571
|
sessionId: record.sessionId,
|
|
14339
14572
|
expectedVersion
|
|
14340
14573
|
};
|
|
@@ -14343,12 +14576,16 @@ async function scheduleTurnTimeoutResume(request, options = {}) {
|
|
|
14343
14576
|
const nowMs = options.nowMs ?? Date.now();
|
|
14344
14577
|
await requestConversationWork({
|
|
14345
14578
|
conversationId: request.conversationId,
|
|
14579
|
+
destination: request.destination,
|
|
14346
14580
|
nowMs,
|
|
14347
14581
|
state: options.state
|
|
14348
14582
|
});
|
|
14349
14583
|
const queue = options.queue ?? getVercelConversationWorkQueue();
|
|
14350
14584
|
await queue.send(
|
|
14351
|
-
{
|
|
14585
|
+
{
|
|
14586
|
+
conversationId: request.conversationId,
|
|
14587
|
+
destination: request.destination
|
|
14588
|
+
},
|
|
14352
14589
|
{
|
|
14353
14590
|
idempotencyKey: [
|
|
14354
14591
|
"timeout",
|
|
@@ -14394,7 +14631,10 @@ function heartbeatIdempotencyKey(reason, conversationId, nowMs) {
|
|
|
14394
14631
|
}
|
|
14395
14632
|
async function sendRecoveryNudge(args) {
|
|
14396
14633
|
await args.queue.send(
|
|
14397
|
-
{
|
|
14634
|
+
{
|
|
14635
|
+
conversationId: args.conversationId,
|
|
14636
|
+
destination: args.destination
|
|
14637
|
+
},
|
|
14398
14638
|
{ idempotencyKey: args.idempotencyKey }
|
|
14399
14639
|
);
|
|
14400
14640
|
await markConversationWorkEnqueued({
|
|
@@ -14432,6 +14672,7 @@ async function recoverConversationWork(args) {
|
|
|
14432
14672
|
}
|
|
14433
14673
|
await sendRecoveryNudge({
|
|
14434
14674
|
conversationId,
|
|
14675
|
+
destination: work.destination,
|
|
14435
14676
|
idempotencyKey: heartbeatIdempotencyKey(
|
|
14436
14677
|
"lease",
|
|
14437
14678
|
conversationId,
|
|
@@ -14458,6 +14699,7 @@ async function recoverConversationWork(args) {
|
|
|
14458
14699
|
}
|
|
14459
14700
|
await sendRecoveryNudge({
|
|
14460
14701
|
conversationId,
|
|
14702
|
+
destination: work.destination,
|
|
14461
14703
|
idempotencyKey: heartbeatIdempotencyKey(
|
|
14462
14704
|
"pending",
|
|
14463
14705
|
conversationId,
|
|
@@ -14487,78 +14729,92 @@ async function recoverConversationWork(args) {
|
|
|
14487
14729
|
return result;
|
|
14488
14730
|
}
|
|
14489
14731
|
|
|
14490
|
-
// src/chat/
|
|
14491
|
-
|
|
14492
|
-
|
|
14732
|
+
// src/chat/agent-dispatch/validation.ts
|
|
14733
|
+
import {
|
|
14734
|
+
dispatchOptionsSchema
|
|
14735
|
+
} from "@sentry/junior-plugin-api";
|
|
14736
|
+
function hasIssueAtPath(issues, path9) {
|
|
14737
|
+
return issues.some(
|
|
14738
|
+
(issue) => issue.path.length === path9.length && issue.path.every((value, index) => value === path9[index])
|
|
14739
|
+
);
|
|
14493
14740
|
}
|
|
14494
|
-
function
|
|
14495
|
-
return
|
|
14741
|
+
function hasIssueUnderPath(issues, path9) {
|
|
14742
|
+
return issues.some(
|
|
14743
|
+
(issue) => path9.every((value, index) => issue.path[index] === value)
|
|
14744
|
+
);
|
|
14496
14745
|
}
|
|
14497
|
-
|
|
14498
|
-
|
|
14499
|
-
|
|
14500
|
-
|
|
14501
|
-
|
|
14502
|
-
|
|
14503
|
-
|
|
14504
|
-
|
|
14505
|
-
|
|
14506
|
-
|
|
14746
|
+
function dispatchOptionsErrorMessage(issues) {
|
|
14747
|
+
if (hasIssueAtPath(issues, [])) {
|
|
14748
|
+
const unknownKeys = issues.some(
|
|
14749
|
+
(issue) => issue.code === "unrecognized_keys"
|
|
14750
|
+
);
|
|
14751
|
+
return unknownKeys ? "Dispatch options must not include unknown fields" : "Dispatch options are required";
|
|
14752
|
+
}
|
|
14753
|
+
if (issues.some(
|
|
14754
|
+
(issue) => issue.code === "unrecognized_keys" && issue.path[0] === "destination"
|
|
14755
|
+
)) {
|
|
14756
|
+
return "Dispatch destination must not include unknown fields";
|
|
14757
|
+
}
|
|
14758
|
+
if (issues.some(
|
|
14759
|
+
(issue) => issue.code === "unrecognized_keys" && issue.path[0] === "credentialSubject"
|
|
14760
|
+
)) {
|
|
14761
|
+
return "Dispatch credentialSubject binding is runtime-owned";
|
|
14507
14762
|
}
|
|
14508
|
-
if (
|
|
14509
|
-
|
|
14763
|
+
if (hasIssueAtPath(issues, ["destination"])) {
|
|
14764
|
+
return "Dispatch destination platform must be slack";
|
|
14510
14765
|
}
|
|
14511
|
-
if (
|
|
14512
|
-
|
|
14766
|
+
if (hasIssueUnderPath(issues, ["destination", "teamId"])) {
|
|
14767
|
+
return "Dispatch destination teamId must be a Slack team id";
|
|
14513
14768
|
}
|
|
14514
|
-
if (
|
|
14515
|
-
|
|
14769
|
+
if (hasIssueUnderPath(issues, ["destination", "channelId"])) {
|
|
14770
|
+
return "Dispatch destination channelId must be a Slack channel id";
|
|
14516
14771
|
}
|
|
14517
|
-
if (
|
|
14518
|
-
|
|
14519
|
-
|
|
14772
|
+
if (hasIssueUnderPath(issues, ["idempotencyKey"])) {
|
|
14773
|
+
const tooLong = issues.some(
|
|
14774
|
+
(issue) => issue.path[0] === "idempotencyKey" && issue.code === "too_big"
|
|
14520
14775
|
);
|
|
14776
|
+
return tooLong ? "Dispatch idempotencyKey exceeds the maximum length" : "Dispatch idempotencyKey is required";
|
|
14521
14777
|
}
|
|
14522
|
-
if (
|
|
14523
|
-
|
|
14778
|
+
if (hasIssueUnderPath(issues, ["input"])) {
|
|
14779
|
+
const tooLong = issues.some(
|
|
14780
|
+
(issue) => issue.path[0] === "input" && issue.code === "too_big"
|
|
14781
|
+
);
|
|
14782
|
+
return tooLong ? "Dispatch input exceeds the maximum length" : "Dispatch input is required";
|
|
14524
14783
|
}
|
|
14525
|
-
if (
|
|
14526
|
-
|
|
14784
|
+
if (hasIssueUnderPath(issues, ["credentialSubject", "userId"])) {
|
|
14785
|
+
return "Dispatch credentialSubject userId is required";
|
|
14527
14786
|
}
|
|
14528
|
-
if (
|
|
14529
|
-
|
|
14530
|
-
|
|
14531
|
-
|
|
14532
|
-
|
|
14533
|
-
|
|
14534
|
-
|
|
14535
|
-
|
|
14536
|
-
|
|
14537
|
-
|
|
14538
|
-
|
|
14539
|
-
|
|
14540
|
-
|
|
14787
|
+
if (hasIssueUnderPath(issues, ["credentialSubject", "allowedWhen"])) {
|
|
14788
|
+
return "Dispatch credentialSubject allowedWhen must be private-direct-conversation";
|
|
14789
|
+
}
|
|
14790
|
+
if (hasIssueUnderPath(issues, ["credentialSubject"])) {
|
|
14791
|
+
return "Dispatch credentialSubject type must be user";
|
|
14792
|
+
}
|
|
14793
|
+
const metadataIssue = issues.find(
|
|
14794
|
+
(issue) => issue.path[0] === "metadata" && (issue.message === "Dispatch metadata has too many keys" || issue.message === "Dispatch metadata key exceeds the maximum length" || issue.message === "Dispatch metadata value exceeds the maximum length")
|
|
14795
|
+
);
|
|
14796
|
+
if (metadataIssue) {
|
|
14797
|
+
return metadataIssue.message;
|
|
14798
|
+
}
|
|
14799
|
+
if (hasIssueUnderPath(issues, ["metadata"])) {
|
|
14800
|
+
return "Dispatch metadata values must be strings";
|
|
14801
|
+
}
|
|
14802
|
+
return "Dispatch options are invalid";
|
|
14803
|
+
}
|
|
14804
|
+
function validateDispatchOptions(options) {
|
|
14805
|
+
const parsed = dispatchOptionsSchema.safeParse(options);
|
|
14806
|
+
if (!parsed.success) {
|
|
14807
|
+
throw new Error(dispatchOptionsErrorMessage(parsed.error.issues));
|
|
14808
|
+
}
|
|
14809
|
+
const candidate = parsed.data;
|
|
14810
|
+
const { credentialSubject, destination } = candidate;
|
|
14811
|
+
if (credentialSubject !== void 0) {
|
|
14812
|
+
if (!isDmChannel(destination.channelId)) {
|
|
14541
14813
|
throw new Error(
|
|
14542
14814
|
"Dispatch credentialSubject requires a private direct Slack destination"
|
|
14543
14815
|
);
|
|
14544
14816
|
}
|
|
14545
14817
|
}
|
|
14546
|
-
const metadata = options.metadata ?? {};
|
|
14547
|
-
const entries = Object.entries(metadata);
|
|
14548
|
-
if (entries.length > MAX_METADATA_KEYS) {
|
|
14549
|
-
throw new Error("Dispatch metadata has too many keys");
|
|
14550
|
-
}
|
|
14551
|
-
for (const [key, value] of entries) {
|
|
14552
|
-
if (!key.trim() || typeof value !== "string") {
|
|
14553
|
-
throw new Error("Dispatch metadata values must be strings");
|
|
14554
|
-
}
|
|
14555
|
-
if (key.length > MAX_METADATA_KEY_LENGTH) {
|
|
14556
|
-
throw new Error("Dispatch metadata key exceeds the maximum length");
|
|
14557
|
-
}
|
|
14558
|
-
if (value.length > MAX_METADATA_VALUE_LENGTH) {
|
|
14559
|
-
throw new Error("Dispatch metadata value exceeds the maximum length");
|
|
14560
|
-
}
|
|
14561
|
-
}
|
|
14562
14818
|
}
|
|
14563
14819
|
async function verifyDispatchCredentialSubjectAccess(options) {
|
|
14564
14820
|
if (!options.credentialSubject) {
|
|
@@ -14672,8 +14928,8 @@ function isStaleDispatch(args) {
|
|
|
14672
14928
|
}
|
|
14673
14929
|
async function failDispatch(args) {
|
|
14674
14930
|
await withDispatchLock(args.record.id, async (state) => {
|
|
14675
|
-
const current =
|
|
14676
|
-
getDispatchStorageKey(args.record.id)
|
|
14931
|
+
const current = parseDispatchRecord(
|
|
14932
|
+
await state.get(getDispatchStorageKey(args.record.id))
|
|
14677
14933
|
) ?? args.record;
|
|
14678
14934
|
if (isTerminalDispatchStatus(current.status)) {
|
|
14679
14935
|
return;
|
|
@@ -14805,7 +15061,7 @@ async function recoverStaleDispatches(args) {
|
|
|
14805
15061
|
}
|
|
14806
15062
|
return recovered;
|
|
14807
15063
|
}
|
|
14808
|
-
async function
|
|
15064
|
+
async function runPluginHeartbeats(args) {
|
|
14809
15065
|
let count = 0;
|
|
14810
15066
|
for (const plugin of getAgentPlugins()) {
|
|
14811
15067
|
if (count >= (args.limit ?? DEFAULT_PLUGIN_LIMIT)) {
|
|
@@ -14831,7 +15087,7 @@ async function runTrustedPluginHeartbeats(args) {
|
|
|
14831
15087
|
);
|
|
14832
15088
|
if (typeof result?.dispatchCount === "number" && result.dispatchCount > 0) {
|
|
14833
15089
|
logInfo(
|
|
14834
|
-
"
|
|
15090
|
+
"plugin_heartbeat_dispatched",
|
|
14835
15091
|
{},
|
|
14836
15092
|
{
|
|
14837
15093
|
"app.dispatch.count": result.dispatchCount,
|
|
@@ -14843,10 +15099,10 @@ async function runTrustedPluginHeartbeats(args) {
|
|
|
14843
15099
|
} catch (error) {
|
|
14844
15100
|
logException(
|
|
14845
15101
|
error,
|
|
14846
|
-
"
|
|
15102
|
+
"plugin_heartbeat_failed",
|
|
14847
15103
|
{},
|
|
14848
15104
|
{ "app.plugin.name": plugin.name },
|
|
14849
|
-
"
|
|
15105
|
+
"Plugin heartbeat failed"
|
|
14850
15106
|
);
|
|
14851
15107
|
}
|
|
14852
15108
|
}
|
|
@@ -14861,7 +15117,7 @@ async function runHeartbeat(args) {
|
|
|
14861
15117
|
nowMs: args.nowMs
|
|
14862
15118
|
});
|
|
14863
15119
|
await recoverStaleDispatches({ nowMs: args.nowMs });
|
|
14864
|
-
await
|
|
15120
|
+
await runPluginHeartbeats({ nowMs: args.nowMs });
|
|
14865
15121
|
}
|
|
14866
15122
|
|
|
14867
15123
|
// src/handlers/heartbeat.ts
|
|
@@ -15393,10 +15649,6 @@ function getWorkspaceTeamId() {
|
|
|
15393
15649
|
}
|
|
15394
15650
|
|
|
15395
15651
|
// src/chat/runtime/thread-context.ts
|
|
15396
|
-
function toSlackTeamId(value) {
|
|
15397
|
-
const candidate = toOptionalString(value);
|
|
15398
|
-
return candidate && isSlackTeamId(candidate) ? candidate : void 0;
|
|
15399
|
-
}
|
|
15400
15652
|
function escapeRegExp2(value) {
|
|
15401
15653
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
15402
15654
|
}
|
|
@@ -15472,14 +15724,6 @@ function getMessageTs(message) {
|
|
|
15472
15724
|
const rawRecord = raw;
|
|
15473
15725
|
return toOptionalString(rawRecord.ts) ?? toOptionalString(rawRecord.event_ts) ?? toOptionalString(rawRecord.message?.ts);
|
|
15474
15726
|
}
|
|
15475
|
-
function getTeamId(message) {
|
|
15476
|
-
const raw = message.raw;
|
|
15477
|
-
if (!raw || typeof raw !== "object") {
|
|
15478
|
-
return void 0;
|
|
15479
|
-
}
|
|
15480
|
-
const rawRecord = raw;
|
|
15481
|
-
return toSlackTeamId(rawRecord.team_id) ?? toSlackTeamId(rawRecord.team) ?? toSlackTeamId(getWorkspaceTeamId()) ?? toSlackTeamId(rawRecord.user_team);
|
|
15482
|
-
}
|
|
15483
15727
|
|
|
15484
15728
|
// src/chat/runtime/processing-reaction.ts
|
|
15485
15729
|
var noProcessingReaction = {
|
|
@@ -15821,10 +16065,6 @@ async function resumeSlackTurn(args) {
|
|
|
15821
16065
|
status.start();
|
|
15822
16066
|
const generateReply = runArgs.generateReply ?? generateAssistantReply;
|
|
15823
16067
|
const replyContext = createResumeReplyContext(runArgs, status);
|
|
15824
|
-
const priorSessionRecord = replyContext.correlation?.conversationId && replyContext.correlation?.turnId ? await getAgentTurnSessionRecord(
|
|
15825
|
-
replyContext.correlation.conversationId,
|
|
15826
|
-
replyContext.correlation.turnId
|
|
15827
|
-
) : void 0;
|
|
15828
16068
|
const replyPromise = generateReply(runArgs.messageText, replyContext);
|
|
15829
16069
|
const replyTimeoutMs = resolveReplyTimeoutMs(runArgs.replyTimeoutMs);
|
|
15830
16070
|
let reply = typeof replyTimeoutMs === "number" ? await Promise.race([
|
|
@@ -15847,13 +16087,7 @@ async function resumeSlackTurn(args) {
|
|
|
15847
16087
|
});
|
|
15848
16088
|
await status.stop();
|
|
15849
16089
|
const footer = buildSlackReplyFooter({
|
|
15850
|
-
conversationId: runArgs.replyContext?.correlation?.conversationId ?? lockKey
|
|
15851
|
-
durationMs: typeof priorSessionRecord?.cumulativeDurationMs === "number" || typeof reply.diagnostics.durationMs === "number" ? (priorSessionRecord?.cumulativeDurationMs ?? 0) + (reply.diagnostics.durationMs ?? 0) : void 0,
|
|
15852
|
-
thinkingLevel: reply.diagnostics.thinkingLevel,
|
|
15853
|
-
usage: addAgentTurnUsage(
|
|
15854
|
-
priorSessionRecord?.cumulativeUsage,
|
|
15855
|
-
reply.diagnostics.usage
|
|
15856
|
-
) ?? reply.diagnostics.usage
|
|
16090
|
+
conversationId: runArgs.replyContext?.correlation?.conversationId ?? lockKey
|
|
15857
16091
|
});
|
|
15858
16092
|
await postSlackApiReplyPosts({
|
|
15859
16093
|
channelId: runArgs.channelId,
|
|
@@ -16192,9 +16426,10 @@ async function persistFailedReplyState(channelId, threadTs, sessionId, expectedV
|
|
|
16192
16426
|
}
|
|
16193
16427
|
async function resumeAuthorizedMcpTurn(args) {
|
|
16194
16428
|
const { authSession, provider } = args;
|
|
16195
|
-
if (!authSession.channelId || !authSession.threadTs) {
|
|
16429
|
+
if (!authSession.channelId || !authSession.destination || !authSession.threadTs) {
|
|
16196
16430
|
return;
|
|
16197
16431
|
}
|
|
16432
|
+
const destination = authSession.destination;
|
|
16198
16433
|
const threadId = `slack:${authSession.channelId}:${authSession.threadTs}`;
|
|
16199
16434
|
const currentState = await getPersistedThreadState(threadId);
|
|
16200
16435
|
const conversation = coerceThreadConversationState(currentState);
|
|
@@ -16303,6 +16538,7 @@ async function resumeAuthorizedMcpTurn(args) {
|
|
|
16303
16538
|
actor: { type: "user", userId: authSession.userId }
|
|
16304
16539
|
},
|
|
16305
16540
|
requester,
|
|
16541
|
+
destination,
|
|
16306
16542
|
correlation: {
|
|
16307
16543
|
conversationId: authSession.conversationId,
|
|
16308
16544
|
turnId: lockedSessionId,
|
|
@@ -16391,6 +16627,7 @@ async function resumeAuthorizedMcpTurn(args) {
|
|
|
16391
16627
|
}
|
|
16392
16628
|
await scheduleTurnTimeoutResume({
|
|
16393
16629
|
conversationId: authSession.conversationId,
|
|
16630
|
+
destination,
|
|
16394
16631
|
sessionId: lockedSessionId,
|
|
16395
16632
|
expectedVersion: version
|
|
16396
16633
|
});
|
|
@@ -16488,13 +16725,23 @@ async function buildSkillsSummaryText() {
|
|
|
16488
16725
|
}
|
|
16489
16726
|
return lines.join("\n");
|
|
16490
16727
|
}
|
|
16491
|
-
|
|
16492
|
-
|
|
16728
|
+
function accountLabel(account) {
|
|
16729
|
+
const label = account.label ?? account.id;
|
|
16730
|
+
return account.url ? `<${account.url}|${label}>` : label;
|
|
16731
|
+
}
|
|
16732
|
+
function connectedAccountText(plugin, account) {
|
|
16733
|
+
return account ? `*${plugin.manifest.name}*
|
|
16734
|
+
Connected as ${accountLabel(account)}` : `*${plugin.manifest.name}*
|
|
16735
|
+
${plugin.manifest.description}`;
|
|
16736
|
+
}
|
|
16737
|
+
async function connectedOAuthTokens(userId, plugin, userTokenStore) {
|
|
16738
|
+
if (plugin.manifest.oauth || plugin.manifest.credentials) {
|
|
16493
16739
|
const stored = await userTokenStore.get(userId, plugin.manifest.name);
|
|
16494
|
-
return
|
|
16495
|
-
stored && hasRequiredOAuthScope(stored.scope, plugin.manifest.oauth?.scope)
|
|
16496
|
-
);
|
|
16740
|
+
return stored && hasRequiredOAuthScope(stored.scope, plugin.manifest.oauth?.scope) ? stored : void 0;
|
|
16497
16741
|
}
|
|
16742
|
+
return void 0;
|
|
16743
|
+
}
|
|
16744
|
+
async function hasConnectedMcpAccount(userId, plugin) {
|
|
16498
16745
|
if (plugin.manifest.mcp) {
|
|
16499
16746
|
return Boolean(
|
|
16500
16747
|
(await getMcpStoredOAuthCredentials(userId, plugin.manifest.name))?.tokens
|
|
@@ -16509,13 +16756,13 @@ async function buildHomeView(userId, userTokenStore) {
|
|
|
16509
16756
|
const providers = getPluginProviders();
|
|
16510
16757
|
const connectedSections = [];
|
|
16511
16758
|
for (const plugin of providers) {
|
|
16512
|
-
|
|
16759
|
+
const tokens = await connectedOAuthTokens(userId, plugin, userTokenStore);
|
|
16760
|
+
if (!tokens && !await hasConnectedMcpAccount(userId, plugin)) continue;
|
|
16513
16761
|
connectedSections.push({
|
|
16514
16762
|
type: "section",
|
|
16515
16763
|
text: {
|
|
16516
16764
|
type: "mrkdwn",
|
|
16517
|
-
text:
|
|
16518
|
-
${plugin.manifest.description}`
|
|
16765
|
+
text: connectedAccountText(plugin, tokens?.account)
|
|
16519
16766
|
},
|
|
16520
16767
|
accessory: {
|
|
16521
16768
|
type: "button",
|
|
@@ -16660,22 +16907,10 @@ async function persistFailedOAuthReplyState(args) {
|
|
|
16660
16907
|
});
|
|
16661
16908
|
}
|
|
16662
16909
|
async function resumeOAuthSessionRecordTurn(stored) {
|
|
16663
|
-
if (!stored.resumeConversationId || !stored.resumeSessionId || !stored.channelId || !stored.threadTs) {
|
|
16664
|
-
return false;
|
|
16665
|
-
}
|
|
16666
|
-
const sessionRecord = await getAgentTurnSessionRecord(
|
|
16667
|
-
stored.resumeConversationId,
|
|
16668
|
-
stored.resumeSessionId
|
|
16669
|
-
);
|
|
16670
|
-
if (!sessionRecord) {
|
|
16910
|
+
if (!stored.resumeConversationId || !stored.resumeSessionId || !stored.channelId || !stored.destination || !stored.threadTs) {
|
|
16671
16911
|
return false;
|
|
16672
16912
|
}
|
|
16673
|
-
|
|
16674
|
-
return true;
|
|
16675
|
-
}
|
|
16676
|
-
if (sessionRecord.state !== "awaiting_resume" || sessionRecord.resumeReason !== "auth") {
|
|
16677
|
-
return true;
|
|
16678
|
-
}
|
|
16913
|
+
const destination = stored.destination;
|
|
16679
16914
|
const currentState = await getPersistedThreadState(
|
|
16680
16915
|
stored.resumeConversationId
|
|
16681
16916
|
);
|
|
@@ -16684,7 +16919,8 @@ async function resumeOAuthSessionRecordTurn(stored) {
|
|
|
16684
16919
|
conversation,
|
|
16685
16920
|
kind: "plugin",
|
|
16686
16921
|
provider: stored.provider,
|
|
16687
|
-
requesterId: stored.userId
|
|
16922
|
+
requesterId: stored.userId,
|
|
16923
|
+
...stored.scope ? { scope: stored.scope } : {}
|
|
16688
16924
|
});
|
|
16689
16925
|
const resolvedSessionId = pendingAuth?.sessionId ?? stored.resumeSessionId;
|
|
16690
16926
|
const userMessage2 = resolvedSessionId ? getTurnUserMessage(conversation, resolvedSessionId) : void 0;
|
|
@@ -16701,32 +16937,34 @@ async function resumeOAuthSessionRecordTurn(stored) {
|
|
|
16701
16937
|
});
|
|
16702
16938
|
return true;
|
|
16703
16939
|
}
|
|
16704
|
-
} else {
|
|
16705
|
-
if (!userMessage2?.author?.userId) {
|
|
16706
|
-
return false;
|
|
16707
|
-
}
|
|
16708
|
-
if (conversation.processing.activeTurnId !== stored.resumeSessionId) {
|
|
16709
|
-
return true;
|
|
16710
|
-
}
|
|
16711
16940
|
}
|
|
16712
|
-
|
|
16941
|
+
const sessionRecord = await getAgentTurnSessionRecord(
|
|
16942
|
+
stored.resumeConversationId,
|
|
16943
|
+
resolvedSessionId
|
|
16944
|
+
);
|
|
16945
|
+
if (!sessionRecord) {
|
|
16713
16946
|
return false;
|
|
16714
16947
|
}
|
|
16948
|
+
if (sessionRecord.state === "completed" || sessionRecord.state === "failed" || sessionRecord.state === "abandoned") {
|
|
16949
|
+
return true;
|
|
16950
|
+
}
|
|
16951
|
+
if (sessionRecord.state !== "awaiting_resume" || sessionRecord.resumeReason !== "auth") {
|
|
16952
|
+
return true;
|
|
16953
|
+
}
|
|
16954
|
+
if (!userMessage2?.author?.userId) {
|
|
16955
|
+
return false;
|
|
16956
|
+
}
|
|
16957
|
+
if (!pendingAuth && conversation.processing.activeTurnId !== stored.resumeSessionId) {
|
|
16958
|
+
return true;
|
|
16959
|
+
}
|
|
16715
16960
|
await resumeSlackTurn({
|
|
16716
|
-
messageText: stored.pendingMessage ?? userMessage2.text,
|
|
16961
|
+
messageText: pendingAuth ? userMessage2.text : stored.pendingMessage ?? userMessage2.text,
|
|
16717
16962
|
channelId: stored.channelId,
|
|
16718
16963
|
threadTs: stored.threadTs,
|
|
16719
16964
|
messageTs: getTurnUserSlackMessageTs(userMessage2),
|
|
16720
16965
|
lockKey: stored.resumeConversationId,
|
|
16721
16966
|
initialText: "",
|
|
16722
16967
|
beforeStart: async () => {
|
|
16723
|
-
const lockedSessionRecord = await getAgentTurnSessionRecord(
|
|
16724
|
-
stored.resumeConversationId,
|
|
16725
|
-
stored.resumeSessionId
|
|
16726
|
-
);
|
|
16727
|
-
if (!lockedSessionRecord || lockedSessionRecord.state !== "awaiting_resume" || lockedSessionRecord.resumeReason !== "auth") {
|
|
16728
|
-
return false;
|
|
16729
|
-
}
|
|
16730
16968
|
const lockedState = await getPersistedThreadState(
|
|
16731
16969
|
stored.resumeConversationId
|
|
16732
16970
|
);
|
|
@@ -16736,12 +16974,20 @@ async function resumeOAuthSessionRecordTurn(stored) {
|
|
|
16736
16974
|
conversation: lockedConversation,
|
|
16737
16975
|
kind: "plugin",
|
|
16738
16976
|
provider: stored.provider,
|
|
16739
|
-
requesterId: stored.userId
|
|
16977
|
+
requesterId: stored.userId,
|
|
16978
|
+
...stored.scope ? { scope: stored.scope } : {}
|
|
16740
16979
|
});
|
|
16741
16980
|
const lockedSessionId = lockedPendingAuth?.sessionId ?? stored.resumeSessionId;
|
|
16742
16981
|
if (lockedSessionId !== resolvedSessionId) {
|
|
16743
16982
|
return false;
|
|
16744
16983
|
}
|
|
16984
|
+
const lockedSessionRecord = await getAgentTurnSessionRecord(
|
|
16985
|
+
stored.resumeConversationId,
|
|
16986
|
+
lockedSessionId
|
|
16987
|
+
);
|
|
16988
|
+
if (!lockedSessionRecord || lockedSessionRecord.state !== "awaiting_resume" || lockedSessionRecord.resumeReason !== "auth") {
|
|
16989
|
+
return false;
|
|
16990
|
+
}
|
|
16745
16991
|
if (lockedPendingAuth) {
|
|
16746
16992
|
if (!isPendingAuthLatestRequest(lockedConversation, lockedPendingAuth)) {
|
|
16747
16993
|
clearPendingAuth(lockedConversation, lockedPendingAuth.sessionId);
|
|
@@ -16789,7 +17035,7 @@ async function resumeOAuthSessionRecordTurn(stored) {
|
|
|
16789
17035
|
lockedUserMessage.author.userId
|
|
16790
17036
|
);
|
|
16791
17037
|
return {
|
|
16792
|
-
messageText: stored.pendingMessage ?? lockedUserMessage.text,
|
|
17038
|
+
messageText: lockedPendingAuth ? lockedUserMessage.text : stored.pendingMessage ?? lockedUserMessage.text,
|
|
16793
17039
|
messageTs: getTurnUserSlackMessageTs(lockedUserMessage),
|
|
16794
17040
|
replyContext: {
|
|
16795
17041
|
credentialContext: {
|
|
@@ -16799,6 +17045,7 @@ async function resumeOAuthSessionRecordTurn(stored) {
|
|
|
16799
17045
|
}
|
|
16800
17046
|
},
|
|
16801
17047
|
requester,
|
|
17048
|
+
destination,
|
|
16802
17049
|
correlation: {
|
|
16803
17050
|
conversationId: stored.resumeConversationId,
|
|
16804
17051
|
turnId: lockedSessionId,
|
|
@@ -16875,6 +17122,7 @@ async function resumeOAuthSessionRecordTurn(stored) {
|
|
|
16875
17122
|
}
|
|
16876
17123
|
await scheduleTurnTimeoutResume({
|
|
16877
17124
|
conversationId: stored.resumeConversationId,
|
|
17125
|
+
destination,
|
|
16878
17126
|
sessionId: lockedSessionId,
|
|
16879
17127
|
expectedVersion: version
|
|
16880
17128
|
});
|
|
@@ -16885,7 +17133,9 @@ async function resumeOAuthSessionRecordTurn(stored) {
|
|
|
16885
17133
|
return true;
|
|
16886
17134
|
}
|
|
16887
17135
|
async function resumePendingOAuthMessage(stored) {
|
|
16888
|
-
if (!stored.pendingMessage || !stored.channelId || !stored.threadTs)
|
|
17136
|
+
if (!stored.pendingMessage || !stored.channelId || !stored.destination || !stored.threadTs) {
|
|
17137
|
+
return;
|
|
17138
|
+
}
|
|
16889
17139
|
const threadId = `slack:${stored.channelId}:${stored.threadTs}`;
|
|
16890
17140
|
const conversation = coerceThreadConversationState(
|
|
16891
17141
|
await getPersistedThreadState(threadId)
|
|
@@ -16906,6 +17156,13 @@ async function resumePendingOAuthMessage(stored) {
|
|
|
16906
17156
|
actor: { type: "user", userId: stored.userId }
|
|
16907
17157
|
},
|
|
16908
17158
|
requester,
|
|
17159
|
+
destination: stored.destination,
|
|
17160
|
+
correlation: {
|
|
17161
|
+
conversationId: threadId,
|
|
17162
|
+
channelId: stored.channelId,
|
|
17163
|
+
threadTs: stored.threadTs,
|
|
17164
|
+
requesterId: stored.userId
|
|
17165
|
+
},
|
|
16909
17166
|
conversationContext,
|
|
16910
17167
|
piMessages: conversation.piMessages,
|
|
16911
17168
|
configuration: stored.configuration
|
|
@@ -16965,7 +17222,7 @@ async function GET4(request, provider, waitUntil) {
|
|
|
16965
17222
|
}
|
|
16966
17223
|
const stateAdapter = getStateAdapter();
|
|
16967
17224
|
const stateKey2 = `oauth-state:${state}`;
|
|
16968
|
-
const stored = await stateAdapter.get(stateKey2);
|
|
17225
|
+
const stored = parseOAuthStatePayload(await stateAdapter.get(stateKey2));
|
|
16969
17226
|
if (!stored) {
|
|
16970
17227
|
return htmlErrorResponse(
|
|
16971
17228
|
"Link expired",
|
|
@@ -16999,6 +17256,7 @@ async function GET4(request, provider, waitUntil) {
|
|
|
16999
17256
|
);
|
|
17000
17257
|
}
|
|
17001
17258
|
const redirectUri = `${baseUrl}${providerConfig.callbackPath}`;
|
|
17259
|
+
const requestedScope = stored.scope ?? providerConfig.scope;
|
|
17002
17260
|
let tokenResponse;
|
|
17003
17261
|
try {
|
|
17004
17262
|
const tokenRequest = buildOAuthTokenRequest({
|
|
@@ -17031,13 +17289,12 @@ async function GET4(request, provider, waitUntil) {
|
|
|
17031
17289
|
500
|
|
17032
17290
|
);
|
|
17033
17291
|
}
|
|
17034
|
-
const tokenData = await tokenResponse.json();
|
|
17035
17292
|
let parsedTokenResponse;
|
|
17036
17293
|
try {
|
|
17037
|
-
|
|
17038
|
-
|
|
17039
|
-
providerConfig.
|
|
17040
|
-
);
|
|
17294
|
+
const tokenData = await tokenResponse.json();
|
|
17295
|
+
parsedTokenResponse = parseOAuthTokenResponse(tokenData, requestedScope, {
|
|
17296
|
+
treatEmptyScopeAsUnreported: providerConfig.treatEmptyScopeAsUnreported
|
|
17297
|
+
});
|
|
17041
17298
|
} catch {
|
|
17042
17299
|
return htmlErrorResponse(
|
|
17043
17300
|
"Connection failed",
|
|
@@ -17045,7 +17302,7 @@ async function GET4(request, provider, waitUntil) {
|
|
|
17045
17302
|
500
|
|
17046
17303
|
);
|
|
17047
17304
|
}
|
|
17048
|
-
if (!hasRequiredOAuthScope(parsedTokenResponse.scope,
|
|
17305
|
+
if (!hasRequiredOAuthScope(parsedTokenResponse.scope, requestedScope)) {
|
|
17049
17306
|
return htmlErrorResponse(
|
|
17050
17307
|
"Connection failed",
|
|
17051
17308
|
`The ${providerLabel} authorization did not grant the access Junior requires. Return to Slack and ask Junior to connect your ${providerLabel} account again.`,
|
|
@@ -17053,7 +17310,23 @@ async function GET4(request, provider, waitUntil) {
|
|
|
17053
17310
|
);
|
|
17054
17311
|
}
|
|
17055
17312
|
const userTokenStore = createUserTokenStore();
|
|
17056
|
-
|
|
17313
|
+
let account;
|
|
17314
|
+
try {
|
|
17315
|
+
account = await resolvePluginOAuthAccount({
|
|
17316
|
+
provider,
|
|
17317
|
+
tokens: parsedTokenResponse
|
|
17318
|
+
});
|
|
17319
|
+
} catch {
|
|
17320
|
+
return htmlErrorResponse(
|
|
17321
|
+
"Connection failed",
|
|
17322
|
+
`Junior could not verify the connected ${providerLabel} account. Please try again.`,
|
|
17323
|
+
500
|
|
17324
|
+
);
|
|
17325
|
+
}
|
|
17326
|
+
await userTokenStore.set(stored.userId, provider, {
|
|
17327
|
+
...parsedTokenResponse,
|
|
17328
|
+
...account ? { account } : {}
|
|
17329
|
+
});
|
|
17057
17330
|
waitUntil(async () => {
|
|
17058
17331
|
try {
|
|
17059
17332
|
await publishAppHomeView(getSlackClient(), stored.userId, userTokenStore);
|
|
@@ -17101,6 +17374,148 @@ async function GET4(request, provider, waitUntil) {
|
|
|
17101
17374
|
});
|
|
17102
17375
|
}
|
|
17103
17376
|
|
|
17377
|
+
// src/chat/sandbox/egress-credentials.ts
|
|
17378
|
+
var HTTP_READ_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
|
|
17379
|
+
var SandboxEgressCredentialNeededError = class extends Error {
|
|
17380
|
+
authorization;
|
|
17381
|
+
grant;
|
|
17382
|
+
provider;
|
|
17383
|
+
constructor(input) {
|
|
17384
|
+
super(input.message);
|
|
17385
|
+
this.name = "SandboxEgressCredentialNeededError";
|
|
17386
|
+
this.authorization = input.authorization;
|
|
17387
|
+
this.grant = input.grant;
|
|
17388
|
+
this.provider = input.provider;
|
|
17389
|
+
}
|
|
17390
|
+
};
|
|
17391
|
+
function defaultGrantForProvider(input) {
|
|
17392
|
+
const access = HTTP_READ_METHODS.has(
|
|
17393
|
+
input.method.toUpperCase()
|
|
17394
|
+
) ? "read" : "write";
|
|
17395
|
+
return {
|
|
17396
|
+
source: "broker",
|
|
17397
|
+
grant: {
|
|
17398
|
+
name: "default",
|
|
17399
|
+
access,
|
|
17400
|
+
reason: `sandbox-egress:${input.provider}:${access}`
|
|
17401
|
+
}
|
|
17402
|
+
};
|
|
17403
|
+
}
|
|
17404
|
+
function oauthAuthorizationForProvider(provider) {
|
|
17405
|
+
const oauth = getPluginOAuthConfig(provider);
|
|
17406
|
+
return oauth ? {
|
|
17407
|
+
type: "oauth",
|
|
17408
|
+
provider,
|
|
17409
|
+
...oauth.scope ? { scope: oauth.scope } : {}
|
|
17410
|
+
} : void 0;
|
|
17411
|
+
}
|
|
17412
|
+
function credentialSubjectFromContext(context) {
|
|
17413
|
+
return "subject" in context.credentials && context.credentials.subject ? { type: "user", userId: context.credentials.subject.userId } : void 0;
|
|
17414
|
+
}
|
|
17415
|
+
function assertLeaseTransformsOwnedByProvider(provider, lease) {
|
|
17416
|
+
for (const transform of lease.headerTransforms) {
|
|
17417
|
+
if (resolveSandboxEgressProviderForHost(transform.domain) !== provider) {
|
|
17418
|
+
throw new Error(
|
|
17419
|
+
`Credential lease for ${provider} included header transform for unowned domain ${transform.domain}`
|
|
17420
|
+
);
|
|
17421
|
+
}
|
|
17422
|
+
}
|
|
17423
|
+
}
|
|
17424
|
+
async function selectSandboxEgressGrant(input) {
|
|
17425
|
+
if (!hasEgressCredentialHooks(input.provider)) {
|
|
17426
|
+
return defaultGrantForProvider(input);
|
|
17427
|
+
}
|
|
17428
|
+
const pluginGrant = await selectPluginGrant({
|
|
17429
|
+
provider: input.provider,
|
|
17430
|
+
method: input.method,
|
|
17431
|
+
upstreamUrl: input.upstreamUrl
|
|
17432
|
+
});
|
|
17433
|
+
if (!pluginGrant) {
|
|
17434
|
+
throw new Error(
|
|
17435
|
+
`Plugin "${input.provider}" grantForEgress must return a grant for sandbox egress`
|
|
17436
|
+
);
|
|
17437
|
+
}
|
|
17438
|
+
return { source: "plugin", grant: pluginGrant };
|
|
17439
|
+
}
|
|
17440
|
+
function authorizationForSandboxEgressGrant(provider, selection) {
|
|
17441
|
+
return selection.source === "broker" ? oauthAuthorizationForProvider(provider) : void 0;
|
|
17442
|
+
}
|
|
17443
|
+
async function sandboxEgressCredentialLease(provider, selection, context) {
|
|
17444
|
+
const { grant } = selection;
|
|
17445
|
+
const cached = await getSandboxEgressCredentialLease(
|
|
17446
|
+
provider,
|
|
17447
|
+
grant.name,
|
|
17448
|
+
context
|
|
17449
|
+
);
|
|
17450
|
+
if (cached) {
|
|
17451
|
+
if (selection.source === "plugin" && cached.grant.access !== grant.access) {
|
|
17452
|
+
throw new Error(
|
|
17453
|
+
`Cached credential lease for ${provider}/${grant.name} has ${cached.grant.access} access, but ${grant.access} was selected`
|
|
17454
|
+
);
|
|
17455
|
+
}
|
|
17456
|
+
return {
|
|
17457
|
+
...cached,
|
|
17458
|
+
grant
|
|
17459
|
+
};
|
|
17460
|
+
}
|
|
17461
|
+
let lease;
|
|
17462
|
+
if (selection.source === "plugin") {
|
|
17463
|
+
const credentialSubject = credentialSubjectFromContext(context);
|
|
17464
|
+
const pluginResult = await issuePluginCredential({
|
|
17465
|
+
provider,
|
|
17466
|
+
grant,
|
|
17467
|
+
actor: context.credentials.actor,
|
|
17468
|
+
...credentialSubject ? { credentialSubject } : {},
|
|
17469
|
+
userTokenStore: createUserTokenStore()
|
|
17470
|
+
});
|
|
17471
|
+
if (pluginResult.type === "needed") {
|
|
17472
|
+
throw new SandboxEgressCredentialNeededError({
|
|
17473
|
+
provider,
|
|
17474
|
+
grant,
|
|
17475
|
+
authorization: pluginResult.authorization,
|
|
17476
|
+
message: pluginResult.message
|
|
17477
|
+
});
|
|
17478
|
+
}
|
|
17479
|
+
if (pluginResult.type === "unavailable") {
|
|
17480
|
+
throw new CredentialUnavailableError(provider, pluginResult.message);
|
|
17481
|
+
}
|
|
17482
|
+
lease = pluginResult.lease;
|
|
17483
|
+
} else {
|
|
17484
|
+
lease = await issueProviderCredentialLease({
|
|
17485
|
+
context: context.credentials,
|
|
17486
|
+
provider,
|
|
17487
|
+
reason: grant.reason ?? `sandbox-egress:${provider}:default`
|
|
17488
|
+
});
|
|
17489
|
+
}
|
|
17490
|
+
const headerTransforms = lease.headerTransforms ?? [];
|
|
17491
|
+
if (headerTransforms.length === 0) {
|
|
17492
|
+
throw new Error(
|
|
17493
|
+
`Credential lease for ${provider} did not include header transforms`
|
|
17494
|
+
);
|
|
17495
|
+
}
|
|
17496
|
+
const leaseExpiresAtMs = Date.parse(lease.expiresAt);
|
|
17497
|
+
if (!Number.isFinite(leaseExpiresAtMs) || leaseExpiresAtMs <= Date.now()) {
|
|
17498
|
+
throw new Error(`Credential lease for ${provider} is expired`);
|
|
17499
|
+
}
|
|
17500
|
+
const authorization = selection.source === "broker" ? oauthAuthorizationForProvider(provider) : lease.authorization;
|
|
17501
|
+
const cachedLease = {
|
|
17502
|
+
provider,
|
|
17503
|
+
grant,
|
|
17504
|
+
...lease.account ? { account: lease.account } : {},
|
|
17505
|
+
...authorization ? { authorization } : {},
|
|
17506
|
+
expiresAt: lease.expiresAt,
|
|
17507
|
+
headerTransforms
|
|
17508
|
+
};
|
|
17509
|
+
assertLeaseTransformsOwnedByProvider(provider, cachedLease);
|
|
17510
|
+
await setSandboxEgressCredentialLease(context, cachedLease);
|
|
17511
|
+
return cachedLease;
|
|
17512
|
+
}
|
|
17513
|
+
function hasSandboxEgressLeaseTransformForHost(lease, host) {
|
|
17514
|
+
return lease.headerTransforms.some(
|
|
17515
|
+
(transform) => matchesSandboxEgressDomain(host, transform.domain)
|
|
17516
|
+
);
|
|
17517
|
+
}
|
|
17518
|
+
|
|
17104
17519
|
// src/chat/sandbox/egress-oidc.ts
|
|
17105
17520
|
import {
|
|
17106
17521
|
createRemoteJWKSet,
|
|
@@ -17204,6 +17619,19 @@ var UPSTREAM_PERMISSION_REJECTION_STATUS = 403;
|
|
|
17204
17619
|
function jsonError(message, status) {
|
|
17205
17620
|
return Response.json({ error: message }, { status });
|
|
17206
17621
|
}
|
|
17622
|
+
function authRequiredResponse(input) {
|
|
17623
|
+
return new Response(
|
|
17624
|
+
`junior-auth-required provider=${input.provider} grant=${input.grant.name} access=${input.grant.access} 401 unauthorized
|
|
17625
|
+
${input.message}`,
|
|
17626
|
+
{
|
|
17627
|
+
status: 401,
|
|
17628
|
+
headers: {
|
|
17629
|
+
"content-type": "text/plain; charset=utf-8",
|
|
17630
|
+
"cache-control": "no-store"
|
|
17631
|
+
}
|
|
17632
|
+
}
|
|
17633
|
+
);
|
|
17634
|
+
}
|
|
17207
17635
|
function shouldLogSandboxEgressInfo() {
|
|
17208
17636
|
const environment = (process.env.SENTRY_ENVIRONMENT ?? process.env.VERCEL_ENV ?? process.env.NODE_ENV ?? "").trim().toLowerCase();
|
|
17209
17637
|
return environment !== "production";
|
|
@@ -17211,7 +17639,10 @@ function shouldLogSandboxEgressInfo() {
|
|
|
17211
17639
|
function egressAttributes(input) {
|
|
17212
17640
|
return {
|
|
17213
17641
|
...input.egressId ? { "app.sandbox.egress_id": input.egressId } : {},
|
|
17214
|
-
...input.provider ? { "app.
|
|
17642
|
+
...input.provider ? { "app.provider.name": input.provider } : {},
|
|
17643
|
+
...input.grantName ? { "app.grant.name": input.grantName } : {},
|
|
17644
|
+
...input.grantAccess ? { "app.grant.access": input.grantAccess } : {},
|
|
17645
|
+
...input.grantReason ? { "app.grant.reason": input.grantReason } : {},
|
|
17215
17646
|
...input.host ? { "server.address": input.host } : {},
|
|
17216
17647
|
...input.method ? { "http.request.method": input.method } : {},
|
|
17217
17648
|
...input.path ? { "url.path": input.path } : {},
|
|
@@ -17247,9 +17678,42 @@ function routingAttributes(request, upstreamUrl) {
|
|
|
17247
17678
|
};
|
|
17248
17679
|
if (upstreamUrl) {
|
|
17249
17680
|
attributes["app.sandbox.egress.upstream_path"] = upstreamUrl.pathname;
|
|
17681
|
+
const gitService = upstreamUrl.searchParams.get("service");
|
|
17682
|
+
if (upstreamUrl.hostname.toLowerCase() === "github.com" && (gitService === "git-upload-pack" || gitService === "git-receive-pack")) {
|
|
17683
|
+
attributes["app.sandbox.egress.git_service"] = gitService;
|
|
17684
|
+
}
|
|
17250
17685
|
}
|
|
17251
17686
|
return attributes;
|
|
17252
17687
|
}
|
|
17688
|
+
function displayedUpstreamPath(upstreamUrl) {
|
|
17689
|
+
const gitService = upstreamUrl.searchParams.get("service");
|
|
17690
|
+
if (upstreamUrl.hostname.toLowerCase() === "github.com" && (gitService === "git-upload-pack" || gitService === "git-receive-pack")) {
|
|
17691
|
+
return `${upstreamUrl.pathname}?service=${gitService}`;
|
|
17692
|
+
}
|
|
17693
|
+
return upstreamUrl.pathname;
|
|
17694
|
+
}
|
|
17695
|
+
function upstreamPermissionAttributes(provider, upstream) {
|
|
17696
|
+
if (provider !== "github") {
|
|
17697
|
+
return {};
|
|
17698
|
+
}
|
|
17699
|
+
return {
|
|
17700
|
+
"app.github.accepted_permissions": upstream.headers.get("x-accepted-github-permissions") ?? void 0,
|
|
17701
|
+
"app.github.sso": upstream.headers.get("x-github-sso") ?? void 0
|
|
17702
|
+
};
|
|
17703
|
+
}
|
|
17704
|
+
function githubPermissionHeaders(upstream) {
|
|
17705
|
+
const acceptedPermissions = upstream.headers.get(
|
|
17706
|
+
"x-accepted-github-permissions"
|
|
17707
|
+
);
|
|
17708
|
+
const sso = upstream.headers.get("x-github-sso");
|
|
17709
|
+
return {
|
|
17710
|
+
...acceptedPermissions ? { acceptedPermissions } : {},
|
|
17711
|
+
...sso ? { sso } : {}
|
|
17712
|
+
};
|
|
17713
|
+
}
|
|
17714
|
+
function permissionDeniedMessage(provider, grant) {
|
|
17715
|
+
return `${provider} returned HTTP 403 after Junior injected the ${grant.name} grant. Junior forwarded the request; this is not a local runtime block.`;
|
|
17716
|
+
}
|
|
17253
17717
|
function logSandboxEgressUpstreamRequest(input) {
|
|
17254
17718
|
if (!shouldLogSandboxEgressInfo()) {
|
|
17255
17719
|
return;
|
|
@@ -17260,6 +17724,9 @@ function logSandboxEgressUpstreamRequest(input) {
|
|
|
17260
17724
|
{
|
|
17261
17725
|
...egressAttributes({
|
|
17262
17726
|
egressId: input.egressId,
|
|
17727
|
+
grantAccess: input.grantAccess,
|
|
17728
|
+
grantName: input.grantName,
|
|
17729
|
+
grantReason: input.grantReason,
|
|
17263
17730
|
host: input.upstreamUrl.hostname,
|
|
17264
17731
|
method: input.request.method,
|
|
17265
17732
|
path: input.upstreamUrl.pathname,
|
|
@@ -17269,7 +17736,7 @@ function logSandboxEgressUpstreamRequest(input) {
|
|
|
17269
17736
|
...routingAttributes(input.request, input.upstreamUrl),
|
|
17270
17737
|
"app.sandbox.egress.upstream_ok": input.upstream.ok
|
|
17271
17738
|
},
|
|
17272
|
-
`Sandbox egress ${input.request.method} ${input.upstreamUrl.hostname}${input.upstreamUrl
|
|
17739
|
+
`Sandbox egress ${input.request.method} ${input.upstreamUrl.hostname}${displayedUpstreamPath(input.upstreamUrl)} -> ${input.upstream.status}`
|
|
17273
17740
|
);
|
|
17274
17741
|
}
|
|
17275
17742
|
function normalizeHost(value) {
|
|
@@ -17384,35 +17851,6 @@ function responseHeaders(upstream) {
|
|
|
17384
17851
|
});
|
|
17385
17852
|
return headers;
|
|
17386
17853
|
}
|
|
17387
|
-
async function credentialLease(provider, context) {
|
|
17388
|
-
const cached = await getSandboxEgressCredentialLease(provider, context);
|
|
17389
|
-
if (cached) {
|
|
17390
|
-
return cached;
|
|
17391
|
-
}
|
|
17392
|
-
const lease = await issueProviderCredentialLease({
|
|
17393
|
-
context: context.credentials,
|
|
17394
|
-
provider,
|
|
17395
|
-
reason: `sandbox-egress:${provider}`
|
|
17396
|
-
});
|
|
17397
|
-
const headerTransforms = lease.headerTransforms ?? [];
|
|
17398
|
-
if (headerTransforms.length === 0) {
|
|
17399
|
-
throw new Error(
|
|
17400
|
-
`Credential lease for ${provider} did not include header transforms`
|
|
17401
|
-
);
|
|
17402
|
-
}
|
|
17403
|
-
const cachedLease = {
|
|
17404
|
-
provider,
|
|
17405
|
-
expiresAt: lease.expiresAt,
|
|
17406
|
-
headerTransforms
|
|
17407
|
-
};
|
|
17408
|
-
await setSandboxEgressCredentialLease(context, cachedLease);
|
|
17409
|
-
return cachedLease;
|
|
17410
|
-
}
|
|
17411
|
-
function hasTransformForHost(lease, host) {
|
|
17412
|
-
return lease.headerTransforms.some(
|
|
17413
|
-
(transform) => matchesSandboxEgressDomain(host, transform.domain)
|
|
17414
|
-
);
|
|
17415
|
-
}
|
|
17416
17854
|
function isSandboxEgressForwardedRequest(request) {
|
|
17417
17855
|
return Boolean(
|
|
17418
17856
|
request.headers.get(OIDC_TOKEN_HEADER)?.trim() && request.headers.get(FORWARDED_HOST_HEADER)?.trim() && request.headers.get(FORWARDED_SCHEME_HEADER)?.trim()
|
|
@@ -17518,17 +17956,72 @@ async function proxySandboxEgressRequest(request, deps = {}) {
|
|
|
17518
17956
|
403
|
|
17519
17957
|
);
|
|
17520
17958
|
}
|
|
17959
|
+
const grantSelection = await selectSandboxEgressGrant({
|
|
17960
|
+
provider,
|
|
17961
|
+
method: request.method,
|
|
17962
|
+
upstreamUrl
|
|
17963
|
+
});
|
|
17521
17964
|
let lease;
|
|
17522
17965
|
try {
|
|
17523
|
-
lease = await
|
|
17966
|
+
lease = await sandboxEgressCredentialLease(
|
|
17967
|
+
provider,
|
|
17968
|
+
grantSelection,
|
|
17969
|
+
credentialContext
|
|
17970
|
+
);
|
|
17524
17971
|
} catch (error) {
|
|
17972
|
+
if (error instanceof SandboxEgressCredentialNeededError) {
|
|
17973
|
+
await setSandboxEgressAuthRequiredSignal(credentialContext, {
|
|
17974
|
+
provider: error.provider,
|
|
17975
|
+
grant: error.grant,
|
|
17976
|
+
...error.authorization ? { authorization: error.authorization } : {},
|
|
17977
|
+
message: error.message
|
|
17978
|
+
});
|
|
17979
|
+
logWarn(
|
|
17980
|
+
"sandbox_egress_credential_needed",
|
|
17981
|
+
{},
|
|
17982
|
+
{
|
|
17983
|
+
...egressAttributes({
|
|
17984
|
+
egressId: activeEgressId,
|
|
17985
|
+
grantAccess: error.grant.access,
|
|
17986
|
+
grantName: error.grant.name,
|
|
17987
|
+
grantReason: error.grant.reason,
|
|
17988
|
+
host: upstreamUrl.hostname,
|
|
17989
|
+
method: request.method,
|
|
17990
|
+
path: upstreamUrl.pathname,
|
|
17991
|
+
provider: error.provider,
|
|
17992
|
+
status: 401
|
|
17993
|
+
}),
|
|
17994
|
+
...routingAttributes(request, upstreamUrl)
|
|
17995
|
+
},
|
|
17996
|
+
"Sandbox egress grant needs user authorization before issuing a credential lease"
|
|
17997
|
+
);
|
|
17998
|
+
return authRequiredResponse({
|
|
17999
|
+
provider: error.provider,
|
|
18000
|
+
grant: error.grant,
|
|
18001
|
+
message: error.message
|
|
18002
|
+
});
|
|
18003
|
+
}
|
|
17525
18004
|
if (error instanceof CredentialUnavailableError) {
|
|
18005
|
+
const failedGrant = grantSelection.grant;
|
|
18006
|
+
const authorization = authorizationForSandboxEgressGrant(
|
|
18007
|
+
error.provider,
|
|
18008
|
+
grantSelection
|
|
18009
|
+
);
|
|
18010
|
+
await setSandboxEgressAuthRequiredSignal(credentialContext, {
|
|
18011
|
+
provider: error.provider,
|
|
18012
|
+
grant: failedGrant,
|
|
18013
|
+
...authorization ? { authorization } : {},
|
|
18014
|
+
message: error.message
|
|
18015
|
+
});
|
|
17526
18016
|
logWarn(
|
|
17527
18017
|
"sandbox_egress_credential_unavailable",
|
|
17528
18018
|
{},
|
|
17529
18019
|
{
|
|
17530
18020
|
...egressAttributes({
|
|
17531
18021
|
egressId: activeEgressId,
|
|
18022
|
+
grantAccess: failedGrant.access,
|
|
18023
|
+
grantName: failedGrant.name,
|
|
18024
|
+
grantReason: failedGrant.reason,
|
|
17532
18025
|
host: upstreamUrl.hostname,
|
|
17533
18026
|
method: request.method,
|
|
17534
18027
|
path: upstreamUrl.pathname,
|
|
@@ -17537,26 +18030,26 @@ async function proxySandboxEgressRequest(request, deps = {}) {
|
|
|
17537
18030
|
}),
|
|
17538
18031
|
...routingAttributes(request, upstreamUrl)
|
|
17539
18032
|
},
|
|
17540
|
-
"Sandbox egress
|
|
17541
|
-
);
|
|
17542
|
-
return new Response(
|
|
17543
|
-
`junior-auth-required provider=${error.provider} 401 unauthorized
|
|
17544
|
-
${error.message}`,
|
|
17545
|
-
{
|
|
17546
|
-
status: 401,
|
|
17547
|
-
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
17548
|
-
}
|
|
18033
|
+
"Sandbox egress credential lease is unavailable for selected grant"
|
|
17549
18034
|
);
|
|
18035
|
+
return authRequiredResponse({
|
|
18036
|
+
provider: error.provider,
|
|
18037
|
+
grant: failedGrant,
|
|
18038
|
+
message: error.message
|
|
18039
|
+
});
|
|
17550
18040
|
}
|
|
17551
18041
|
throw error;
|
|
17552
18042
|
}
|
|
17553
|
-
if (!
|
|
18043
|
+
if (!hasSandboxEgressLeaseTransformForHost(lease, upstreamUrl.hostname)) {
|
|
17554
18044
|
logWarn(
|
|
17555
18045
|
"sandbox_egress_transform_missing",
|
|
17556
18046
|
{},
|
|
17557
18047
|
{
|
|
17558
18048
|
...egressAttributes({
|
|
17559
18049
|
egressId: activeEgressId,
|
|
18050
|
+
grantAccess: lease.grant.access,
|
|
18051
|
+
grantName: lease.grant.name,
|
|
18052
|
+
grantReason: lease.grant.reason,
|
|
17560
18053
|
host: upstreamUrl.hostname,
|
|
17561
18054
|
method: request.method,
|
|
17562
18055
|
path: upstreamUrl.pathname,
|
|
@@ -17572,9 +18065,9 @@ ${error.message}`,
|
|
|
17572
18065
|
);
|
|
17573
18066
|
return jsonError("Credential lease does not cover forwarded host", 403);
|
|
17574
18067
|
}
|
|
17575
|
-
const body = await requestBodyBytes(request);
|
|
17576
18068
|
const fetchImpl = deps.fetch ?? fetch;
|
|
17577
18069
|
const headers = requestHeaders(request, lease, upstreamUrl.hostname);
|
|
18070
|
+
const body = await requestBodyBytes(request);
|
|
17578
18071
|
const intercepted = await deps.interceptHttp?.({
|
|
17579
18072
|
provider,
|
|
17580
18073
|
request: new Request(upstreamUrl, {
|
|
@@ -17595,6 +18088,9 @@ ${error.message}`,
|
|
|
17595
18088
|
});
|
|
17596
18089
|
logSandboxEgressUpstreamRequest({
|
|
17597
18090
|
egressId: activeEgressId,
|
|
18091
|
+
grantAccess: lease.grant.access,
|
|
18092
|
+
grantName: lease.grant.name,
|
|
18093
|
+
grantReason: lease.grant.reason,
|
|
17598
18094
|
provider,
|
|
17599
18095
|
request,
|
|
17600
18096
|
upstream,
|
|
@@ -17607,6 +18103,9 @@ ${error.message}`,
|
|
|
17607
18103
|
{
|
|
17608
18104
|
...egressAttributes({
|
|
17609
18105
|
egressId: activeEgressId,
|
|
18106
|
+
grantAccess: lease.grant.access,
|
|
18107
|
+
grantName: lease.grant.name,
|
|
18108
|
+
grantReason: lease.grant.reason,
|
|
17610
18109
|
host: upstreamUrl.hostname,
|
|
17611
18110
|
method: request.method,
|
|
17612
18111
|
path: upstreamUrl.pathname,
|
|
@@ -17614,6 +18113,7 @@ ${error.message}`,
|
|
|
17614
18113
|
status: upstream.status
|
|
17615
18114
|
}),
|
|
17616
18115
|
...routingAttributes(request, upstreamUrl),
|
|
18116
|
+
...upstreamPermissionAttributes(provider, upstream),
|
|
17617
18117
|
"error.type": `http_${upstream.status}`
|
|
17618
18118
|
},
|
|
17619
18119
|
`Sandbox egress upstream returned HTTP ${upstream.status}`
|
|
@@ -17626,6 +18126,9 @@ ${error.message}`,
|
|
|
17626
18126
|
{
|
|
17627
18127
|
...egressAttributes({
|
|
17628
18128
|
egressId: activeEgressId,
|
|
18129
|
+
grantAccess: lease.grant.access,
|
|
18130
|
+
grantName: lease.grant.name,
|
|
18131
|
+
grantReason: lease.grant.reason,
|
|
17629
18132
|
host: upstreamUrl.hostname,
|
|
17630
18133
|
method: request.method,
|
|
17631
18134
|
path: upstreamUrl.pathname,
|
|
@@ -17633,27 +18136,49 @@ ${error.message}`,
|
|
|
17633
18136
|
status: upstream.status
|
|
17634
18137
|
}),
|
|
17635
18138
|
...routingAttributes(request, upstreamUrl),
|
|
18139
|
+
...upstreamPermissionAttributes(provider, upstream),
|
|
17636
18140
|
...upstream.status === UPSTREAM_TOKEN_REJECTION_STATUS ? {
|
|
17637
18141
|
"app.sandbox.egress.www_authenticate": upstream.headers.get("www-authenticate") ?? void 0
|
|
17638
18142
|
} : {}
|
|
17639
18143
|
},
|
|
17640
18144
|
upstream.status === UPSTREAM_TOKEN_REJECTION_STATUS ? "Sandbox egress upstream auth rejected injected credential" : "Sandbox egress upstream permission denied"
|
|
17641
18145
|
);
|
|
17642
|
-
await clearSandboxEgressCredentialLease(provider, credentialContext);
|
|
17643
18146
|
if (upstream.status === UPSTREAM_TOKEN_REJECTION_STATUS) {
|
|
18147
|
+
await clearSandboxEgressCredentialLease(
|
|
18148
|
+
provider,
|
|
18149
|
+
lease.grant.name,
|
|
18150
|
+
credentialContext
|
|
18151
|
+
);
|
|
18152
|
+
await setSandboxEgressAuthRequiredSignal(credentialContext, {
|
|
18153
|
+
provider,
|
|
18154
|
+
grant: lease.grant,
|
|
18155
|
+
...lease.authorization ? { authorization: lease.authorization } : {},
|
|
18156
|
+
message: `Provider rejected the injected ${provider} credential.`
|
|
18157
|
+
});
|
|
17644
18158
|
await upstream.body?.cancel().catch(() => void 0);
|
|
17645
|
-
return
|
|
17646
|
-
|
|
17647
|
-
|
|
17648
|
-
|
|
17649
|
-
|
|
17650
|
-
|
|
17651
|
-
|
|
17652
|
-
|
|
17653
|
-
|
|
17654
|
-
|
|
17655
|
-
|
|
18159
|
+
return authRequiredResponse({
|
|
18160
|
+
provider,
|
|
18161
|
+
grant: lease.grant,
|
|
18162
|
+
message: `Provider rejected the injected ${provider} credential.
|
|
18163
|
+
`
|
|
18164
|
+
});
|
|
18165
|
+
} else {
|
|
18166
|
+
await clearSandboxEgressCredentialLease(
|
|
18167
|
+
provider,
|
|
18168
|
+
lease.grant.name,
|
|
18169
|
+
credentialContext
|
|
17656
18170
|
);
|
|
18171
|
+
await setSandboxEgressPermissionDeniedSignal(credentialContext, {
|
|
18172
|
+
provider,
|
|
18173
|
+
grant: lease.grant,
|
|
18174
|
+
...lease.account ? { account: lease.account } : {},
|
|
18175
|
+
message: permissionDeniedMessage(provider, lease.grant),
|
|
18176
|
+
source: "upstream",
|
|
18177
|
+
status: UPSTREAM_PERMISSION_REJECTION_STATUS,
|
|
18178
|
+
upstreamHost: upstreamUrl.hostname,
|
|
18179
|
+
upstreamPath: displayedUpstreamPath(upstreamUrl),
|
|
18180
|
+
...provider === "github" ? githubPermissionHeaders(upstream) : {}
|
|
18181
|
+
});
|
|
17657
18182
|
}
|
|
17658
18183
|
}
|
|
17659
18184
|
return new Response(upstream.body, {
|
|
@@ -17798,6 +18323,7 @@ async function resumeTimedOutTurn(payload, options = {}) {
|
|
|
17798
18323
|
}
|
|
17799
18324
|
},
|
|
17800
18325
|
requester,
|
|
18326
|
+
destination: payload.destination,
|
|
17801
18327
|
correlation: {
|
|
17802
18328
|
conversationId: payload.conversationId,
|
|
17803
18329
|
turnId: payload.sessionId,
|
|
@@ -17865,6 +18391,7 @@ async function resumeTimedOutTurn(payload, options = {}) {
|
|
|
17865
18391
|
}
|
|
17866
18392
|
await scheduleTurnTimeoutResume2({
|
|
17867
18393
|
conversationId: payload.conversationId,
|
|
18394
|
+
destination: payload.destination,
|
|
17868
18395
|
sessionId: payload.sessionId,
|
|
17869
18396
|
expectedVersion: version
|
|
17870
18397
|
});
|
|
@@ -17942,14 +18469,14 @@ async function POST2(request, waitUntil, options = {}) {
|
|
|
17942
18469
|
}
|
|
17943
18470
|
|
|
17944
18471
|
// src/chat/services/subscribed-decision.ts
|
|
17945
|
-
import { z as
|
|
17946
|
-
var replyDecisionSchema =
|
|
17947
|
-
should_reply:
|
|
17948
|
-
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(
|
|
17949
18476
|
"Whether Junior should unsubscribe from this thread because the user clearly asked it to stop participating."
|
|
17950
18477
|
),
|
|
17951
|
-
confidence:
|
|
17952
|
-
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.")
|
|
17953
18480
|
});
|
|
17954
18481
|
var ROUTER_CONFIDENCE_THRESHOLD = 0.8;
|
|
17955
18482
|
var ROUTER_CLASSIFIER_MAX_TOKENS = 240;
|
|
@@ -18259,6 +18786,9 @@ async function decideSubscribedThreadReply(args) {
|
|
|
18259
18786
|
reasonDetail: reason
|
|
18260
18787
|
};
|
|
18261
18788
|
} catch (error) {
|
|
18789
|
+
if (isProviderRetryError(error)) {
|
|
18790
|
+
throw error;
|
|
18791
|
+
}
|
|
18262
18792
|
args.logClassifierFailure(error, args.input);
|
|
18263
18793
|
return {
|
|
18264
18794
|
shouldReply: false,
|
|
@@ -18343,6 +18873,9 @@ async function ensureSlackMessageActorIdentity(message, lookupSlackUser2) {
|
|
|
18343
18873
|
|
|
18344
18874
|
// src/chat/runtime/slack-runtime.ts
|
|
18345
18875
|
var THREAD_OPTOUT_ACK = "Understood. I'll stay out of this thread unless someone @mentions me again.";
|
|
18876
|
+
function shouldRethrowTurnControlError(error) {
|
|
18877
|
+
return isCooperativeTurnYieldError(error) || isTurnInputCommitLostError(error) || isProviderRetryError(error);
|
|
18878
|
+
}
|
|
18346
18879
|
async function maybeHandleThreadOptOutDecision(args) {
|
|
18347
18880
|
if (!args.decision?.shouldUnsubscribe) {
|
|
18348
18881
|
return false;
|
|
@@ -18372,7 +18905,7 @@ function getQueuedMessagesFromSlackMessages(messages, options) {
|
|
|
18372
18905
|
);
|
|
18373
18906
|
}
|
|
18374
18907
|
function createSteeringMessageDrain(hooks, options) {
|
|
18375
|
-
if (!hooks
|
|
18908
|
+
if (!hooks.drainSteeringMessages) {
|
|
18376
18909
|
return void 0;
|
|
18377
18910
|
}
|
|
18378
18911
|
return async (inject) => {
|
|
@@ -18410,7 +18943,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
18410
18943
|
if (shouldKeepProcessingReactionForToolInvocation(invocation)) {
|
|
18411
18944
|
processingReaction.keep();
|
|
18412
18945
|
}
|
|
18413
|
-
hooks
|
|
18946
|
+
hooks.onToolInvocation?.(invocation);
|
|
18414
18947
|
};
|
|
18415
18948
|
};
|
|
18416
18949
|
const stopProcessingReactions = async (processingReactions) => {
|
|
@@ -18528,7 +19061,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
18528
19061
|
);
|
|
18529
19062
|
await deps.withSpan("chat.turn", "chat.turn", context, async () => {
|
|
18530
19063
|
await thread.subscribe();
|
|
18531
|
-
const queuedMessages = getQueuedMessages(hooks
|
|
19064
|
+
const queuedMessages = getQueuedMessages(hooks.messageContext, {
|
|
18532
19065
|
explicitMention: true,
|
|
18533
19066
|
stripLeadingBotMention: deps.stripLeadingBotMention
|
|
18534
19067
|
});
|
|
@@ -18545,7 +19078,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
18545
19078
|
);
|
|
18546
19079
|
};
|
|
18547
19080
|
const onInputCommitted = async () => {
|
|
18548
|
-
await hooks
|
|
19081
|
+
await hooks.onInputCommitted?.();
|
|
18549
19082
|
await startQueuedProcessingReactions();
|
|
18550
19083
|
};
|
|
18551
19084
|
const drainSteeringMessages = createSteeringMessageDrain(hooks, {
|
|
@@ -18561,18 +19094,19 @@ function createSlackTurnRuntime(deps) {
|
|
|
18561
19094
|
});
|
|
18562
19095
|
await deps.replyToThread(thread, message, {
|
|
18563
19096
|
explicitMention: true,
|
|
18564
|
-
beforeFirstResponsePost: hooks
|
|
19097
|
+
beforeFirstResponsePost: hooks.beforeFirstResponsePost,
|
|
19098
|
+
destination: hooks.destination,
|
|
18565
19099
|
queuedMessages,
|
|
18566
19100
|
onInputCommitted,
|
|
18567
19101
|
onToolInvocation: toolInvocationHook,
|
|
18568
19102
|
onTurnCompleted,
|
|
18569
19103
|
drainSteeringMessages,
|
|
18570
|
-
onTurnStatePersisted: hooks
|
|
18571
|
-
shouldYield: hooks
|
|
19104
|
+
onTurnStatePersisted: hooks.onTurnStatePersisted,
|
|
19105
|
+
shouldYield: hooks.shouldYield
|
|
18572
19106
|
});
|
|
18573
19107
|
});
|
|
18574
19108
|
} catch (error) {
|
|
18575
|
-
if (
|
|
19109
|
+
if (shouldRethrowTurnControlError(error)) {
|
|
18576
19110
|
throw error;
|
|
18577
19111
|
}
|
|
18578
19112
|
const errorContext = logContext({
|
|
@@ -18604,7 +19138,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
18604
19138
|
"Sentry did not return an event ID for mention_handler_failed"
|
|
18605
19139
|
);
|
|
18606
19140
|
}
|
|
18607
|
-
await hooks
|
|
19141
|
+
await hooks.beforeFirstResponsePost?.();
|
|
18608
19142
|
await postFallbackErrorReplyWithLogging({
|
|
18609
19143
|
thread,
|
|
18610
19144
|
errorContext,
|
|
@@ -18658,7 +19192,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
18658
19192
|
channelId,
|
|
18659
19193
|
runId
|
|
18660
19194
|
};
|
|
18661
|
-
const queuedMessages = getQueuedMessages(hooks
|
|
19195
|
+
const queuedMessages = getQueuedMessages(hooks.messageContext, {
|
|
18662
19196
|
explicitMention: Boolean(message.isMention),
|
|
18663
19197
|
stripLeadingBotMention: deps.stripLeadingBotMention
|
|
18664
19198
|
});
|
|
@@ -18717,7 +19251,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
18717
19251
|
if (await maybeHandleThreadOptOutDecision({
|
|
18718
19252
|
thread,
|
|
18719
19253
|
decision,
|
|
18720
|
-
beforeFirstResponsePost: hooks
|
|
19254
|
+
beforeFirstResponsePost: hooks.beforeFirstResponsePost
|
|
18721
19255
|
})) {
|
|
18722
19256
|
await skipSubscribedMessage({
|
|
18723
19257
|
thread,
|
|
@@ -18757,7 +19291,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
18757
19291
|
);
|
|
18758
19292
|
};
|
|
18759
19293
|
const onInputCommitted = async () => {
|
|
18760
|
-
await hooks
|
|
19294
|
+
await hooks.onInputCommitted?.();
|
|
18761
19295
|
await startQueuedProcessingReactions();
|
|
18762
19296
|
};
|
|
18763
19297
|
const toolInvocationHook = createToolInvocationHook(
|
|
@@ -18766,19 +19300,20 @@ function createSlackTurnRuntime(deps) {
|
|
|
18766
19300
|
);
|
|
18767
19301
|
await deps.replyToThread(thread, message, {
|
|
18768
19302
|
explicitMention: Boolean(message.isMention),
|
|
19303
|
+
destination: hooks.destination,
|
|
18769
19304
|
preparedState,
|
|
18770
|
-
beforeFirstResponsePost: hooks
|
|
19305
|
+
beforeFirstResponsePost: hooks.beforeFirstResponsePost,
|
|
18771
19306
|
queuedMessages,
|
|
18772
19307
|
onInputCommitted,
|
|
18773
19308
|
onToolInvocation: toolInvocationHook,
|
|
18774
19309
|
onTurnCompleted,
|
|
18775
19310
|
drainSteeringMessages,
|
|
18776
|
-
onTurnStatePersisted: hooks
|
|
18777
|
-
shouldYield: hooks
|
|
19311
|
+
onTurnStatePersisted: hooks.onTurnStatePersisted,
|
|
19312
|
+
shouldYield: hooks.shouldYield
|
|
18778
19313
|
});
|
|
18779
19314
|
});
|
|
18780
19315
|
} catch (error) {
|
|
18781
|
-
if (
|
|
19316
|
+
if (shouldRethrowTurnControlError(error)) {
|
|
18782
19317
|
throw error;
|
|
18783
19318
|
}
|
|
18784
19319
|
const errorContext = logContext({
|
|
@@ -18810,7 +19345,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
18810
19345
|
"Sentry did not return an event ID for subscribed_message_handler_failed"
|
|
18811
19346
|
);
|
|
18812
19347
|
}
|
|
18813
|
-
await hooks
|
|
19348
|
+
await hooks.beforeFirstResponsePost?.();
|
|
18814
19349
|
await postFallbackErrorReplyWithLogging({
|
|
18815
19350
|
thread,
|
|
18816
19351
|
errorContext,
|
|
@@ -19935,7 +20470,7 @@ async function loadPiMessagesForTurn(args) {
|
|
|
19935
20470
|
return { piMessages: fallback };
|
|
19936
20471
|
}
|
|
19937
20472
|
function createReplyToThread(deps) {
|
|
19938
|
-
return async function replyToThread(thread, message, options
|
|
20473
|
+
return async function replyToThread(thread, message, options) {
|
|
19939
20474
|
if (message.author.isMe) {
|
|
19940
20475
|
return;
|
|
19941
20476
|
}
|
|
@@ -19950,7 +20485,8 @@ function createReplyToThread(deps) {
|
|
|
19950
20485
|
const threadTs = getThreadTs(threadId);
|
|
19951
20486
|
const assistantThreadContext = getAssistantThreadContext(message);
|
|
19952
20487
|
const messageTs = getMessageTs(message);
|
|
19953
|
-
const
|
|
20488
|
+
const destination = options.destination;
|
|
20489
|
+
const teamId = destination.teamId;
|
|
19954
20490
|
const runId = getRunId(thread, message);
|
|
19955
20491
|
const conversationId = threadId ?? runId;
|
|
19956
20492
|
await withSpan(
|
|
@@ -20167,6 +20703,7 @@ function createReplyToThread(deps) {
|
|
|
20167
20703
|
state: "running",
|
|
20168
20704
|
surface: "slack",
|
|
20169
20705
|
requester,
|
|
20706
|
+
destination,
|
|
20170
20707
|
traceId: getActiveTraceId()
|
|
20171
20708
|
}).catch((error) => {
|
|
20172
20709
|
logException(
|
|
@@ -20347,6 +20884,7 @@ function createReplyToThread(deps) {
|
|
|
20347
20884
|
omittedImageAttachmentCount,
|
|
20348
20885
|
userAttachments,
|
|
20349
20886
|
slackConversation,
|
|
20887
|
+
destination,
|
|
20350
20888
|
surface: "slack",
|
|
20351
20889
|
turnDeadlineAtMs: getTurnRequestDeadline()?.deadlineAtMs,
|
|
20352
20890
|
correlation: {
|
|
@@ -20416,10 +20954,7 @@ function createReplyToThread(deps) {
|
|
|
20416
20954
|
);
|
|
20417
20955
|
const plannedPosts = planSlackReplyPosts({ reply });
|
|
20418
20956
|
const replyFooter = buildSlackReplyFooter({
|
|
20419
|
-
conversationId
|
|
20420
|
-
durationMs: reply.diagnostics.durationMs,
|
|
20421
|
-
thinkingLevel: reply.diagnostics.thinkingLevel,
|
|
20422
|
-
usage: reply.diagnostics.usage
|
|
20957
|
+
conversationId
|
|
20423
20958
|
});
|
|
20424
20959
|
const shouldUseSlackFooter = Boolean(replyFooter) && Boolean(channelId && threadTs) && thread.adapter?.name === "slack";
|
|
20425
20960
|
if (plannedPosts.length > 0) {
|
|
@@ -20508,6 +21043,7 @@ function createReplyToThread(deps) {
|
|
|
20508
21043
|
state: "completed",
|
|
20509
21044
|
conversationTitle: titleUpdateResult?.title,
|
|
20510
21045
|
requester,
|
|
21046
|
+
destination,
|
|
20511
21047
|
traceId: getActiveTraceId()
|
|
20512
21048
|
});
|
|
20513
21049
|
}
|
|
@@ -20548,10 +21084,11 @@ function createReplyToThread(deps) {
|
|
|
20548
21084
|
const conversationIdForResume = error.metadata?.conversationId;
|
|
20549
21085
|
const sessionIdForResume = error.metadata?.sessionId;
|
|
20550
21086
|
const version = error.metadata?.version;
|
|
20551
|
-
if (conversationIdForResume && sessionIdForResume && typeof version === "number") {
|
|
21087
|
+
if (conversationIdForResume && sessionIdForResume && typeof version === "number" && destination) {
|
|
20552
21088
|
try {
|
|
20553
21089
|
await deps.services.scheduleTurnTimeoutResume({
|
|
20554
21090
|
conversationId: conversationIdForResume,
|
|
21091
|
+
destination,
|
|
20555
21092
|
sessionId: sessionIdForResume,
|
|
20556
21093
|
expectedVersion: version
|
|
20557
21094
|
});
|
|
@@ -20659,6 +21196,7 @@ function createReplyToThread(deps) {
|
|
|
20659
21196
|
startedAtMs: message.metadata.dateSent.getTime(),
|
|
20660
21197
|
state: "failed",
|
|
20661
21198
|
requester,
|
|
21199
|
+
destination,
|
|
20662
21200
|
traceId: getActiveTraceId()
|
|
20663
21201
|
});
|
|
20664
21202
|
const sessionRecord = await getAgentTurnSessionRecord(
|
|
@@ -21097,7 +21635,7 @@ function nonEmptyString(value) {
|
|
|
21097
21635
|
return trimmed || void 0;
|
|
21098
21636
|
}
|
|
21099
21637
|
|
|
21100
|
-
// src/chat/
|
|
21638
|
+
// src/chat/slack/attachment-fetchers.ts
|
|
21101
21639
|
function rehydrateAttachmentFetchers(message, downloadPrivateSlackFile2 = downloadPrivateSlackFile) {
|
|
21102
21640
|
for (const attachment of message.attachments) {
|
|
21103
21641
|
if (!attachment.fetchData && attachment.url) {
|
|
@@ -21397,6 +21935,7 @@ function createSlackConversationWorker(options) {
|
|
|
21397
21935
|
try {
|
|
21398
21936
|
if (route === "mention") {
|
|
21399
21937
|
await options.runtime.handleNewMention(thread, latestMessage, {
|
|
21938
|
+
destination: context.destination,
|
|
21400
21939
|
messageContext,
|
|
21401
21940
|
drainSteeringMessages,
|
|
21402
21941
|
onInputCommitted,
|
|
@@ -21405,6 +21944,7 @@ function createSlackConversationWorker(options) {
|
|
|
21405
21944
|
return;
|
|
21406
21945
|
}
|
|
21407
21946
|
await options.runtime.handleSubscribedMessage(thread, latestMessage, {
|
|
21947
|
+
destination: context.destination,
|
|
21408
21948
|
messageContext,
|
|
21409
21949
|
drainSteeringMessages,
|
|
21410
21950
|
onInputCommitted,
|
|
@@ -21429,8 +21969,16 @@ function createSlackConversationWorker(options) {
|
|
|
21429
21969
|
}
|
|
21430
21970
|
function buildSlackInboundMessage(args) {
|
|
21431
21971
|
const authorId = requireSlackAuthorId(args.message);
|
|
21972
|
+
const destination = createSlackDestination({
|
|
21973
|
+
channelId: args.thread.channelId,
|
|
21974
|
+
teamId: args.installation?.teamId
|
|
21975
|
+
});
|
|
21976
|
+
if (!destination) {
|
|
21977
|
+
throw new Error("Slack inbound message requires destination context");
|
|
21978
|
+
}
|
|
21432
21979
|
return {
|
|
21433
21980
|
conversationId: args.conversationId,
|
|
21981
|
+
destination,
|
|
21434
21982
|
inboundMessageId: [
|
|
21435
21983
|
"slack",
|
|
21436
21984
|
args.installation?.teamId ?? args.installation?.enterpriseId ?? "unknown",
|
|
@@ -22337,7 +22885,11 @@ async function POST3(request, platform, waitUntil) {
|
|
|
22337
22885
|
}
|
|
22338
22886
|
|
|
22339
22887
|
// src/chat/task-execution/vercel-callback.ts
|
|
22340
|
-
import {
|
|
22888
|
+
import {
|
|
22889
|
+
handleCallback,
|
|
22890
|
+
QueueClient,
|
|
22891
|
+
registerDevConsumer
|
|
22892
|
+
} from "@vercel/queue";
|
|
22341
22893
|
|
|
22342
22894
|
// src/chat/task-execution/worker.ts
|
|
22343
22895
|
var CONVERSATION_WORK_DEFER_DELAY_MS = 15e3;
|
|
@@ -22350,7 +22902,10 @@ function nudgeIdempotencyKey(reason, conversationId, nowMs) {
|
|
|
22350
22902
|
}
|
|
22351
22903
|
async function sendWakeNudge(args) {
|
|
22352
22904
|
await args.options.queue.send(
|
|
22353
|
-
{
|
|
22905
|
+
{
|
|
22906
|
+
conversationId: args.conversationId,
|
|
22907
|
+
destination: args.destination
|
|
22908
|
+
},
|
|
22354
22909
|
{
|
|
22355
22910
|
delayMs: args.delayMs,
|
|
22356
22911
|
idempotencyKey: args.idempotencyKey
|
|
@@ -22365,6 +22920,7 @@ async function sendWakeNudge(args) {
|
|
|
22365
22920
|
async function requestLostLeaseRecovery(args) {
|
|
22366
22921
|
const continuationMarked = await requestConversationContinuation({
|
|
22367
22922
|
conversationId: args.conversationId,
|
|
22923
|
+
destination: args.destination,
|
|
22368
22924
|
leaseToken: args.leaseToken,
|
|
22369
22925
|
nowMs: args.nowMs,
|
|
22370
22926
|
state: args.options.state
|
|
@@ -22383,6 +22939,7 @@ async function requestLostLeaseRecovery(args) {
|
|
|
22383
22939
|
}
|
|
22384
22940
|
await sendWakeNudge({
|
|
22385
22941
|
conversationId: args.conversationId,
|
|
22942
|
+
destination: args.destination,
|
|
22386
22943
|
idempotencyKey: nudgeIdempotencyKey(
|
|
22387
22944
|
"lost_lease",
|
|
22388
22945
|
args.conversationId,
|
|
@@ -22426,7 +22983,8 @@ function startLeaseCheckIn(args) {
|
|
|
22426
22983
|
timer.unref?.();
|
|
22427
22984
|
return timer;
|
|
22428
22985
|
}
|
|
22429
|
-
async function processConversationWork(
|
|
22986
|
+
async function processConversationWork(message, options) {
|
|
22987
|
+
const conversationId = message.conversationId;
|
|
22430
22988
|
const initial = await getConversationWorkState({
|
|
22431
22989
|
conversationId,
|
|
22432
22990
|
state: options.state
|
|
@@ -22434,6 +22992,12 @@ async function processConversationWork(conversationId, options) {
|
|
|
22434
22992
|
if (!initial || countPendingConversationMessages(initial) === 0 && !initial.needsRun && !initial.lease) {
|
|
22435
22993
|
return { status: "no_work" };
|
|
22436
22994
|
}
|
|
22995
|
+
if (!sameDestination(initial.destination, message.destination)) {
|
|
22996
|
+
throw new Error(
|
|
22997
|
+
`Conversation work queue destination changed for ${conversationId}`
|
|
22998
|
+
);
|
|
22999
|
+
}
|
|
23000
|
+
const destination = initial.destination;
|
|
22437
23001
|
const lease = await startConversationWork({
|
|
22438
23002
|
conversationId,
|
|
22439
23003
|
nowMs: now2(options),
|
|
@@ -22446,6 +23010,7 @@ async function processConversationWork(conversationId, options) {
|
|
|
22446
23010
|
const nudgeNowMs = now2(options);
|
|
22447
23011
|
await sendWakeNudge({
|
|
22448
23012
|
conversationId,
|
|
23013
|
+
destination,
|
|
22449
23014
|
delayMs: CONVERSATION_WORK_DEFER_DELAY_MS,
|
|
22450
23015
|
idempotencyKey: nudgeIdempotencyKey("active", conversationId, nudgeNowMs),
|
|
22451
23016
|
nowMs: nudgeNowMs,
|
|
@@ -22484,6 +23049,7 @@ async function processConversationWork(conversationId, options) {
|
|
|
22484
23049
|
);
|
|
22485
23050
|
const workerContext = {
|
|
22486
23051
|
conversationId,
|
|
23052
|
+
destination,
|
|
22487
23053
|
leaseToken: lease.leaseToken,
|
|
22488
23054
|
shouldYield: () => leaseLost || now2(options) >= softYieldDeadlineMs,
|
|
22489
23055
|
checkIn: async () => {
|
|
@@ -22511,6 +23077,7 @@ async function processConversationWork(conversationId, options) {
|
|
|
22511
23077
|
if (result.status === "lost_lease") {
|
|
22512
23078
|
await requestLostLeaseRecovery({
|
|
22513
23079
|
conversationId,
|
|
23080
|
+
destination,
|
|
22514
23081
|
leaseToken: lease.leaseToken,
|
|
22515
23082
|
nowMs: now2(options),
|
|
22516
23083
|
options
|
|
@@ -22520,6 +23087,7 @@ async function processConversationWork(conversationId, options) {
|
|
|
22520
23087
|
if (leaseLost) {
|
|
22521
23088
|
await requestLostLeaseRecovery({
|
|
22522
23089
|
conversationId,
|
|
23090
|
+
destination,
|
|
22523
23091
|
leaseToken: lease.leaseToken,
|
|
22524
23092
|
nowMs: now2(options),
|
|
22525
23093
|
options
|
|
@@ -22530,6 +23098,7 @@ async function processConversationWork(conversationId, options) {
|
|
|
22530
23098
|
const yieldNowMs = now2(options);
|
|
22531
23099
|
const continuationMarked = await requestConversationContinuation({
|
|
22532
23100
|
conversationId,
|
|
23101
|
+
destination,
|
|
22533
23102
|
leaseToken: lease.leaseToken,
|
|
22534
23103
|
nowMs: yieldNowMs,
|
|
22535
23104
|
state: options.state
|
|
@@ -22539,6 +23108,7 @@ async function processConversationWork(conversationId, options) {
|
|
|
22539
23108
|
}
|
|
22540
23109
|
await sendWakeNudge({
|
|
22541
23110
|
conversationId,
|
|
23111
|
+
destination,
|
|
22542
23112
|
idempotencyKey: nudgeIdempotencyKey(
|
|
22543
23113
|
"yield",
|
|
22544
23114
|
conversationId,
|
|
@@ -22577,6 +23147,7 @@ async function processConversationWork(conversationId, options) {
|
|
|
22577
23147
|
const nudgeNowMs = now2(options);
|
|
22578
23148
|
await sendWakeNudge({
|
|
22579
23149
|
conversationId,
|
|
23150
|
+
destination,
|
|
22580
23151
|
idempotencyKey: nudgeIdempotencyKey(
|
|
22581
23152
|
"pending",
|
|
22582
23153
|
conversationId,
|
|
@@ -22601,6 +23172,7 @@ async function processConversationWork(conversationId, options) {
|
|
|
22601
23172
|
try {
|
|
22602
23173
|
const continuationMarked = await requestConversationContinuation({
|
|
22603
23174
|
conversationId,
|
|
23175
|
+
destination,
|
|
22604
23176
|
leaseToken: lease.leaseToken,
|
|
22605
23177
|
nowMs: errorNowMs,
|
|
22606
23178
|
state: options.state
|
|
@@ -22608,6 +23180,7 @@ async function processConversationWork(conversationId, options) {
|
|
|
22608
23180
|
if (continuationMarked) {
|
|
22609
23181
|
await sendWakeNudge({
|
|
22610
23182
|
conversationId,
|
|
23183
|
+
destination,
|
|
22611
23184
|
idempotencyKey: nudgeIdempotencyKey(
|
|
22612
23185
|
"error",
|
|
22613
23186
|
conversationId,
|
|
@@ -22642,15 +23215,17 @@ async function processConversationWork(conversationId, options) {
|
|
|
22642
23215
|
"Conversation work release failed after runner error"
|
|
22643
23216
|
);
|
|
22644
23217
|
}
|
|
22645
|
-
|
|
22646
|
-
|
|
22647
|
-
|
|
22648
|
-
|
|
22649
|
-
|
|
22650
|
-
|
|
22651
|
-
|
|
22652
|
-
|
|
22653
|
-
|
|
23218
|
+
if (!isProviderRetryError(error)) {
|
|
23219
|
+
logException(
|
|
23220
|
+
error,
|
|
23221
|
+
"conversation_work_failed",
|
|
23222
|
+
{ conversationId },
|
|
23223
|
+
{
|
|
23224
|
+
"app.worker.elapsed_ms": now2(options) - startedAtMs
|
|
23225
|
+
},
|
|
23226
|
+
"Conversation work failed"
|
|
23227
|
+
);
|
|
23228
|
+
}
|
|
22654
23229
|
throw error;
|
|
22655
23230
|
} finally {
|
|
22656
23231
|
clearInterval(timer);
|
|
@@ -22659,12 +23234,19 @@ async function processConversationWork(conversationId, options) {
|
|
|
22659
23234
|
|
|
22660
23235
|
// src/chat/task-execution/vercel-callback.ts
|
|
22661
23236
|
var CONVERSATION_WORK_VISIBILITY_TIMEOUT_BUFFER_SECONDS = 30;
|
|
23237
|
+
var CONVERSATION_WORK_DEV_CONSUMER_GROUP = "junior_conversation_work_dev";
|
|
22662
23238
|
function parseConversationQueueMessage(message) {
|
|
22663
|
-
|
|
22664
|
-
|
|
23239
|
+
const destination = parseDestination(
|
|
23240
|
+
message?.destination
|
|
23241
|
+
);
|
|
23242
|
+
if (!message || typeof message !== "object" || typeof message.conversationId !== "string" || !message.conversationId.trim() || !destination) {
|
|
23243
|
+
throw new Error(
|
|
23244
|
+
"Conversation queue message is missing destination context"
|
|
23245
|
+
);
|
|
22665
23246
|
}
|
|
22666
23247
|
return {
|
|
22667
|
-
conversationId: message.conversationId
|
|
23248
|
+
conversationId: message.conversationId,
|
|
23249
|
+
destination
|
|
22668
23250
|
};
|
|
22669
23251
|
}
|
|
22670
23252
|
function resolveConversationWorkVisibilityTimeoutSeconds(functionMaxDurationSeconds = getChatConfig().functionMaxDurationSeconds) {
|
|
@@ -22672,7 +23254,7 @@ function resolveConversationWorkVisibilityTimeoutSeconds(functionMaxDurationSeco
|
|
|
22672
23254
|
}
|
|
22673
23255
|
async function processConversationQueueMessage(message, options) {
|
|
22674
23256
|
const parsed = parseConversationQueueMessage(message);
|
|
22675
|
-
return await processConversationWork(parsed
|
|
23257
|
+
return await processConversationWork(parsed, {
|
|
22676
23258
|
checkInIntervalMs: options.checkInIntervalMs,
|
|
22677
23259
|
nowMs: options.nowMs,
|
|
22678
23260
|
queue: options.queue ?? getVercelConversationWorkQueue(),
|
|
@@ -22681,22 +23263,35 @@ async function processConversationQueueMessage(message, options) {
|
|
|
22681
23263
|
state: options.state
|
|
22682
23264
|
});
|
|
22683
23265
|
}
|
|
23266
|
+
async function handleConversationQueueMessage(message, options) {
|
|
23267
|
+
const verified = verifySignedConversationQueueMessage(message);
|
|
23268
|
+
if (!verified) {
|
|
23269
|
+
throw new Error("Unauthorized conversation queue message");
|
|
23270
|
+
}
|
|
23271
|
+
await runWithTurnRequestDeadline(
|
|
23272
|
+
() => processConversationQueueMessage(verified, options)
|
|
23273
|
+
);
|
|
23274
|
+
}
|
|
22684
23275
|
function createVercelConversationWorkCallback(options) {
|
|
22685
23276
|
return handleCallback(
|
|
22686
|
-
|
|
22687
|
-
const verified = verifySignedConversationQueueMessage(message);
|
|
22688
|
-
if (!verified) {
|
|
22689
|
-
throw new Error("Unauthorized conversation queue message");
|
|
22690
|
-
}
|
|
22691
|
-
await runWithTurnRequestDeadline(
|
|
22692
|
-
() => processConversationQueueMessage(verified, options)
|
|
22693
|
-
);
|
|
22694
|
-
},
|
|
23277
|
+
(message) => handleConversationQueueMessage(message, options),
|
|
22695
23278
|
{
|
|
22696
23279
|
visibilityTimeoutSeconds: options.visibilityTimeoutSeconds ?? resolveConversationWorkVisibilityTimeoutSeconds()
|
|
22697
23280
|
}
|
|
22698
23281
|
);
|
|
22699
23282
|
}
|
|
23283
|
+
function registerVercelConversationWorkDevConsumer(options) {
|
|
23284
|
+
if (process.env.NODE_ENV !== "development") {
|
|
23285
|
+
return void 0;
|
|
23286
|
+
}
|
|
23287
|
+
return registerDevConsumer({
|
|
23288
|
+
client: new QueueClient(),
|
|
23289
|
+
consumerGroup: CONVERSATION_WORK_DEV_CONSUMER_GROUP,
|
|
23290
|
+
handler: (message) => handleConversationQueueMessage(message, options),
|
|
23291
|
+
topic: resolveConversationWorkQueueTopic(options),
|
|
23292
|
+
visibilityTimeoutSeconds: options.visibilityTimeoutSeconds ?? resolveConversationWorkVisibilityTimeoutSeconds()
|
|
23293
|
+
});
|
|
23294
|
+
}
|
|
22700
23295
|
|
|
22701
23296
|
// src/app.ts
|
|
22702
23297
|
async function defaultWaitUntil() {
|
|
@@ -22719,7 +23314,7 @@ async function resolveVirtualConfig() {
|
|
|
22719
23314
|
return {
|
|
22720
23315
|
pluginSet: mod.pluginSet,
|
|
22721
23316
|
plugins: mod.plugins,
|
|
22722
|
-
|
|
23317
|
+
pluginHookRegistrations: mod.pluginHookRegistrations ?? []
|
|
22723
23318
|
};
|
|
22724
23319
|
} catch (error) {
|
|
22725
23320
|
if (!isMissingVirtualConfig(error)) {
|
|
@@ -22788,20 +23383,20 @@ function validateBuildIncludesPluginPackages(pluginConfig, virtualConfig) {
|
|
|
22788
23383
|
`createApp() registered plugin package(s) not bundled by juniorNitro(): ${missing.join(", ")}. Point juniorNitro({ plugins: "./plugins" }) at the runtime plugin module or pass the same defineJuniorPlugins(...) set to juniorNitro({ plugins }) and createApp({ plugins }).`
|
|
22789
23384
|
);
|
|
22790
23385
|
}
|
|
22791
|
-
function
|
|
22792
|
-
const
|
|
22793
|
-
if (
|
|
23386
|
+
function validateBuildIncludesPluginHookRegistrations(hookRegistrations, virtualConfig) {
|
|
23387
|
+
const bundledHookRegistrations = virtualConfig?.pluginHookRegistrations ?? [];
|
|
23388
|
+
if (bundledHookRegistrations.length === 0) {
|
|
22794
23389
|
return;
|
|
22795
23390
|
}
|
|
22796
|
-
const registered = new Set(
|
|
22797
|
-
const missing =
|
|
23391
|
+
const registered = new Set(hookRegistrations.map((plugin) => plugin.name));
|
|
23392
|
+
const missing = bundledHookRegistrations.filter(
|
|
22798
23393
|
(pluginName) => !registered.has(pluginName)
|
|
22799
23394
|
);
|
|
22800
23395
|
if (missing.length === 0) {
|
|
22801
23396
|
return;
|
|
22802
23397
|
}
|
|
22803
23398
|
throw new Error(
|
|
22804
|
-
`createApp() is missing
|
|
23399
|
+
`createApp() is missing plugin registration(s) with runtime hooks bundled by juniorNitro(): ${missing.join(", ")}. Pass a runtime-safe plugin module to juniorNitro({ plugins: "./plugins" }) or pass the same defineJuniorPlugins(...) set to createApp({ plugins }).`
|
|
22805
23400
|
);
|
|
22806
23401
|
}
|
|
22807
23402
|
function validatePluginRegistrations(registrations) {
|
|
@@ -22817,6 +23412,51 @@ function validatePluginRegistrations(registrations) {
|
|
|
22817
23412
|
}
|
|
22818
23413
|
}
|
|
22819
23414
|
}
|
|
23415
|
+
function validatePluginEgressCredentialHooks(registrations) {
|
|
23416
|
+
const plugins = new Map(
|
|
23417
|
+
registrations.map((registration) => [registration.name, registration])
|
|
23418
|
+
);
|
|
23419
|
+
for (const provider of getPluginProviders()) {
|
|
23420
|
+
const hooks = plugins.get(provider.manifest.name)?.hooks;
|
|
23421
|
+
const hasGrantHook = Boolean(hooks?.grantForEgress);
|
|
23422
|
+
const hasIssueHook = Boolean(hooks?.issueCredential);
|
|
23423
|
+
const hasGenericCredentials = Boolean(
|
|
23424
|
+
provider.manifest.credentials || provider.manifest.apiHeaders
|
|
23425
|
+
);
|
|
23426
|
+
const hasDomains = Boolean(provider.manifest.domains?.length);
|
|
23427
|
+
const hasHookManagedOAuth = Boolean(
|
|
23428
|
+
provider.manifest.oauth && !provider.manifest.credentials
|
|
23429
|
+
);
|
|
23430
|
+
if (!hasGrantHook && !hasIssueHook) {
|
|
23431
|
+
if (hasDomains && !hasGenericCredentials) {
|
|
23432
|
+
throw new Error(
|
|
23433
|
+
`Plugin "${provider.manifest.name}" manifest.domains requires egress credential hooks when no generic credentials or apiHeaders are configured.`
|
|
23434
|
+
);
|
|
23435
|
+
}
|
|
23436
|
+
if (hasHookManagedOAuth) {
|
|
23437
|
+
throw new Error(
|
|
23438
|
+
`Plugin "${provider.manifest.name}" manifest.oauth without oauth-bearer credentials requires egress credential hooks.`
|
|
23439
|
+
);
|
|
23440
|
+
}
|
|
23441
|
+
continue;
|
|
23442
|
+
}
|
|
23443
|
+
if (!hasGrantHook || !hasIssueHook) {
|
|
23444
|
+
throw new Error(
|
|
23445
|
+
`Plugin "${provider.manifest.name}" egress credential hooks must include both grantForEgress and issueCredential.`
|
|
23446
|
+
);
|
|
23447
|
+
}
|
|
23448
|
+
if (hasGenericCredentials) {
|
|
23449
|
+
throw new Error(
|
|
23450
|
+
`Plugin "${provider.manifest.name}" egress credential hooks must use manifest.domains instead of generic credentials or apiHeaders.`
|
|
23451
|
+
);
|
|
23452
|
+
}
|
|
23453
|
+
if (!hasDomains) {
|
|
23454
|
+
throw new Error(
|
|
23455
|
+
`Plugin "${provider.manifest.name}" egress credential hooks require manifest.domains to list sandbox egress hosts.`
|
|
23456
|
+
);
|
|
23457
|
+
}
|
|
23458
|
+
}
|
|
23459
|
+
}
|
|
22820
23460
|
function mountAgentPluginRoutes(app, routes) {
|
|
22821
23461
|
for (const route of routes) {
|
|
22822
23462
|
const handler = (c) => route.handler(c.req.raw);
|
|
@@ -22834,12 +23474,12 @@ function mountAgentPluginRoutes(app, routes) {
|
|
|
22834
23474
|
async function createApp(options) {
|
|
22835
23475
|
const virtualConfig = await resolveVirtualConfig();
|
|
22836
23476
|
const configuredPlugins = options?.plugins ?? virtualConfig?.pluginSet;
|
|
22837
|
-
const agentPlugins =
|
|
23477
|
+
const agentPlugins = pluginHookRegistrationsFromPluginSet(configuredPlugins);
|
|
22838
23478
|
const pluginConfig = configuredPlugins ? pluginCatalogConfigFromPluginSet(configuredPlugins) : virtualConfig?.plugins ?? resolveEnvPluginCatalogConfig();
|
|
22839
23479
|
if (configuredPlugins) {
|
|
22840
23480
|
validateBuildIncludesPluginPackages(pluginConfig, virtualConfig);
|
|
22841
23481
|
}
|
|
22842
|
-
|
|
23482
|
+
validateBuildIncludesPluginHookRegistrations(agentPlugins, virtualConfig);
|
|
22843
23483
|
validateAgentPlugins(agentPlugins);
|
|
22844
23484
|
const shouldValidatePluginCatalog = hasConfiguredPluginCatalog(pluginConfig) || Boolean(configuredPlugins?.registrations.length) || Boolean(Object.keys(options?.configDefaults ?? {}).length);
|
|
22845
23485
|
const previousPluginCatalogConfig = setPluginCatalogConfig(pluginConfig);
|
|
@@ -22855,6 +23495,9 @@ async function createApp(options) {
|
|
|
22855
23495
|
if (shouldValidatePluginCatalog) {
|
|
22856
23496
|
getPluginCatalogSignature();
|
|
22857
23497
|
validatePluginRegistrations(configuredPlugins?.registrations ?? []);
|
|
23498
|
+
validatePluginEgressCredentialHooks(
|
|
23499
|
+
configuredPlugins?.registrations ?? []
|
|
23500
|
+
);
|
|
22858
23501
|
}
|
|
22859
23502
|
agentPluginRoutes = getAgentPluginRoutes();
|
|
22860
23503
|
} catch (error) {
|
|
@@ -22892,9 +23535,17 @@ async function createApp(options) {
|
|
|
22892
23535
|
return POST(c.req.raw, waitUntil);
|
|
22893
23536
|
});
|
|
22894
23537
|
let agentContinuePOST;
|
|
23538
|
+
let conversationWorkOptions;
|
|
23539
|
+
const getConversationWorkOptions = () => {
|
|
23540
|
+
conversationWorkOptions ??= options?.conversationWork ?? getProductionConversationWorkOptions();
|
|
23541
|
+
return conversationWorkOptions;
|
|
23542
|
+
};
|
|
23543
|
+
if (process.env.NODE_ENV === "development") {
|
|
23544
|
+
registerVercelConversationWorkDevConsumer(getConversationWorkOptions());
|
|
23545
|
+
}
|
|
22895
23546
|
app.post("/api/internal/agent/continue", (c) => {
|
|
22896
23547
|
agentContinuePOST ??= createVercelConversationWorkCallback(
|
|
22897
|
-
|
|
23548
|
+
getConversationWorkOptions()
|
|
22898
23549
|
);
|
|
22899
23550
|
return agentContinuePOST(c.req.raw);
|
|
22900
23551
|
});
|