@sentry/junior 0.80.1 → 0.82.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/{agent-hooks-ZGTDOXQY.js → agent-hooks-5ZRILRC3.js} +1 -1
- package/dist/app.js +7 -905
- package/dist/chat/agent-dispatch/runner.d.ts +1 -1
- package/dist/chat/app/services.d.ts +1 -1
- package/dist/chat/egress/credentialed.d.ts +57 -0
- package/dist/chat/egress/plugin.d.ts +24 -0
- package/dist/chat/plugins/credential-hooks.d.ts +2 -0
- package/dist/chat/respond.d.ts +1 -1
- package/dist/chat/sandbox/{egress-credentials.d.ts → egress/credentials.d.ts} +28 -5
- package/dist/chat/sandbox/egress/policy.d.ts +32 -0
- package/dist/chat/sandbox/egress/proxy.d.ts +35 -0
- package/dist/chat/sandbox/{egress-session.d.ts → egress/session.d.ts} +41 -11
- package/dist/chat/sandbox/sandbox.d.ts +2 -2
- package/dist/chat/tools/types.d.ts +2 -1
- package/dist/{chunk-W2QGQCKG.js → chunk-NC6LR6U4.js} +1062 -14
- package/dist/{chunk-Y3YUOEAZ.js → chunk-TR2G37II.js} +14 -5
- package/dist/{chunk-RV5RYIJW.js → chunk-UXPG6ZIN.js} +28 -0
- package/dist/cli/chat.js +3 -3
- package/dist/cli/env.js +7 -1
- package/dist/cli/init.js +1 -0
- package/dist/cli/main.js +1 -1
- package/dist/cli/plugins.js +1 -1
- package/dist/handlers/agent-dispatch.d.ts +1 -1
- package/dist/handlers/sandbox-egress-proxy.d.ts +2 -2
- package/dist/reporting.js +1 -1
- package/dist/{runner-JOVPVMIH.js → runner-VRFJLG2M.js} +2 -2
- package/package.json +6 -5
- package/dist/chat/sandbox/egress-policy.d.ts +0 -15
- package/dist/chat/sandbox/egress-proxy.d.ts +0 -19
- /package/dist/chat/sandbox/{egress-oidc.d.ts → egress/oidc.d.ts} +0 -0
- /package/dist/chat/sandbox/{egress-schemas.d.ts → egress/schemas.d.ts} +0 -0
- /package/dist/chat/sandbox/{egress-tracing.d.ts → egress/tracing.d.ts} +0 -0
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
getPlugins,
|
|
43
43
|
getSlackToolContext,
|
|
44
44
|
resolveChannelCapabilities
|
|
45
|
-
} from "./chunk-
|
|
45
|
+
} from "./chunk-TR2G37II.js";
|
|
46
46
|
import {
|
|
47
47
|
createPluginLogger,
|
|
48
48
|
createPluginState
|
|
@@ -122,6 +122,7 @@ import {
|
|
|
122
122
|
parseSkillInvocation
|
|
123
123
|
} from "./chunk-WCXVEQPI.js";
|
|
124
124
|
import {
|
|
125
|
+
CredentialUnavailableError,
|
|
125
126
|
credentialContextSchema,
|
|
126
127
|
pluginCatalogRuntime,
|
|
127
128
|
resolveAuthTokenPlaceholder,
|
|
@@ -191,7 +192,7 @@ function getConfigDefaults() {
|
|
|
191
192
|
return cloneDefaults(installDefaults);
|
|
192
193
|
}
|
|
193
194
|
|
|
194
|
-
// src/chat/sandbox/egress
|
|
195
|
+
// src/chat/sandbox/egress/tracing.ts
|
|
195
196
|
function isValidDomainPattern(domain) {
|
|
196
197
|
if (domain.includes("*")) {
|
|
197
198
|
return domain.startsWith("*.") && domain.indexOf("*", 1) === -1;
|
|
@@ -882,6 +883,17 @@ function parsePrefixFlag(extras) {
|
|
|
882
883
|
error: "jr-rpc config list accepts optional --prefix <value>\n"
|
|
883
884
|
};
|
|
884
885
|
}
|
|
886
|
+
function buildUnknownKeyWarning(key) {
|
|
887
|
+
const knownKeys = pluginCatalogRuntime.getProviders().flatMap((plugin) => [...plugin.manifest.configKeys]);
|
|
888
|
+
if (knownKeys.includes(key)) {
|
|
889
|
+
return "";
|
|
890
|
+
}
|
|
891
|
+
const prefix = key.includes(".") ? key.slice(0, key.indexOf(".") + 1) : "";
|
|
892
|
+
const suggestions = prefix ? knownKeys.filter((k) => k.startsWith(prefix)) : knownKeys;
|
|
893
|
+
const hint = suggestions.length > 0 ? `Known keys with prefix "${prefix}": ${suggestions.join(", ")}` : `Known config keys: ${knownKeys.join(", ")}`;
|
|
894
|
+
return `Warning: "${key}" is not a recognized config key. ${hint}
|
|
895
|
+
`;
|
|
896
|
+
}
|
|
885
897
|
async function handleConfigCommand(args, deps) {
|
|
886
898
|
const usage = [
|
|
887
899
|
"jr-rpc config get <key>",
|
|
@@ -960,6 +972,7 @@ ${usage}
|
|
|
960
972
|
});
|
|
961
973
|
}
|
|
962
974
|
}
|
|
975
|
+
const unknownKeyWarning = buildUnknownKeyWarning(key);
|
|
963
976
|
try {
|
|
964
977
|
const entry = await configuration.set({
|
|
965
978
|
key,
|
|
@@ -989,6 +1002,7 @@ ${usage}
|
|
|
989
1002
|
updatedBy: entry.updatedBy,
|
|
990
1003
|
source: entry.source
|
|
991
1004
|
},
|
|
1005
|
+
stderr: unknownKeyWarning,
|
|
992
1006
|
exitCode: 0
|
|
993
1007
|
});
|
|
994
1008
|
} catch (error) {
|
|
@@ -4191,7 +4205,7 @@ function createOperationKey(toolName, input) {
|
|
|
4191
4205
|
// src/chat/tools/slack/channel-post-message.ts
|
|
4192
4206
|
function createSlackChannelPostMessageTool(context, state) {
|
|
4193
4207
|
return tool({
|
|
4194
|
-
description: "Post a message
|
|
4208
|
+
description: "Post a new top-level message to the current Slack channel. Use only when the user explicitly asks to post/send/share/say something to the channel. Do not use for thread replies, inline @mentions, or pinging mentioned users.",
|
|
4195
4209
|
inputSchema: Type14.Object({
|
|
4196
4210
|
text: Type14.String({
|
|
4197
4211
|
minLength: 1,
|
|
@@ -7149,10 +7163,10 @@ async function startOAuthFlow(provider, input) {
|
|
|
7149
7163
|
};
|
|
7150
7164
|
}
|
|
7151
7165
|
|
|
7152
|
-
// src/chat/sandbox/egress
|
|
7166
|
+
// src/chat/sandbox/egress/session.ts
|
|
7153
7167
|
import { createHmac, randomUUID, timingSafeEqual } from "crypto";
|
|
7154
7168
|
|
|
7155
|
-
// src/chat/sandbox/egress
|
|
7169
|
+
// src/chat/sandbox/egress/schemas.ts
|
|
7156
7170
|
import { z } from "zod";
|
|
7157
7171
|
import {
|
|
7158
7172
|
pluginAuthorizationSchema,
|
|
@@ -7217,7 +7231,7 @@ function parseSandboxEgressPermissionDeniedSignal(value) {
|
|
|
7217
7231
|
return result.success ? result.data : void 0;
|
|
7218
7232
|
}
|
|
7219
7233
|
|
|
7220
|
-
// src/chat/sandbox/egress
|
|
7234
|
+
// src/chat/sandbox/egress/session.ts
|
|
7221
7235
|
var SANDBOX_EGRESS_PROXY_PATH = "/api/internal/sandbox-egress";
|
|
7222
7236
|
var SANDBOX_EGRESS_TOKEN_VERSION = "v1";
|
|
7223
7237
|
var SANDBOX_EGRESS_HMAC_CONTEXT = "junior.sandbox_egress.v1";
|
|
@@ -7419,7 +7433,7 @@ async function consumeSandboxEgressPermissionDeniedSignal(egressId) {
|
|
|
7419
7433
|
return signal;
|
|
7420
7434
|
}
|
|
7421
7435
|
|
|
7422
|
-
// src/chat/sandbox/egress
|
|
7436
|
+
// src/chat/sandbox/egress/policy.ts
|
|
7423
7437
|
function matchesSandboxEgressDomain(host, domain) {
|
|
7424
7438
|
return host.toLowerCase() === domain.toLowerCase();
|
|
7425
7439
|
}
|
|
@@ -11199,6 +11213,1028 @@ function createMcpAuthOrchestration(input) {
|
|
|
11199
11213
|
};
|
|
11200
11214
|
}
|
|
11201
11215
|
|
|
11216
|
+
// src/chat/egress/plugin.ts
|
|
11217
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
11218
|
+
|
|
11219
|
+
// src/chat/plugins/credential-hooks.ts
|
|
11220
|
+
import {
|
|
11221
|
+
pluginAuthorizationSchema as pluginAuthorizationSchema2,
|
|
11222
|
+
pluginCredentialResultSchema,
|
|
11223
|
+
pluginGrantSchema as pluginGrantSchema2,
|
|
11224
|
+
pluginProviderAccountSchema as pluginProviderAccountSchema2
|
|
11225
|
+
} from "@sentry/junior-plugin-api";
|
|
11226
|
+
function parseSchema(schema, value, message) {
|
|
11227
|
+
const result = schema.safeParse(value);
|
|
11228
|
+
if (!result.success) {
|
|
11229
|
+
throw new Error(message);
|
|
11230
|
+
}
|
|
11231
|
+
return result.data;
|
|
11232
|
+
}
|
|
11233
|
+
function parseAuthorization(value, pluginName) {
|
|
11234
|
+
if (value === void 0) {
|
|
11235
|
+
return void 0;
|
|
11236
|
+
}
|
|
11237
|
+
const authorization = parseSchema(
|
|
11238
|
+
pluginAuthorizationSchema2,
|
|
11239
|
+
value,
|
|
11240
|
+
`Plugin "${pluginName}" grant authorization is invalid`
|
|
11241
|
+
);
|
|
11242
|
+
if (authorization.provider !== pluginName) {
|
|
11243
|
+
throw new Error(
|
|
11244
|
+
`Plugin "${pluginName}" grant authorization provider must match the issuing plugin`
|
|
11245
|
+
);
|
|
11246
|
+
}
|
|
11247
|
+
return authorization;
|
|
11248
|
+
}
|
|
11249
|
+
function parseGrant(value, pluginName) {
|
|
11250
|
+
return parseSchema(
|
|
11251
|
+
pluginGrantSchema2,
|
|
11252
|
+
value,
|
|
11253
|
+
`Plugin "${pluginName}" grantForEgress returned an invalid grant`
|
|
11254
|
+
);
|
|
11255
|
+
}
|
|
11256
|
+
function pluginFor(provider) {
|
|
11257
|
+
return getPlugins().find((candidate) => candidate.manifest.name === provider);
|
|
11258
|
+
}
|
|
11259
|
+
function basePluginContext(plugin) {
|
|
11260
|
+
const pluginName = plugin.manifest.name;
|
|
11261
|
+
return {
|
|
11262
|
+
plugin: { name: pluginName },
|
|
11263
|
+
log: createPluginLogger(pluginName),
|
|
11264
|
+
db: getDb()
|
|
11265
|
+
};
|
|
11266
|
+
}
|
|
11267
|
+
function parseCredentialResult(value, pluginName) {
|
|
11268
|
+
const result = parseSchema(
|
|
11269
|
+
pluginCredentialResultSchema,
|
|
11270
|
+
value,
|
|
11271
|
+
`Plugin "${pluginName}" issueCredential result is invalid`
|
|
11272
|
+
);
|
|
11273
|
+
if (result.type === "lease") {
|
|
11274
|
+
parseAuthorization(result.lease.authorization, pluginName);
|
|
11275
|
+
return result;
|
|
11276
|
+
}
|
|
11277
|
+
if (result.type === "unavailable") {
|
|
11278
|
+
return result;
|
|
11279
|
+
}
|
|
11280
|
+
parseAuthorization(result.authorization, pluginName);
|
|
11281
|
+
return result;
|
|
11282
|
+
}
|
|
11283
|
+
async function selectPluginGrant(input) {
|
|
11284
|
+
const plugin = pluginFor(input.provider);
|
|
11285
|
+
const hook = plugin?.hooks?.grantForEgress;
|
|
11286
|
+
if (!plugin || !hook) {
|
|
11287
|
+
return void 0;
|
|
11288
|
+
}
|
|
11289
|
+
const result = await hook({
|
|
11290
|
+
...basePluginContext(plugin),
|
|
11291
|
+
request: {
|
|
11292
|
+
...input.bodyText !== void 0 ? { bodyText: input.bodyText } : {},
|
|
11293
|
+
method: input.method,
|
|
11294
|
+
...input.operation ? { operation: input.operation } : {},
|
|
11295
|
+
url: input.upstreamUrl.toString()
|
|
11296
|
+
}
|
|
11297
|
+
});
|
|
11298
|
+
return result === void 0 ? void 0 : parseGrant(result, plugin.manifest.name);
|
|
11299
|
+
}
|
|
11300
|
+
async function onPluginEgressResponse(input) {
|
|
11301
|
+
const plugin = pluginFor(input.provider);
|
|
11302
|
+
const hook = plugin?.hooks?.onEgressResponse;
|
|
11303
|
+
if (!plugin || !hook) {
|
|
11304
|
+
return {};
|
|
11305
|
+
}
|
|
11306
|
+
let permissionDenied;
|
|
11307
|
+
await hook({
|
|
11308
|
+
...basePluginContext(plugin),
|
|
11309
|
+
grant: input.grant,
|
|
11310
|
+
permissionDenied(message) {
|
|
11311
|
+
const trimmed = message.trim();
|
|
11312
|
+
if (!trimmed) {
|
|
11313
|
+
throw new Error(
|
|
11314
|
+
`Plugin "${plugin.manifest.name}" onEgressResponse permissionDenied message is empty`
|
|
11315
|
+
);
|
|
11316
|
+
}
|
|
11317
|
+
permissionDenied = { message: trimmed };
|
|
11318
|
+
},
|
|
11319
|
+
request: {
|
|
11320
|
+
method: input.method,
|
|
11321
|
+
...input.operation ? { operation: input.operation } : {},
|
|
11322
|
+
url: input.upstreamUrl.toString()
|
|
11323
|
+
},
|
|
11324
|
+
response: input.response
|
|
11325
|
+
});
|
|
11326
|
+
return permissionDenied ? { permissionDenied } : {};
|
|
11327
|
+
}
|
|
11328
|
+
function hasEgressCredentialHooks(provider) {
|
|
11329
|
+
const hooks = pluginFor(provider)?.hooks;
|
|
11330
|
+
return Boolean(hooks?.grantForEgress || hooks?.issueCredential);
|
|
11331
|
+
}
|
|
11332
|
+
async function resolvePluginOAuthAccount(input) {
|
|
11333
|
+
const plugin = pluginFor(input.provider);
|
|
11334
|
+
const hook = plugin?.hooks?.resolveOAuthAccount;
|
|
11335
|
+
if (!plugin || !hook) {
|
|
11336
|
+
return void 0;
|
|
11337
|
+
}
|
|
11338
|
+
const account = await hook({
|
|
11339
|
+
...basePluginContext(plugin),
|
|
11340
|
+
tokens: input.tokens
|
|
11341
|
+
});
|
|
11342
|
+
return account === void 0 ? void 0 : parseSchema(
|
|
11343
|
+
pluginProviderAccountSchema2,
|
|
11344
|
+
account,
|
|
11345
|
+
`Plugin "${plugin.manifest.name}" resolveOAuthAccount returned an invalid account`
|
|
11346
|
+
);
|
|
11347
|
+
}
|
|
11348
|
+
async function issuePluginCredential(input) {
|
|
11349
|
+
const plugin = pluginFor(input.provider);
|
|
11350
|
+
const hook = plugin?.hooks?.issueCredential;
|
|
11351
|
+
if (!plugin || !hook) {
|
|
11352
|
+
throw new Error(`Plugin "${input.provider}" has no issueCredential hook`);
|
|
11353
|
+
}
|
|
11354
|
+
const currentUserId = input.actor.type === "user" ? input.actor.userId : void 0;
|
|
11355
|
+
const credentialSubjectUserId = input.credentialSubject?.userId;
|
|
11356
|
+
const result = await hook({
|
|
11357
|
+
...basePluginContext(plugin),
|
|
11358
|
+
actor: input.actor,
|
|
11359
|
+
grant: input.grant,
|
|
11360
|
+
...input.credentialSubject ? { credentialSubject: input.credentialSubject } : {},
|
|
11361
|
+
tokens: {
|
|
11362
|
+
...currentUserId ? {
|
|
11363
|
+
currentUser: {
|
|
11364
|
+
userId: currentUserId,
|
|
11365
|
+
get: async () => await input.userTokenStore.get(
|
|
11366
|
+
currentUserId,
|
|
11367
|
+
plugin.manifest.name
|
|
11368
|
+
),
|
|
11369
|
+
set: async (tokens) => {
|
|
11370
|
+
await input.userTokenStore.set(
|
|
11371
|
+
currentUserId,
|
|
11372
|
+
plugin.manifest.name,
|
|
11373
|
+
tokens
|
|
11374
|
+
);
|
|
11375
|
+
},
|
|
11376
|
+
withRefresh: async (callback) => await input.userTokenStore.withRefresh(
|
|
11377
|
+
currentUserId,
|
|
11378
|
+
plugin.manifest.name,
|
|
11379
|
+
callback
|
|
11380
|
+
)
|
|
11381
|
+
}
|
|
11382
|
+
} : {},
|
|
11383
|
+
...credentialSubjectUserId ? {
|
|
11384
|
+
credentialSubject: {
|
|
11385
|
+
userId: credentialSubjectUserId,
|
|
11386
|
+
get: async () => await input.userTokenStore.get(
|
|
11387
|
+
credentialSubjectUserId,
|
|
11388
|
+
plugin.manifest.name
|
|
11389
|
+
),
|
|
11390
|
+
set: async (tokens) => {
|
|
11391
|
+
await input.userTokenStore.set(
|
|
11392
|
+
credentialSubjectUserId,
|
|
11393
|
+
plugin.manifest.name,
|
|
11394
|
+
tokens
|
|
11395
|
+
);
|
|
11396
|
+
},
|
|
11397
|
+
withRefresh: async (callback) => await input.userTokenStore.withRefresh(
|
|
11398
|
+
credentialSubjectUserId,
|
|
11399
|
+
plugin.manifest.name,
|
|
11400
|
+
callback
|
|
11401
|
+
)
|
|
11402
|
+
}
|
|
11403
|
+
} : {}
|
|
11404
|
+
}
|
|
11405
|
+
});
|
|
11406
|
+
return parseCredentialResult(result, plugin.manifest.name);
|
|
11407
|
+
}
|
|
11408
|
+
|
|
11409
|
+
// src/chat/sandbox/egress/credentials.ts
|
|
11410
|
+
var HTTP_READ_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
|
|
11411
|
+
var SandboxEgressCredentialError = class extends Error {
|
|
11412
|
+
authorization;
|
|
11413
|
+
grant;
|
|
11414
|
+
kind;
|
|
11415
|
+
provider;
|
|
11416
|
+
constructor(input) {
|
|
11417
|
+
super(input.message);
|
|
11418
|
+
this.name = "SandboxEgressCredentialError";
|
|
11419
|
+
this.authorization = input.authorization;
|
|
11420
|
+
this.grant = input.grant;
|
|
11421
|
+
this.kind = input.kind;
|
|
11422
|
+
this.provider = input.provider;
|
|
11423
|
+
}
|
|
11424
|
+
};
|
|
11425
|
+
function defaultGrantForProvider(input) {
|
|
11426
|
+
const access = HTTP_READ_METHODS.has(
|
|
11427
|
+
input.method.toUpperCase()
|
|
11428
|
+
) ? "read" : "write";
|
|
11429
|
+
return {
|
|
11430
|
+
source: "broker",
|
|
11431
|
+
grant: {
|
|
11432
|
+
name: "default",
|
|
11433
|
+
access,
|
|
11434
|
+
reason: `sandbox-egress:${input.provider}:${access}`
|
|
11435
|
+
}
|
|
11436
|
+
};
|
|
11437
|
+
}
|
|
11438
|
+
function oauthAuthorizationForProvider(provider) {
|
|
11439
|
+
const oauth = pluginCatalogRuntime.getOAuthConfig(provider);
|
|
11440
|
+
return oauth ? {
|
|
11441
|
+
type: "oauth",
|
|
11442
|
+
provider,
|
|
11443
|
+
...oauth.scope ? { scope: oauth.scope } : {}
|
|
11444
|
+
} : void 0;
|
|
11445
|
+
}
|
|
11446
|
+
function credentialSubjectFromContext(context) {
|
|
11447
|
+
return "subject" in context.credentials && context.credentials.subject ? { type: "user", userId: context.credentials.subject.userId } : void 0;
|
|
11448
|
+
}
|
|
11449
|
+
function assertLeaseTransformsOwnedByProvider(provider, lease) {
|
|
11450
|
+
for (const transform of lease.headerTransforms) {
|
|
11451
|
+
if (resolveSandboxEgressProviderForHost(transform.domain) !== provider) {
|
|
11452
|
+
throw new Error(
|
|
11453
|
+
`Credential lease for ${provider} included header transform for unowned domain ${transform.domain}`
|
|
11454
|
+
);
|
|
11455
|
+
}
|
|
11456
|
+
}
|
|
11457
|
+
}
|
|
11458
|
+
async function selectSandboxEgressGrant(input) {
|
|
11459
|
+
if (!hasEgressCredentialHooks(input.provider)) {
|
|
11460
|
+
return defaultGrantForProvider(input);
|
|
11461
|
+
}
|
|
11462
|
+
const pluginGrant = await selectPluginGrant({
|
|
11463
|
+
...input.bodyText !== void 0 ? { bodyText: input.bodyText } : {},
|
|
11464
|
+
...input.operation ? { operation: input.operation } : {},
|
|
11465
|
+
provider: input.provider,
|
|
11466
|
+
method: input.method,
|
|
11467
|
+
upstreamUrl: input.upstreamUrl
|
|
11468
|
+
});
|
|
11469
|
+
if (!pluginGrant) {
|
|
11470
|
+
throw new Error(
|
|
11471
|
+
`Plugin "${input.provider}" grantForEgress must return a grant for sandbox egress`
|
|
11472
|
+
);
|
|
11473
|
+
}
|
|
11474
|
+
return { source: "plugin", grant: pluginGrant };
|
|
11475
|
+
}
|
|
11476
|
+
function authorizationForSandboxEgressGrant(provider, selection) {
|
|
11477
|
+
return selection.source === "broker" ? oauthAuthorizationForProvider(provider) : void 0;
|
|
11478
|
+
}
|
|
11479
|
+
async function sandboxEgressCredentialLease(provider, selection, context) {
|
|
11480
|
+
const { grant } = selection;
|
|
11481
|
+
const cached = await getSandboxEgressCredentialLease(
|
|
11482
|
+
provider,
|
|
11483
|
+
grant.name,
|
|
11484
|
+
context
|
|
11485
|
+
);
|
|
11486
|
+
if (cached) {
|
|
11487
|
+
if (selection.source === "plugin" && cached.grant.access !== grant.access) {
|
|
11488
|
+
throw new Error(
|
|
11489
|
+
`Cached credential lease for ${provider}/${grant.name} has ${cached.grant.access} access, but ${grant.access} was selected`
|
|
11490
|
+
);
|
|
11491
|
+
}
|
|
11492
|
+
return {
|
|
11493
|
+
...cached,
|
|
11494
|
+
grant
|
|
11495
|
+
};
|
|
11496
|
+
}
|
|
11497
|
+
let lease;
|
|
11498
|
+
if (selection.source === "plugin") {
|
|
11499
|
+
const credentialSubject = credentialSubjectFromContext(context);
|
|
11500
|
+
const pluginResult = await issuePluginCredential({
|
|
11501
|
+
provider,
|
|
11502
|
+
grant,
|
|
11503
|
+
actor: context.credentials.actor,
|
|
11504
|
+
...credentialSubject ? { credentialSubject } : {},
|
|
11505
|
+
userTokenStore: createUserTokenStore()
|
|
11506
|
+
});
|
|
11507
|
+
if (pluginResult.type === "needed") {
|
|
11508
|
+
throw new SandboxEgressCredentialError({
|
|
11509
|
+
provider,
|
|
11510
|
+
grant,
|
|
11511
|
+
kind: "auth_required",
|
|
11512
|
+
authorization: pluginResult.authorization,
|
|
11513
|
+
message: pluginResult.message
|
|
11514
|
+
});
|
|
11515
|
+
}
|
|
11516
|
+
if (pluginResult.type === "unavailable") {
|
|
11517
|
+
throw new SandboxEgressCredentialError({
|
|
11518
|
+
provider,
|
|
11519
|
+
grant,
|
|
11520
|
+
kind: "unavailable",
|
|
11521
|
+
message: pluginResult.message
|
|
11522
|
+
});
|
|
11523
|
+
}
|
|
11524
|
+
lease = pluginResult.lease;
|
|
11525
|
+
} else {
|
|
11526
|
+
try {
|
|
11527
|
+
lease = await issueProviderCredentialLease({
|
|
11528
|
+
context: context.credentials,
|
|
11529
|
+
provider,
|
|
11530
|
+
reason: grant.reason ?? `sandbox-egress:${provider}:default`
|
|
11531
|
+
});
|
|
11532
|
+
} catch (error) {
|
|
11533
|
+
if (error instanceof CredentialUnavailableError) {
|
|
11534
|
+
throw new SandboxEgressCredentialError({
|
|
11535
|
+
provider,
|
|
11536
|
+
grant,
|
|
11537
|
+
kind: "auth_required",
|
|
11538
|
+
authorization: authorizationForSandboxEgressGrant(
|
|
11539
|
+
provider,
|
|
11540
|
+
selection
|
|
11541
|
+
),
|
|
11542
|
+
message: error.message
|
|
11543
|
+
});
|
|
11544
|
+
}
|
|
11545
|
+
throw error;
|
|
11546
|
+
}
|
|
11547
|
+
}
|
|
11548
|
+
const headerTransforms = lease.headerTransforms ?? [];
|
|
11549
|
+
if (headerTransforms.length === 0) {
|
|
11550
|
+
throw new Error(
|
|
11551
|
+
`Credential lease for ${provider} did not include header transforms`
|
|
11552
|
+
);
|
|
11553
|
+
}
|
|
11554
|
+
const leaseExpiresAtMs = Date.parse(lease.expiresAt);
|
|
11555
|
+
if (!Number.isFinite(leaseExpiresAtMs) || leaseExpiresAtMs <= Date.now()) {
|
|
11556
|
+
throw new Error(`Credential lease for ${provider} is expired`);
|
|
11557
|
+
}
|
|
11558
|
+
const authorization = selection.source === "broker" ? oauthAuthorizationForProvider(provider) : lease.authorization;
|
|
11559
|
+
const cachedLease = {
|
|
11560
|
+
provider,
|
|
11561
|
+
grant,
|
|
11562
|
+
...lease.account ? { account: lease.account } : {},
|
|
11563
|
+
...authorization ? { authorization } : {},
|
|
11564
|
+
expiresAt: lease.expiresAt,
|
|
11565
|
+
headerTransforms
|
|
11566
|
+
};
|
|
11567
|
+
assertLeaseTransformsOwnedByProvider(provider, cachedLease);
|
|
11568
|
+
await setSandboxEgressCredentialLease(context, cachedLease);
|
|
11569
|
+
return cachedLease;
|
|
11570
|
+
}
|
|
11571
|
+
function hasSandboxEgressLeaseTransformForHost(lease, host) {
|
|
11572
|
+
return lease.headerTransforms.some(
|
|
11573
|
+
(transform) => matchesSandboxEgressDomain(host, transform.domain)
|
|
11574
|
+
);
|
|
11575
|
+
}
|
|
11576
|
+
|
|
11577
|
+
// src/chat/egress/credentialed.ts
|
|
11578
|
+
import {
|
|
11579
|
+
EgressAuthRequired,
|
|
11580
|
+
EgressPolicyDenied
|
|
11581
|
+
} from "@sentry/junior-plugin-api";
|
|
11582
|
+
var HOP_BY_HOP_HEADERS = /* @__PURE__ */ new Set([
|
|
11583
|
+
"connection",
|
|
11584
|
+
"host",
|
|
11585
|
+
"keep-alive",
|
|
11586
|
+
"proxy-authenticate",
|
|
11587
|
+
"proxy-authorization",
|
|
11588
|
+
"te",
|
|
11589
|
+
"trailer",
|
|
11590
|
+
"transfer-encoding",
|
|
11591
|
+
"upgrade"
|
|
11592
|
+
]);
|
|
11593
|
+
var PROXY_ONLY_HEADERS = /* @__PURE__ */ new Set([
|
|
11594
|
+
"vercel-sandbox-oidc-token",
|
|
11595
|
+
"vercel-forwarded-host",
|
|
11596
|
+
"vercel-forwarded-scheme",
|
|
11597
|
+
"vercel-forwarded-port",
|
|
11598
|
+
"vercel-forwarded-path"
|
|
11599
|
+
]);
|
|
11600
|
+
var TRACE_PROPAGATION_HEADERS = /* @__PURE__ */ new Set([
|
|
11601
|
+
"baggage",
|
|
11602
|
+
"sentry-trace",
|
|
11603
|
+
"traceparent"
|
|
11604
|
+
]);
|
|
11605
|
+
var DECODED_RESPONSE_HEADERS = /* @__PURE__ */ new Set([
|
|
11606
|
+
"content-encoding",
|
|
11607
|
+
"content-length"
|
|
11608
|
+
]);
|
|
11609
|
+
var UPSTREAM_TOKEN_REJECTION_STATUS = 401;
|
|
11610
|
+
var UPSTREAM_PERMISSION_REJECTION_STATUS = 403;
|
|
11611
|
+
var GRANT_SELECTION_BODY_TEXT_LIMIT_BYTES = 64 * 1024;
|
|
11612
|
+
var RESPONSE_BODY_TEXT_LIMIT_BYTES = 64 * 1024;
|
|
11613
|
+
function authRequiredResponse(input) {
|
|
11614
|
+
return new Response(
|
|
11615
|
+
`junior-auth-required provider=${input.provider} grant=${input.grant.name} access=${input.grant.access} 401 unauthorized
|
|
11616
|
+
${input.message}`,
|
|
11617
|
+
{
|
|
11618
|
+
status: 401,
|
|
11619
|
+
headers: {
|
|
11620
|
+
"content-type": "text/plain; charset=utf-8",
|
|
11621
|
+
"cache-control": "no-store"
|
|
11622
|
+
}
|
|
11623
|
+
}
|
|
11624
|
+
);
|
|
11625
|
+
}
|
|
11626
|
+
function policyDeniedResponse(error) {
|
|
11627
|
+
return Response.json(
|
|
11628
|
+
{ error: error.message },
|
|
11629
|
+
{
|
|
11630
|
+
status: 403,
|
|
11631
|
+
headers: {
|
|
11632
|
+
"cache-control": "no-store"
|
|
11633
|
+
}
|
|
11634
|
+
}
|
|
11635
|
+
);
|
|
11636
|
+
}
|
|
11637
|
+
function shouldLogSandboxEgressInfo() {
|
|
11638
|
+
const environment = (process.env.SENTRY_ENVIRONMENT ?? process.env.VERCEL_ENV ?? process.env.NODE_ENV ?? "").trim().toLowerCase();
|
|
11639
|
+
return environment !== "production";
|
|
11640
|
+
}
|
|
11641
|
+
function egressAttributes(input) {
|
|
11642
|
+
return {
|
|
11643
|
+
...input.egressId ? { "app.sandbox.egress_id": input.egressId } : {},
|
|
11644
|
+
...input.provider ? { "app.provider.name": input.provider } : {},
|
|
11645
|
+
...input.grantName ? { "app.grant.name": input.grantName } : {},
|
|
11646
|
+
...input.grantAccess ? { "app.grant.access": input.grantAccess } : {},
|
|
11647
|
+
...input.grantReason ? { "app.grant.reason": input.grantReason } : {},
|
|
11648
|
+
...input.host ? { "server.address": input.host } : {},
|
|
11649
|
+
...input.method ? { "http.request.method": input.method } : {},
|
|
11650
|
+
...input.path ? { "url.path": input.path } : {},
|
|
11651
|
+
...input.status ? { "http.response.status_code": input.status } : {}
|
|
11652
|
+
};
|
|
11653
|
+
}
|
|
11654
|
+
function redactedProxyPath(pathname) {
|
|
11655
|
+
if (pathname.startsWith(`${SANDBOX_EGRESS_PROXY_PATH}/`)) {
|
|
11656
|
+
return `${SANDBOX_EGRESS_PROXY_PATH}/<token>`;
|
|
11657
|
+
}
|
|
11658
|
+
return pathname;
|
|
11659
|
+
}
|
|
11660
|
+
function routingAttributes(request, upstreamUrl) {
|
|
11661
|
+
const proxyUrl = new URL(request.url);
|
|
11662
|
+
const attributes = {
|
|
11663
|
+
"app.sandbox.egress.proxy_path": redactedProxyPath(proxyUrl.pathname)
|
|
11664
|
+
};
|
|
11665
|
+
if (upstreamUrl) {
|
|
11666
|
+
attributes["app.sandbox.egress.upstream_path"] = upstreamUrl.pathname;
|
|
11667
|
+
const gitService = upstreamUrl.searchParams.get("service");
|
|
11668
|
+
if (upstreamUrl.hostname.toLowerCase() === "github.com" && (gitService === "git-upload-pack" || gitService === "git-receive-pack")) {
|
|
11669
|
+
attributes["app.sandbox.egress.git_service"] = gitService;
|
|
11670
|
+
}
|
|
11671
|
+
}
|
|
11672
|
+
return attributes;
|
|
11673
|
+
}
|
|
11674
|
+
function displayedUpstreamPath(upstreamUrl) {
|
|
11675
|
+
const gitService = upstreamUrl.searchParams.get("service");
|
|
11676
|
+
if (upstreamUrl.hostname.toLowerCase() === "github.com" && (gitService === "git-upload-pack" || gitService === "git-receive-pack")) {
|
|
11677
|
+
return `${upstreamUrl.pathname}?service=${gitService}`;
|
|
11678
|
+
}
|
|
11679
|
+
return upstreamUrl.pathname;
|
|
11680
|
+
}
|
|
11681
|
+
function upstreamPermissionAttributes(provider, upstream) {
|
|
11682
|
+
if (provider !== "github") {
|
|
11683
|
+
return {};
|
|
11684
|
+
}
|
|
11685
|
+
return {
|
|
11686
|
+
"app.github.accepted_permissions": upstream.headers.get("x-accepted-github-permissions") ?? void 0,
|
|
11687
|
+
"app.github.sso": upstream.headers.get("x-github-sso") ?? void 0
|
|
11688
|
+
};
|
|
11689
|
+
}
|
|
11690
|
+
function githubPermissionHeaders(upstream) {
|
|
11691
|
+
const acceptedPermissions = upstream.headers.get(
|
|
11692
|
+
"x-accepted-github-permissions"
|
|
11693
|
+
);
|
|
11694
|
+
const sso = upstream.headers.get("x-github-sso");
|
|
11695
|
+
return {
|
|
11696
|
+
...acceptedPermissions ? { acceptedPermissions } : {},
|
|
11697
|
+
...sso ? { sso } : {}
|
|
11698
|
+
};
|
|
11699
|
+
}
|
|
11700
|
+
function permissionDeniedMessage(provider, grant) {
|
|
11701
|
+
return `${provider} returned HTTP 403 after Junior injected the ${grant.name} grant. Junior forwarded the request; this is not a local runtime block.`;
|
|
11702
|
+
}
|
|
11703
|
+
function isEgressAuthRequired(error) {
|
|
11704
|
+
return error instanceof EgressAuthRequired || error instanceof Error && error.name === "EgressAuthRequired";
|
|
11705
|
+
}
|
|
11706
|
+
function isEgressPolicyDenied(error) {
|
|
11707
|
+
return error instanceof EgressPolicyDenied || error instanceof Error && error.name === "EgressPolicyDenied";
|
|
11708
|
+
}
|
|
11709
|
+
function logSandboxEgressUpstreamRequest(input) {
|
|
11710
|
+
if (!shouldLogSandboxEgressInfo()) {
|
|
11711
|
+
return;
|
|
11712
|
+
}
|
|
11713
|
+
logInfo(
|
|
11714
|
+
"sandbox_egress_upstream_request",
|
|
11715
|
+
{},
|
|
11716
|
+
{
|
|
11717
|
+
...egressAttributes({
|
|
11718
|
+
egressId: input.egressId,
|
|
11719
|
+
grantAccess: input.grantAccess,
|
|
11720
|
+
grantName: input.grantName,
|
|
11721
|
+
grantReason: input.grantReason,
|
|
11722
|
+
host: input.upstreamUrl.hostname,
|
|
11723
|
+
method: input.request.method,
|
|
11724
|
+
path: input.upstreamUrl.pathname,
|
|
11725
|
+
provider: input.provider,
|
|
11726
|
+
status: input.upstream.status
|
|
11727
|
+
}),
|
|
11728
|
+
...routingAttributes(input.request, input.upstreamUrl),
|
|
11729
|
+
"app.sandbox.egress.upstream_ok": input.upstream.ok
|
|
11730
|
+
},
|
|
11731
|
+
`Sandbox egress ${input.request.method} ${input.upstreamUrl.hostname}${displayedUpstreamPath(input.upstreamUrl)} -> ${input.upstream.status}`
|
|
11732
|
+
);
|
|
11733
|
+
}
|
|
11734
|
+
async function requestBodyBytes(request) {
|
|
11735
|
+
if (request.method === "GET" || request.method === "HEAD" || request.body === null) {
|
|
11736
|
+
return void 0;
|
|
11737
|
+
}
|
|
11738
|
+
return await request.arrayBuffer();
|
|
11739
|
+
}
|
|
11740
|
+
function isGrantSelectionBodyVisible(input) {
|
|
11741
|
+
return input.provider === "github" && input.upstreamUrl.hostname.toLowerCase() === "api.github.com" && input.upstreamUrl.pathname.toLowerCase().endsWith("/graphql");
|
|
11742
|
+
}
|
|
11743
|
+
function grantSelectionBodyText(input) {
|
|
11744
|
+
if (input.body === void 0) {
|
|
11745
|
+
return void 0;
|
|
11746
|
+
}
|
|
11747
|
+
if (input.body.byteLength > GRANT_SELECTION_BODY_TEXT_LIMIT_BYTES) {
|
|
11748
|
+
if (!input.operation && input.provider === "github" && input.request.method.toUpperCase() === "POST" && input.upstreamUrl.hostname.toLowerCase() === "api.github.com" && input.upstreamUrl.pathname.toLowerCase().endsWith("/graphql")) {
|
|
11749
|
+
throw new EgressPolicyDenied(
|
|
11750
|
+
"GitHub GraphQL request body is too large for Junior to inspect before issuing credentials."
|
|
11751
|
+
);
|
|
11752
|
+
}
|
|
11753
|
+
return void 0;
|
|
11754
|
+
}
|
|
11755
|
+
return new TextDecoder().decode(input.body);
|
|
11756
|
+
}
|
|
11757
|
+
function responseContentLength(upstream) {
|
|
11758
|
+
const raw = upstream.headers.get("content-length");
|
|
11759
|
+
if (!raw) {
|
|
11760
|
+
return void 0;
|
|
11761
|
+
}
|
|
11762
|
+
const parsed = Number(raw);
|
|
11763
|
+
return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : void 0;
|
|
11764
|
+
}
|
|
11765
|
+
async function responseTextWithinLimit(upstream, maxBytes) {
|
|
11766
|
+
const limit = Math.min(
|
|
11767
|
+
Math.max(0, Math.floor(maxBytes)),
|
|
11768
|
+
RESPONSE_BODY_TEXT_LIMIT_BYTES
|
|
11769
|
+
);
|
|
11770
|
+
if (limit <= 0) {
|
|
11771
|
+
return void 0;
|
|
11772
|
+
}
|
|
11773
|
+
const contentLength = responseContentLength(upstream);
|
|
11774
|
+
if (contentLength !== void 0 && contentLength > limit) {
|
|
11775
|
+
return void 0;
|
|
11776
|
+
}
|
|
11777
|
+
let clone;
|
|
11778
|
+
try {
|
|
11779
|
+
clone = upstream.clone();
|
|
11780
|
+
} catch {
|
|
11781
|
+
return void 0;
|
|
11782
|
+
}
|
|
11783
|
+
const body = clone.body;
|
|
11784
|
+
if (!body) {
|
|
11785
|
+
return "";
|
|
11786
|
+
}
|
|
11787
|
+
const reader = body.getReader();
|
|
11788
|
+
const chunks = [];
|
|
11789
|
+
let bytes = 0;
|
|
11790
|
+
try {
|
|
11791
|
+
while (true) {
|
|
11792
|
+
const { done, value } = await reader.read();
|
|
11793
|
+
if (done) {
|
|
11794
|
+
break;
|
|
11795
|
+
}
|
|
11796
|
+
if (!value) {
|
|
11797
|
+
continue;
|
|
11798
|
+
}
|
|
11799
|
+
bytes += value.byteLength;
|
|
11800
|
+
if (bytes > limit) {
|
|
11801
|
+
await reader.cancel().catch(() => void 0);
|
|
11802
|
+
return void 0;
|
|
11803
|
+
}
|
|
11804
|
+
chunks.push(value);
|
|
11805
|
+
}
|
|
11806
|
+
} catch {
|
|
11807
|
+
await reader.cancel().catch(() => void 0);
|
|
11808
|
+
return void 0;
|
|
11809
|
+
} finally {
|
|
11810
|
+
reader.releaseLock();
|
|
11811
|
+
}
|
|
11812
|
+
const combined = new Uint8Array(bytes);
|
|
11813
|
+
let offset = 0;
|
|
11814
|
+
for (const chunk of chunks) {
|
|
11815
|
+
combined.set(chunk, offset);
|
|
11816
|
+
offset += chunk.byteLength;
|
|
11817
|
+
}
|
|
11818
|
+
return new TextDecoder().decode(combined);
|
|
11819
|
+
}
|
|
11820
|
+
function requestHeaders(request, lease, upstreamHost, tracePropagation) {
|
|
11821
|
+
const headers = new Headers();
|
|
11822
|
+
const mayPropagateTrace = shouldPropagateSandboxEgressTrace(
|
|
11823
|
+
upstreamHost,
|
|
11824
|
+
tracePropagation
|
|
11825
|
+
);
|
|
11826
|
+
request.headers.forEach((value, key) => {
|
|
11827
|
+
const normalized = key.toLowerCase();
|
|
11828
|
+
if (HOP_BY_HOP_HEADERS.has(normalized) || PROXY_ONLY_HEADERS.has(normalized)) {
|
|
11829
|
+
return;
|
|
11830
|
+
}
|
|
11831
|
+
if (TRACE_PROPAGATION_HEADERS.has(normalized) && !mayPropagateTrace) {
|
|
11832
|
+
return;
|
|
11833
|
+
}
|
|
11834
|
+
headers.append(key, value);
|
|
11835
|
+
});
|
|
11836
|
+
for (const transform of lease.headerTransforms) {
|
|
11837
|
+
if (!matchesSandboxEgressDomain(upstreamHost, transform.domain)) {
|
|
11838
|
+
continue;
|
|
11839
|
+
}
|
|
11840
|
+
for (const [key, value] of Object.entries(transform.headers)) {
|
|
11841
|
+
if (TRACE_PROPAGATION_HEADERS.has(key.toLowerCase()) && !mayPropagateTrace) {
|
|
11842
|
+
continue;
|
|
11843
|
+
}
|
|
11844
|
+
headers.set(key, value);
|
|
11845
|
+
}
|
|
11846
|
+
}
|
|
11847
|
+
return headers;
|
|
11848
|
+
}
|
|
11849
|
+
function responseHeaders(upstream) {
|
|
11850
|
+
const headers = new Headers();
|
|
11851
|
+
upstream.headers.forEach((value, key) => {
|
|
11852
|
+
const normalized = key.toLowerCase();
|
|
11853
|
+
if (!HOP_BY_HOP_HEADERS.has(normalized) && !DECODED_RESPONSE_HEADERS.has(normalized)) {
|
|
11854
|
+
headers.append(key, value);
|
|
11855
|
+
}
|
|
11856
|
+
});
|
|
11857
|
+
return headers;
|
|
11858
|
+
}
|
|
11859
|
+
function leaseLogAttributes(input) {
|
|
11860
|
+
return {
|
|
11861
|
+
...egressAttributes({
|
|
11862
|
+
egressId: input.egressId,
|
|
11863
|
+
grantAccess: input.lease.grant.access,
|
|
11864
|
+
grantName: input.lease.grant.name,
|
|
11865
|
+
grantReason: input.lease.grant.reason,
|
|
11866
|
+
host: input.upstreamUrl.hostname,
|
|
11867
|
+
method: input.request.method,
|
|
11868
|
+
path: input.upstreamUrl.pathname,
|
|
11869
|
+
provider: input.provider,
|
|
11870
|
+
status: input.status
|
|
11871
|
+
}),
|
|
11872
|
+
...routingAttributes(input.request, input.upstreamUrl),
|
|
11873
|
+
...input.upstream ? upstreamPermissionAttributes(input.provider, input.upstream) : {}
|
|
11874
|
+
};
|
|
11875
|
+
}
|
|
11876
|
+
async function recordSandboxAuthRequired(input) {
|
|
11877
|
+
await setSandboxEgressAuthRequiredSignal(input.credentialContext, {
|
|
11878
|
+
provider: input.provider,
|
|
11879
|
+
grant: input.grant,
|
|
11880
|
+
kind: input.kind ?? "auth_required",
|
|
11881
|
+
...input.authorization ? { authorization: input.authorization } : {},
|
|
11882
|
+
message: input.message
|
|
11883
|
+
});
|
|
11884
|
+
}
|
|
11885
|
+
async function recordSandboxPermissionDenied(input) {
|
|
11886
|
+
await setSandboxEgressPermissionDeniedSignal(input.credentialContext, {
|
|
11887
|
+
provider: input.provider,
|
|
11888
|
+
grant: input.lease.grant,
|
|
11889
|
+
...input.lease.account ? { account: input.lease.account } : {},
|
|
11890
|
+
message: input.message,
|
|
11891
|
+
source: "upstream",
|
|
11892
|
+
status: input.upstream.status,
|
|
11893
|
+
upstreamHost: input.upstreamUrl.hostname,
|
|
11894
|
+
upstreamPath: displayedUpstreamPath(input.upstreamUrl),
|
|
11895
|
+
...input.provider === "github" ? githubPermissionHeaders(input.upstream) : {}
|
|
11896
|
+
});
|
|
11897
|
+
}
|
|
11898
|
+
async function executeCredentialedEgressRequest(input) {
|
|
11899
|
+
const {
|
|
11900
|
+
activeEgressId,
|
|
11901
|
+
credentialContext,
|
|
11902
|
+
deps,
|
|
11903
|
+
operation,
|
|
11904
|
+
provider,
|
|
11905
|
+
request,
|
|
11906
|
+
upstreamUrl
|
|
11907
|
+
} = input;
|
|
11908
|
+
const bodyForGrantSelection = isGrantSelectionBodyVisible({
|
|
11909
|
+
provider,
|
|
11910
|
+
upstreamUrl
|
|
11911
|
+
}) ? await requestBodyBytes(request) : void 0;
|
|
11912
|
+
let grantSelection;
|
|
11913
|
+
try {
|
|
11914
|
+
grantSelection = await selectSandboxEgressGrant({
|
|
11915
|
+
bodyText: grantSelectionBodyText({
|
|
11916
|
+
body: bodyForGrantSelection,
|
|
11917
|
+
...operation ? { operation } : {},
|
|
11918
|
+
provider,
|
|
11919
|
+
request,
|
|
11920
|
+
upstreamUrl
|
|
11921
|
+
}),
|
|
11922
|
+
...operation ? { operation } : {},
|
|
11923
|
+
provider,
|
|
11924
|
+
method: request.method,
|
|
11925
|
+
upstreamUrl
|
|
11926
|
+
});
|
|
11927
|
+
} catch (error) {
|
|
11928
|
+
if (isEgressPolicyDenied(error)) {
|
|
11929
|
+
logWarn(
|
|
11930
|
+
"sandbox_egress_policy_denied",
|
|
11931
|
+
{},
|
|
11932
|
+
{
|
|
11933
|
+
...egressAttributes({
|
|
11934
|
+
egressId: activeEgressId,
|
|
11935
|
+
host: upstreamUrl.hostname,
|
|
11936
|
+
method: request.method,
|
|
11937
|
+
path: upstreamUrl.pathname,
|
|
11938
|
+
provider,
|
|
11939
|
+
status: 403
|
|
11940
|
+
}),
|
|
11941
|
+
...routingAttributes(request, upstreamUrl)
|
|
11942
|
+
},
|
|
11943
|
+
error.message
|
|
11944
|
+
);
|
|
11945
|
+
return policyDeniedResponse(error);
|
|
11946
|
+
}
|
|
11947
|
+
throw error;
|
|
11948
|
+
}
|
|
11949
|
+
const issueCredentialLease = deps.issueCredentialLease ?? sandboxEgressCredentialLease;
|
|
11950
|
+
const clearCredentialLease = deps.clearCredentialLease ?? clearSandboxEgressCredentialLease;
|
|
11951
|
+
const recordAuthRequired = deps.recordAuthRequired ?? recordSandboxAuthRequired;
|
|
11952
|
+
const recordPermissionDenied = deps.recordPermissionDenied ?? recordSandboxPermissionDenied;
|
|
11953
|
+
let lease;
|
|
11954
|
+
try {
|
|
11955
|
+
lease = await issueCredentialLease(
|
|
11956
|
+
provider,
|
|
11957
|
+
grantSelection,
|
|
11958
|
+
credentialContext
|
|
11959
|
+
);
|
|
11960
|
+
} catch (error) {
|
|
11961
|
+
if (error instanceof SandboxEgressCredentialError) {
|
|
11962
|
+
await recordAuthRequired({
|
|
11963
|
+
credentialContext,
|
|
11964
|
+
provider: error.provider,
|
|
11965
|
+
grant: error.grant,
|
|
11966
|
+
kind: error.kind,
|
|
11967
|
+
authorization: error.authorization,
|
|
11968
|
+
message: error.message
|
|
11969
|
+
});
|
|
11970
|
+
const isAuthRequired = error.kind === "auth_required";
|
|
11971
|
+
logWarn(
|
|
11972
|
+
isAuthRequired ? "sandbox_egress_credential_needed" : "sandbox_egress_credential_unavailable",
|
|
11973
|
+
{},
|
|
11974
|
+
{
|
|
11975
|
+
...egressAttributes({
|
|
11976
|
+
egressId: activeEgressId,
|
|
11977
|
+
grantAccess: error.grant.access,
|
|
11978
|
+
grantName: error.grant.name,
|
|
11979
|
+
grantReason: error.grant.reason,
|
|
11980
|
+
host: upstreamUrl.hostname,
|
|
11981
|
+
method: request.method,
|
|
11982
|
+
path: upstreamUrl.pathname,
|
|
11983
|
+
provider: error.provider,
|
|
11984
|
+
status: 401
|
|
11985
|
+
}),
|
|
11986
|
+
...routingAttributes(request, upstreamUrl)
|
|
11987
|
+
},
|
|
11988
|
+
isAuthRequired ? "Sandbox egress grant needs user authorization before issuing a credential lease" : "Sandbox egress credential lease is unavailable for selected grant"
|
|
11989
|
+
);
|
|
11990
|
+
return authRequiredResponse({
|
|
11991
|
+
provider: error.provider,
|
|
11992
|
+
grant: error.grant,
|
|
11993
|
+
message: error.message
|
|
11994
|
+
});
|
|
11995
|
+
}
|
|
11996
|
+
throw error;
|
|
11997
|
+
}
|
|
11998
|
+
const attributes = (status, upstream2) => leaseLogAttributes({
|
|
11999
|
+
egressId: activeEgressId,
|
|
12000
|
+
lease,
|
|
12001
|
+
provider,
|
|
12002
|
+
request,
|
|
12003
|
+
status,
|
|
12004
|
+
...upstream2 ? { upstream: upstream2 } : {},
|
|
12005
|
+
upstreamUrl
|
|
12006
|
+
});
|
|
12007
|
+
if (!hasSandboxEgressLeaseTransformForHost(lease, upstreamUrl.hostname)) {
|
|
12008
|
+
logWarn(
|
|
12009
|
+
"sandbox_egress_transform_missing",
|
|
12010
|
+
{},
|
|
12011
|
+
{
|
|
12012
|
+
...attributes(403),
|
|
12013
|
+
"app.sandbox.egress.transform_domains": lease.headerTransforms.map(
|
|
12014
|
+
(transform) => transform.domain
|
|
12015
|
+
)
|
|
12016
|
+
},
|
|
12017
|
+
"Sandbox egress credential lease does not cover forwarded host"
|
|
12018
|
+
);
|
|
12019
|
+
return Response.json(
|
|
12020
|
+
{ error: "Credential lease does not cover forwarded host" },
|
|
12021
|
+
{ status: 403 }
|
|
12022
|
+
);
|
|
12023
|
+
}
|
|
12024
|
+
const fetchImpl = deps.fetch ?? fetch;
|
|
12025
|
+
const headers = requestHeaders(
|
|
12026
|
+
request,
|
|
12027
|
+
lease,
|
|
12028
|
+
upstreamUrl.hostname,
|
|
12029
|
+
deps.tracePropagation ?? {}
|
|
12030
|
+
);
|
|
12031
|
+
const body = bodyForGrantSelection ?? await requestBodyBytes(request);
|
|
12032
|
+
const intercepted = await deps.interceptHttp?.({
|
|
12033
|
+
provider,
|
|
12034
|
+
request: new Request(upstreamUrl, {
|
|
12035
|
+
method: request.method,
|
|
12036
|
+
headers,
|
|
12037
|
+
...body !== void 0 ? { body } : {}
|
|
12038
|
+
}),
|
|
12039
|
+
upstreamUrl
|
|
12040
|
+
});
|
|
12041
|
+
if (intercepted) {
|
|
12042
|
+
return intercepted;
|
|
12043
|
+
}
|
|
12044
|
+
const upstream = await fetchImpl(upstreamUrl, {
|
|
12045
|
+
method: request.method,
|
|
12046
|
+
headers,
|
|
12047
|
+
...body !== void 0 ? { body } : {},
|
|
12048
|
+
redirect: "manual"
|
|
12049
|
+
});
|
|
12050
|
+
try {
|
|
12051
|
+
const effects = await onPluginEgressResponse({
|
|
12052
|
+
provider,
|
|
12053
|
+
grant: lease.grant,
|
|
12054
|
+
method: request.method,
|
|
12055
|
+
...operation ? { operation } : {},
|
|
12056
|
+
upstreamUrl,
|
|
12057
|
+
response: {
|
|
12058
|
+
headers: new Headers(upstream.headers),
|
|
12059
|
+
readText: async (maxBytes) => await responseTextWithinLimit(upstream, maxBytes),
|
|
12060
|
+
status: upstream.status
|
|
12061
|
+
}
|
|
12062
|
+
});
|
|
12063
|
+
if (effects.permissionDenied) {
|
|
12064
|
+
await recordPermissionDenied({
|
|
12065
|
+
credentialContext,
|
|
12066
|
+
provider,
|
|
12067
|
+
lease,
|
|
12068
|
+
message: effects.permissionDenied.message,
|
|
12069
|
+
upstream,
|
|
12070
|
+
upstreamUrl
|
|
12071
|
+
});
|
|
12072
|
+
logWarn(
|
|
12073
|
+
"sandbox_egress_upstream_permission_classified",
|
|
12074
|
+
{},
|
|
12075
|
+
{
|
|
12076
|
+
...attributes(upstream.status, upstream)
|
|
12077
|
+
},
|
|
12078
|
+
"Sandbox egress plugin classified upstream response as permission denied"
|
|
12079
|
+
);
|
|
12080
|
+
}
|
|
12081
|
+
} catch (error) {
|
|
12082
|
+
if (!isEgressAuthRequired(error)) {
|
|
12083
|
+
throw error;
|
|
12084
|
+
}
|
|
12085
|
+
await clearCredentialLease(provider, lease.grant.name, credentialContext);
|
|
12086
|
+
await recordAuthRequired({
|
|
12087
|
+
credentialContext,
|
|
12088
|
+
provider,
|
|
12089
|
+
grant: lease.grant,
|
|
12090
|
+
authorization: error.authorization ?? lease.authorization,
|
|
12091
|
+
message: error.message
|
|
12092
|
+
});
|
|
12093
|
+
logWarn(
|
|
12094
|
+
"sandbox_egress_upstream_auth_required_classified",
|
|
12095
|
+
{},
|
|
12096
|
+
{
|
|
12097
|
+
...attributes(upstream.status, upstream)
|
|
12098
|
+
},
|
|
12099
|
+
"Sandbox egress plugin classified upstream response as auth required"
|
|
12100
|
+
);
|
|
12101
|
+
await upstream.body?.cancel().catch(() => void 0);
|
|
12102
|
+
return authRequiredResponse({
|
|
12103
|
+
provider,
|
|
12104
|
+
grant: lease.grant,
|
|
12105
|
+
message: error.message
|
|
12106
|
+
});
|
|
12107
|
+
}
|
|
12108
|
+
logSandboxEgressUpstreamRequest({
|
|
12109
|
+
egressId: activeEgressId,
|
|
12110
|
+
grantAccess: lease.grant.access,
|
|
12111
|
+
grantName: lease.grant.name,
|
|
12112
|
+
grantReason: lease.grant.reason,
|
|
12113
|
+
provider,
|
|
12114
|
+
request,
|
|
12115
|
+
upstream,
|
|
12116
|
+
upstreamUrl
|
|
12117
|
+
});
|
|
12118
|
+
if (upstream.status >= 400) {
|
|
12119
|
+
logWarn(
|
|
12120
|
+
"sandbox_egress_upstream_error_response",
|
|
12121
|
+
{},
|
|
12122
|
+
{
|
|
12123
|
+
...attributes(upstream.status, upstream),
|
|
12124
|
+
"error.type": `http_${upstream.status}`
|
|
12125
|
+
},
|
|
12126
|
+
`Sandbox egress upstream returned HTTP ${upstream.status}`
|
|
12127
|
+
);
|
|
12128
|
+
}
|
|
12129
|
+
if (upstream.status === UPSTREAM_TOKEN_REJECTION_STATUS || upstream.status === UPSTREAM_PERMISSION_REJECTION_STATUS) {
|
|
12130
|
+
logWarn(
|
|
12131
|
+
"sandbox_egress_upstream_auth_rejected",
|
|
12132
|
+
{},
|
|
12133
|
+
{
|
|
12134
|
+
...attributes(upstream.status, upstream),
|
|
12135
|
+
...upstream.status === UPSTREAM_TOKEN_REJECTION_STATUS ? {
|
|
12136
|
+
"app.sandbox.egress.www_authenticate": upstream.headers.get("www-authenticate") ?? void 0
|
|
12137
|
+
} : {}
|
|
12138
|
+
},
|
|
12139
|
+
upstream.status === UPSTREAM_TOKEN_REJECTION_STATUS ? "Sandbox egress upstream auth rejected injected credential" : "Sandbox egress upstream permission denied"
|
|
12140
|
+
);
|
|
12141
|
+
if (upstream.status === UPSTREAM_TOKEN_REJECTION_STATUS) {
|
|
12142
|
+
await clearCredentialLease(provider, lease.grant.name, credentialContext);
|
|
12143
|
+
await recordAuthRequired({
|
|
12144
|
+
credentialContext,
|
|
12145
|
+
provider,
|
|
12146
|
+
grant: lease.grant,
|
|
12147
|
+
authorization: lease.authorization,
|
|
12148
|
+
message: `Provider rejected the injected ${provider} credential.`
|
|
12149
|
+
});
|
|
12150
|
+
await upstream.body?.cancel().catch(() => void 0);
|
|
12151
|
+
return authRequiredResponse({
|
|
12152
|
+
provider,
|
|
12153
|
+
grant: lease.grant,
|
|
12154
|
+
message: `Provider rejected the injected ${provider} credential.
|
|
12155
|
+
`
|
|
12156
|
+
});
|
|
12157
|
+
} else {
|
|
12158
|
+
await clearCredentialLease(provider, lease.grant.name, credentialContext);
|
|
12159
|
+
await recordPermissionDenied({
|
|
12160
|
+
credentialContext,
|
|
12161
|
+
provider,
|
|
12162
|
+
lease,
|
|
12163
|
+
message: permissionDeniedMessage(provider, lease.grant),
|
|
12164
|
+
upstream,
|
|
12165
|
+
upstreamUrl
|
|
12166
|
+
});
|
|
12167
|
+
}
|
|
12168
|
+
}
|
|
12169
|
+
return new Response(upstream.body, {
|
|
12170
|
+
status: upstream.status,
|
|
12171
|
+
statusText: upstream.statusText,
|
|
12172
|
+
headers: responseHeaders(upstream)
|
|
12173
|
+
});
|
|
12174
|
+
}
|
|
12175
|
+
|
|
12176
|
+
// src/chat/egress/plugin.ts
|
|
12177
|
+
function credentialContextForPluginEgress(credentialContext) {
|
|
12178
|
+
return {
|
|
12179
|
+
credentials: credentialContext,
|
|
12180
|
+
contextId: randomUUID4(),
|
|
12181
|
+
egressId: `plugin-egress:${randomUUID4()}`,
|
|
12182
|
+
expiresAtMs: Date.now() + 30 * 60 * 1e3
|
|
12183
|
+
};
|
|
12184
|
+
}
|
|
12185
|
+
function createPluginEgress(deps) {
|
|
12186
|
+
const credentialContext = deps.credentialContext ? credentialContextForPluginEgress(deps.credentialContext) : void 0;
|
|
12187
|
+
return {
|
|
12188
|
+
async fetch(input) {
|
|
12189
|
+
const operation = input.operation.trim();
|
|
12190
|
+
if (!operation) {
|
|
12191
|
+
throw new Error("Plugin egress operation is required");
|
|
12192
|
+
}
|
|
12193
|
+
if (!credentialContext) {
|
|
12194
|
+
throw new PluginCredentialFailureError(
|
|
12195
|
+
input.provider,
|
|
12196
|
+
`Cannot issue ${input.provider} credentials without a credential context.`
|
|
12197
|
+
);
|
|
12198
|
+
}
|
|
12199
|
+
const upstreamUrl = new URL(input.request.url);
|
|
12200
|
+
if (upstreamUrl.protocol !== "https:") {
|
|
12201
|
+
throw new Error("Plugin egress requires HTTPS provider URLs");
|
|
12202
|
+
}
|
|
12203
|
+
const resolvedProvider = resolveSandboxEgressProviderForHost(
|
|
12204
|
+
upstreamUrl.hostname
|
|
12205
|
+
);
|
|
12206
|
+
if (resolvedProvider !== input.provider) {
|
|
12207
|
+
throw new Error(
|
|
12208
|
+
`Plugin egress provider "${input.provider}" does not own ${upstreamUrl.hostname}`
|
|
12209
|
+
);
|
|
12210
|
+
}
|
|
12211
|
+
return await executeCredentialedEgressRequest({
|
|
12212
|
+
activeEgressId: credentialContext.egressId,
|
|
12213
|
+
credentialContext,
|
|
12214
|
+
deps: {
|
|
12215
|
+
...deps.fetch ? { fetch: deps.fetch } : {},
|
|
12216
|
+
recordAuthRequired: async (signal) => {
|
|
12217
|
+
await deps.pluginAuth.handleAuthRequired({
|
|
12218
|
+
...signal.authorization ? { authorization: signal.authorization } : {},
|
|
12219
|
+
grant: signal.grant,
|
|
12220
|
+
kind: signal.kind ?? "auth_required",
|
|
12221
|
+
message: signal.message,
|
|
12222
|
+
provider: signal.provider
|
|
12223
|
+
});
|
|
12224
|
+
},
|
|
12225
|
+
recordPermissionDenied: async () => {
|
|
12226
|
+
},
|
|
12227
|
+
tracePropagation: {}
|
|
12228
|
+
},
|
|
12229
|
+
operation,
|
|
12230
|
+
provider: input.provider,
|
|
12231
|
+
request: input.request,
|
|
12232
|
+
upstreamUrl
|
|
12233
|
+
});
|
|
12234
|
+
}
|
|
12235
|
+
};
|
|
12236
|
+
}
|
|
12237
|
+
|
|
11202
12238
|
// src/chat/respond.ts
|
|
11203
12239
|
var AGENT_ABORT_SETTLE_GRACE_MS = 5e3;
|
|
11204
12240
|
function sleep3(ms) {
|
|
@@ -11811,6 +12847,23 @@ async function generateAssistantReplyInPrivacyContext(messageText2, context, con
|
|
|
11811
12847
|
userText: userInput,
|
|
11812
12848
|
artifactState: context.artifactState,
|
|
11813
12849
|
configuration: configurationValues,
|
|
12850
|
+
egress: createPluginEgress({
|
|
12851
|
+
credentialContext: context.credentialContext,
|
|
12852
|
+
pluginAuth: {
|
|
12853
|
+
async handleAuthRequired(signal) {
|
|
12854
|
+
await pluginAuth.maybeHandleAuthSignal({
|
|
12855
|
+
auth_required: {
|
|
12856
|
+
...signal.authorization ? { authorization: signal.authorization } : {},
|
|
12857
|
+
createdAtMs: Date.now(),
|
|
12858
|
+
grant: signal.grant,
|
|
12859
|
+
kind: signal.kind,
|
|
12860
|
+
message: signal.message,
|
|
12861
|
+
provider: signal.provider
|
|
12862
|
+
}
|
|
12863
|
+
});
|
|
12864
|
+
}
|
|
12865
|
+
}
|
|
12866
|
+
}),
|
|
11814
12867
|
mcpToolManager: turnMcpToolManager,
|
|
11815
12868
|
sandbox,
|
|
11816
12869
|
surface,
|
|
@@ -13162,7 +14215,6 @@ export {
|
|
|
13162
14215
|
setConfigDefaults,
|
|
13163
14216
|
getConfigDefaults,
|
|
13164
14217
|
createUserTokenStore,
|
|
13165
|
-
issueProviderCredentialLease,
|
|
13166
14218
|
listThreadReplies,
|
|
13167
14219
|
postSlackMessage,
|
|
13168
14220
|
deleteSlackMessage,
|
|
@@ -13177,14 +14229,8 @@ export {
|
|
|
13177
14229
|
startOAuthFlow,
|
|
13178
14230
|
SANDBOX_EGRESS_PROXY_PATH,
|
|
13179
14231
|
parseSandboxEgressCredentialToken,
|
|
13180
|
-
setSandboxEgressCredentialLease,
|
|
13181
|
-
getSandboxEgressCredentialLease,
|
|
13182
|
-
clearSandboxEgressCredentialLease,
|
|
13183
|
-
setSandboxEgressAuthRequiredSignal,
|
|
13184
|
-
setSandboxEgressPermissionDeniedSignal,
|
|
13185
14232
|
normalizeSandboxEgressTracePropagationDomains,
|
|
13186
14233
|
shouldPropagateSandboxEgressTrace,
|
|
13187
|
-
matchesSandboxEgressDomain,
|
|
13188
14234
|
resolveSandboxEgressProviderForHost,
|
|
13189
14235
|
shouldEmitDevAgentTrace,
|
|
13190
14236
|
AuthorizationFlowDisabledError,
|
|
@@ -13215,6 +14261,8 @@ export {
|
|
|
13215
14261
|
markTurnFailed,
|
|
13216
14262
|
isRedundantReactionAckText,
|
|
13217
14263
|
finalizeMcpAuthorization,
|
|
14264
|
+
resolvePluginOAuthAccount,
|
|
14265
|
+
executeCredentialedEgressRequest,
|
|
13218
14266
|
generateAssistantReply,
|
|
13219
14267
|
estimateTextTokens,
|
|
13220
14268
|
getAgentContextCompactionTriggerTokens,
|