@sentry/junior 0.42.0 → 0.44.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 +1067 -442
- package/dist/{chunk-SCE5C645.js → chunk-BCG3I2T2.js} +197 -101
- package/dist/{chunk-Y3UO7NR6.js → chunk-QAMTCT2R.js} +50 -10
- package/dist/{chunk-7QYONRLH.js → chunk-ZUUJTQ2H.js} +1 -1
- package/dist/cli/check.js +30 -11
- package/dist/cli/snapshot-warmup.js +2 -2
- package/package.json +3 -2
package/dist/app.js
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
findSkillByName,
|
|
4
4
|
loadSkillsByName,
|
|
5
5
|
parseSkillInvocation
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-ZUUJTQ2H.js";
|
|
7
7
|
import {
|
|
8
8
|
GEN_AI_PROVIDER_NAME,
|
|
9
9
|
MISSING_GATEWAY_CREDENTIALS_ERROR,
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
buildNonInteractiveShellScript,
|
|
15
15
|
completeObject,
|
|
16
16
|
completeText,
|
|
17
|
+
createSandboxInstance,
|
|
17
18
|
getGatewayApiKey,
|
|
18
19
|
getPiGatewayApiKeyOverride,
|
|
19
20
|
getRuntimeDependencyProfileHash,
|
|
@@ -30,7 +31,7 @@ import {
|
|
|
30
31
|
runNonInteractiveCommand,
|
|
31
32
|
sandboxSkillDir,
|
|
32
33
|
sandboxSkillFile
|
|
33
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-QAMTCT2R.js";
|
|
34
35
|
import {
|
|
35
36
|
CredentialUnavailableError,
|
|
36
37
|
buildOAuthTokenRequest,
|
|
@@ -58,6 +59,7 @@ import {
|
|
|
58
59
|
mergeHeaderTransforms,
|
|
59
60
|
parseOAuthTokenResponse,
|
|
60
61
|
resolveAuthTokenPlaceholder,
|
|
62
|
+
resolvePluginCommandEnv,
|
|
61
63
|
serializeGenAiAttribute,
|
|
62
64
|
setSpanAttributes,
|
|
63
65
|
setSpanStatus,
|
|
@@ -66,7 +68,7 @@ import {
|
|
|
66
68
|
toOptionalString,
|
|
67
69
|
withContext,
|
|
68
70
|
withSpan
|
|
69
|
-
} from "./chunk-
|
|
71
|
+
} from "./chunk-BCG3I2T2.js";
|
|
70
72
|
import {
|
|
71
73
|
sentry_exports
|
|
72
74
|
} from "./chunk-Z3YD6NHK.js";
|
|
@@ -3219,140 +3221,6 @@ var ProviderCredentialRouter = class {
|
|
|
3219
3221
|
}
|
|
3220
3222
|
};
|
|
3221
3223
|
|
|
3222
|
-
// src/chat/capabilities/runtime.ts
|
|
3223
|
-
function toHeaderTransforms(lease) {
|
|
3224
|
-
if (!Array.isArray(lease.headerTransforms) || lease.headerTransforms.length === 0) {
|
|
3225
|
-
return [];
|
|
3226
|
-
}
|
|
3227
|
-
return lease.headerTransforms.filter(
|
|
3228
|
-
(transform) => Boolean(transform?.domain?.trim()) && transform.headers && typeof transform.headers === "object" && Object.keys(transform.headers).length > 0
|
|
3229
|
-
).map((transform) => ({
|
|
3230
|
-
domain: transform.domain.trim(),
|
|
3231
|
-
headers: transform.headers
|
|
3232
|
-
}));
|
|
3233
|
-
}
|
|
3234
|
-
var SkillCapabilityRuntime = class {
|
|
3235
|
-
router;
|
|
3236
|
-
requesterId;
|
|
3237
|
-
enabledByProvider = /* @__PURE__ */ new Map();
|
|
3238
|
-
constructor(params) {
|
|
3239
|
-
if (params.router) {
|
|
3240
|
-
this.router = params.router;
|
|
3241
|
-
} else if (params.broker) {
|
|
3242
|
-
this.router = {
|
|
3243
|
-
issue: async (input) => await params.broker.issue(input)
|
|
3244
|
-
};
|
|
3245
|
-
} else {
|
|
3246
|
-
throw new Error(
|
|
3247
|
-
"SkillCapabilityRuntime requires either router or broker"
|
|
3248
|
-
);
|
|
3249
|
-
}
|
|
3250
|
-
this.requesterId = params.requesterId;
|
|
3251
|
-
}
|
|
3252
|
-
async enableCredentialsForTurn(input) {
|
|
3253
|
-
const provider = input.activeSkill?.pluginProvider;
|
|
3254
|
-
if (!provider) {
|
|
3255
|
-
return void 0;
|
|
3256
|
-
}
|
|
3257
|
-
if (!this.requesterId) {
|
|
3258
|
-
throw new Error("Credential enablement requires requester context");
|
|
3259
|
-
}
|
|
3260
|
-
const plugin = getPluginDefinition(provider);
|
|
3261
|
-
if (!plugin?.manifest.credentials && !plugin?.manifest.apiHeaders) {
|
|
3262
|
-
return void 0;
|
|
3263
|
-
}
|
|
3264
|
-
const existing = this.enabledByProvider.get(provider);
|
|
3265
|
-
const now = Date.now();
|
|
3266
|
-
if (existing && existing.expiresAtMs - now > 1e4) {
|
|
3267
|
-
return {
|
|
3268
|
-
reused: true,
|
|
3269
|
-
expiresAt: new Date(existing.expiresAtMs).toISOString()
|
|
3270
|
-
};
|
|
3271
|
-
}
|
|
3272
|
-
logInfo(
|
|
3273
|
-
"credential_issue_request",
|
|
3274
|
-
{},
|
|
3275
|
-
{
|
|
3276
|
-
"app.skill.name": input.activeSkill?.name,
|
|
3277
|
-
"app.credential.provider": provider
|
|
3278
|
-
},
|
|
3279
|
-
"Issuing provider credential for current turn"
|
|
3280
|
-
);
|
|
3281
|
-
try {
|
|
3282
|
-
const lease = await this.router.issue({
|
|
3283
|
-
provider,
|
|
3284
|
-
reason: input.reason,
|
|
3285
|
-
requesterId: this.requesterId
|
|
3286
|
-
});
|
|
3287
|
-
const transforms = toHeaderTransforms(lease);
|
|
3288
|
-
if (transforms.length === 0) {
|
|
3289
|
-
throw new Error(
|
|
3290
|
-
`Credential lease for ${provider} did not include header transforms`
|
|
3291
|
-
);
|
|
3292
|
-
}
|
|
3293
|
-
const expiresAtMs = Date.parse(lease.expiresAt);
|
|
3294
|
-
if (!Number.isFinite(expiresAtMs)) {
|
|
3295
|
-
throw new Error(
|
|
3296
|
-
`Credential lease for ${provider} returned invalid expiresAt`
|
|
3297
|
-
);
|
|
3298
|
-
}
|
|
3299
|
-
this.enabledByProvider.set(provider, {
|
|
3300
|
-
expiresAtMs,
|
|
3301
|
-
transforms,
|
|
3302
|
-
env: lease.env
|
|
3303
|
-
});
|
|
3304
|
-
logInfo(
|
|
3305
|
-
"credential_issue_success",
|
|
3306
|
-
{},
|
|
3307
|
-
{
|
|
3308
|
-
"app.skill.name": input.activeSkill?.name,
|
|
3309
|
-
"app.credential.provider": lease.provider,
|
|
3310
|
-
"app.credential.expires_at": lease.expiresAt,
|
|
3311
|
-
"app.credential.delivery": "header_transform"
|
|
3312
|
-
},
|
|
3313
|
-
"Issued provider credential lease"
|
|
3314
|
-
);
|
|
3315
|
-
return { reused: false, expiresAt: lease.expiresAt };
|
|
3316
|
-
} catch (error) {
|
|
3317
|
-
logWarn(
|
|
3318
|
-
"credential_issue_failed",
|
|
3319
|
-
{},
|
|
3320
|
-
{
|
|
3321
|
-
"app.skill.name": input.activeSkill?.name,
|
|
3322
|
-
"app.credential.provider": provider,
|
|
3323
|
-
"exception.message": error instanceof Error ? error.message : String(error)
|
|
3324
|
-
},
|
|
3325
|
-
"Provider credential resolution failed"
|
|
3326
|
-
);
|
|
3327
|
-
throw error;
|
|
3328
|
-
}
|
|
3329
|
-
}
|
|
3330
|
-
getTurnHeaderTransforms() {
|
|
3331
|
-
const now = Date.now();
|
|
3332
|
-
const headerTransforms = [];
|
|
3333
|
-
for (const [provider, entry] of this.enabledByProvider.entries()) {
|
|
3334
|
-
if (!Number.isFinite(entry.expiresAtMs) || entry.expiresAtMs <= now) {
|
|
3335
|
-
this.enabledByProvider.delete(provider);
|
|
3336
|
-
continue;
|
|
3337
|
-
}
|
|
3338
|
-
headerTransforms.push(...entry.transforms);
|
|
3339
|
-
}
|
|
3340
|
-
return headerTransforms.length > 0 ? headerTransforms : void 0;
|
|
3341
|
-
}
|
|
3342
|
-
getTurnEnv() {
|
|
3343
|
-
const now = Date.now();
|
|
3344
|
-
const env = {};
|
|
3345
|
-
for (const [provider, entry] of this.enabledByProvider.entries()) {
|
|
3346
|
-
if (!Number.isFinite(entry.expiresAtMs) || entry.expiresAtMs <= now) {
|
|
3347
|
-
this.enabledByProvider.delete(provider);
|
|
3348
|
-
continue;
|
|
3349
|
-
}
|
|
3350
|
-
Object.assign(env, entry.env);
|
|
3351
|
-
}
|
|
3352
|
-
return Object.keys(env).length > 0 ? env : void 0;
|
|
3353
|
-
}
|
|
3354
|
-
};
|
|
3355
|
-
|
|
3356
3224
|
// src/chat/credentials/state-adapter-token-store.ts
|
|
3357
3225
|
var KEY_PREFIX = "oauth-token";
|
|
3358
3226
|
var BUFFER_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -3419,12 +3287,13 @@ var TestCredentialBroker = class {
|
|
|
3419
3287
|
|
|
3420
3288
|
// src/chat/capabilities/factory.ts
|
|
3421
3289
|
var ENV_PLACEHOLDER_RE = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
|
|
3290
|
+
var sandboxEgressRouters = /* @__PURE__ */ new WeakMap();
|
|
3422
3291
|
function createUserTokenStore() {
|
|
3423
3292
|
return new StateAdapterTokenStore(getStateAdapter());
|
|
3424
3293
|
}
|
|
3425
3294
|
function resolveTestApiHeaderTransforms(manifest) {
|
|
3426
|
-
const {
|
|
3427
|
-
if (!
|
|
3295
|
+
const { domains, apiHeaders } = manifest;
|
|
3296
|
+
if (!domains || !apiHeaders) {
|
|
3428
3297
|
return [];
|
|
3429
3298
|
}
|
|
3430
3299
|
const headers = Object.fromEntries(
|
|
@@ -3435,44 +3304,51 @@ function resolveTestApiHeaderTransforms(manifest) {
|
|
|
3435
3304
|
})
|
|
3436
3305
|
])
|
|
3437
3306
|
);
|
|
3438
|
-
return
|
|
3307
|
+
return domains.map((domain) => ({ domain, headers }));
|
|
3308
|
+
}
|
|
3309
|
+
function createTestBroker(plugin) {
|
|
3310
|
+
const { apiHeaders, credentials, name } = plugin.manifest;
|
|
3311
|
+
const commandEnv = resolvePluginCommandEnv(plugin.manifest);
|
|
3312
|
+
return new TestCredentialBroker({
|
|
3313
|
+
provider: name,
|
|
3314
|
+
...credentials ? {
|
|
3315
|
+
domains: credentials.domains,
|
|
3316
|
+
...credentials.apiHeaders ? { apiHeaders: credentials.apiHeaders } : {},
|
|
3317
|
+
envKey: credentials.authTokenEnv,
|
|
3318
|
+
placeholder: resolveAuthTokenPlaceholder(credentials)
|
|
3319
|
+
} : {},
|
|
3320
|
+
...apiHeaders ? {
|
|
3321
|
+
headerTransforms: () => resolveTestApiHeaderTransforms(plugin.manifest)
|
|
3322
|
+
} : {},
|
|
3323
|
+
...Object.keys(commandEnv).length > 0 ? { env: commandEnv } : {}
|
|
3324
|
+
});
|
|
3439
3325
|
}
|
|
3440
|
-
function
|
|
3326
|
+
function createProviderCredentialRouter(userTokenStore) {
|
|
3441
3327
|
logCapabilityCatalogLoadedOnce();
|
|
3442
3328
|
const useTestBroker = process.env.EVAL_ENABLE_TEST_CREDENTIALS === "1";
|
|
3443
|
-
const userTokenStore = createUserTokenStore();
|
|
3444
3329
|
const brokersByProvider = {};
|
|
3445
3330
|
for (const plugin of getPluginProviders()) {
|
|
3446
|
-
const {
|
|
3447
|
-
if (!credentials && !apiHeaders) {
|
|
3331
|
+
const { name } = plugin.manifest;
|
|
3332
|
+
if (!plugin.manifest.credentials && !plugin.manifest.apiHeaders) {
|
|
3448
3333
|
continue;
|
|
3449
3334
|
}
|
|
3450
|
-
|
|
3451
|
-
brokersByProvider[name] = useTestBroker ? new TestCredentialBroker({
|
|
3452
|
-
provider: name,
|
|
3453
|
-
headerTransforms: () => resolveTestApiHeaderTransforms(plugin.manifest),
|
|
3454
|
-
...plugin.manifest.commandEnv ? { env: plugin.manifest.commandEnv } : {}
|
|
3455
|
-
}) : createPluginBroker(name, { userTokenStore });
|
|
3456
|
-
continue;
|
|
3457
|
-
}
|
|
3458
|
-
const placeholder = resolveAuthTokenPlaceholder(credentials);
|
|
3459
|
-
brokersByProvider[name] = useTestBroker ? new TestCredentialBroker({
|
|
3460
|
-
provider: name,
|
|
3461
|
-
domains: credentials.apiDomains,
|
|
3462
|
-
...credentials.apiHeaders ? { apiHeaders: credentials.apiHeaders } : {},
|
|
3463
|
-
...apiHeaders ? {
|
|
3464
|
-
headerTransforms: () => resolveTestApiHeaderTransforms(plugin.manifest)
|
|
3465
|
-
} : {},
|
|
3466
|
-
...plugin.manifest.commandEnv ? { env: plugin.manifest.commandEnv } : {},
|
|
3467
|
-
envKey: credentials.authTokenEnv,
|
|
3468
|
-
placeholder
|
|
3469
|
-
}) : createPluginBroker(name, { userTokenStore });
|
|
3335
|
+
brokersByProvider[name] = useTestBroker ? createTestBroker(plugin) : createPluginBroker(name, { userTokenStore });
|
|
3470
3336
|
}
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3337
|
+
return new ProviderCredentialRouter({ brokersByProvider });
|
|
3338
|
+
}
|
|
3339
|
+
function getSandboxEgressRouter() {
|
|
3340
|
+
const stateAdapter = getStateAdapter();
|
|
3341
|
+
let router = sandboxEgressRouters.get(stateAdapter);
|
|
3342
|
+
if (!router) {
|
|
3343
|
+
router = createProviderCredentialRouter(
|
|
3344
|
+
new StateAdapterTokenStore(stateAdapter)
|
|
3345
|
+
);
|
|
3346
|
+
sandboxEgressRouters.set(stateAdapter, router);
|
|
3347
|
+
}
|
|
3348
|
+
return router;
|
|
3349
|
+
}
|
|
3350
|
+
async function issueProviderCredentialLease(input) {
|
|
3351
|
+
return await getSandboxEgressRouter().issue(input);
|
|
3476
3352
|
}
|
|
3477
3353
|
|
|
3478
3354
|
// src/chat/capabilities/jr-rpc-command.ts
|
|
@@ -7354,13 +7230,272 @@ function createSlackThreadReadTool(context) {
|
|
|
7354
7230
|
});
|
|
7355
7231
|
}
|
|
7356
7232
|
|
|
7357
|
-
// src/chat/tools/
|
|
7233
|
+
// src/chat/tools/slack/user-lookup.ts
|
|
7358
7234
|
import { Type as Type19 } from "@sinclair/typebox";
|
|
7235
|
+
|
|
7236
|
+
// src/chat/slack/users.ts
|
|
7237
|
+
function normalizeUser(raw) {
|
|
7238
|
+
const rawFields = raw.profile?.fields;
|
|
7239
|
+
const profileFields = [];
|
|
7240
|
+
if (rawFields && typeof rawFields === "object") {
|
|
7241
|
+
for (const [id, field] of Object.entries(rawFields)) {
|
|
7242
|
+
if (!field) continue;
|
|
7243
|
+
profileFields.push({
|
|
7244
|
+
id,
|
|
7245
|
+
label: field.label || void 0,
|
|
7246
|
+
value: field.value || void 0,
|
|
7247
|
+
alt: field.alt || void 0
|
|
7248
|
+
});
|
|
7249
|
+
}
|
|
7250
|
+
}
|
|
7251
|
+
return {
|
|
7252
|
+
id: raw.id ?? "",
|
|
7253
|
+
team_id: raw.team_id || void 0,
|
|
7254
|
+
name: raw.name || void 0,
|
|
7255
|
+
real_name: raw.real_name || raw.profile?.real_name || void 0,
|
|
7256
|
+
display_name: raw.profile?.display_name || void 0,
|
|
7257
|
+
title: raw.profile?.title || void 0,
|
|
7258
|
+
email: raw.profile?.email || void 0,
|
|
7259
|
+
status_text: raw.profile?.status_text || void 0,
|
|
7260
|
+
status_emoji: raw.profile?.status_emoji || void 0,
|
|
7261
|
+
is_bot: raw.is_bot ?? false,
|
|
7262
|
+
is_deleted: raw.deleted ?? false,
|
|
7263
|
+
timezone: raw.tz || void 0,
|
|
7264
|
+
...profileFields.length > 0 ? { profile_fields: profileFields } : {}
|
|
7265
|
+
};
|
|
7266
|
+
}
|
|
7267
|
+
async function lookupSlackUserProfile(userId) {
|
|
7268
|
+
const client2 = getSlackClient();
|
|
7269
|
+
const result = await withSlackRetries(
|
|
7270
|
+
() => client2.users.info({ user: userId }),
|
|
7271
|
+
3,
|
|
7272
|
+
{ action: "users.info" }
|
|
7273
|
+
);
|
|
7274
|
+
const user = result.user;
|
|
7275
|
+
if (!user) {
|
|
7276
|
+
throw new Error(`Slack users.info returned no user for ${userId}`);
|
|
7277
|
+
}
|
|
7278
|
+
return normalizeUser(user);
|
|
7279
|
+
}
|
|
7280
|
+
async function lookupSlackUserByEmail(email) {
|
|
7281
|
+
const client2 = getSlackClient();
|
|
7282
|
+
let result;
|
|
7283
|
+
try {
|
|
7284
|
+
result = await withSlackRetries(
|
|
7285
|
+
() => client2.users.lookupByEmail({ email }),
|
|
7286
|
+
3,
|
|
7287
|
+
{ action: "users.lookupByEmail" }
|
|
7288
|
+
);
|
|
7289
|
+
} catch (error) {
|
|
7290
|
+
const apiError = error.apiError;
|
|
7291
|
+
if (apiError === "users_not_found") {
|
|
7292
|
+
return null;
|
|
7293
|
+
}
|
|
7294
|
+
throw error;
|
|
7295
|
+
}
|
|
7296
|
+
const user = result.user;
|
|
7297
|
+
if (!user) {
|
|
7298
|
+
return null;
|
|
7299
|
+
}
|
|
7300
|
+
return normalizeUser(user);
|
|
7301
|
+
}
|
|
7302
|
+
function scoreMatch(user, queryLower) {
|
|
7303
|
+
const name = (user.name ?? "").toLowerCase();
|
|
7304
|
+
const realName = (user.real_name ?? user.profile?.real_name ?? "").toLowerCase();
|
|
7305
|
+
const displayName = (user.profile?.display_name ?? "").toLowerCase();
|
|
7306
|
+
if (name === queryLower || displayName === queryLower) return 100;
|
|
7307
|
+
if (realName === queryLower) return 90;
|
|
7308
|
+
if (name.startsWith(queryLower) || displayName.startsWith(queryLower))
|
|
7309
|
+
return 70;
|
|
7310
|
+
if (realName.startsWith(queryLower)) return 60;
|
|
7311
|
+
const realNameWords = realName.split(/\s+/);
|
|
7312
|
+
if (realNameWords.some((w) => w === queryLower)) return 55;
|
|
7313
|
+
if (realNameWords.some((w) => w.startsWith(queryLower))) return 50;
|
|
7314
|
+
if (name.includes(queryLower) || displayName.includes(queryLower)) return 30;
|
|
7315
|
+
if (realName.includes(queryLower)) return 20;
|
|
7316
|
+
return 0;
|
|
7317
|
+
}
|
|
7318
|
+
async function searchSlackUsers(options) {
|
|
7319
|
+
const {
|
|
7320
|
+
query,
|
|
7321
|
+
limit = 10,
|
|
7322
|
+
maxPages = 3,
|
|
7323
|
+
includeDeleted = false,
|
|
7324
|
+
includeBots = false
|
|
7325
|
+
} = options;
|
|
7326
|
+
const queryLower = query.toLowerCase().trim();
|
|
7327
|
+
const client2 = getSlackClient();
|
|
7328
|
+
const matches = [];
|
|
7329
|
+
let cursor;
|
|
7330
|
+
let pages = 0;
|
|
7331
|
+
let totalScanned = 0;
|
|
7332
|
+
let truncated = false;
|
|
7333
|
+
while (pages < maxPages) {
|
|
7334
|
+
pages++;
|
|
7335
|
+
const result = await withSlackRetries(
|
|
7336
|
+
() => client2.users.list({
|
|
7337
|
+
limit: 200,
|
|
7338
|
+
...cursor ? { cursor } : {}
|
|
7339
|
+
}),
|
|
7340
|
+
3,
|
|
7341
|
+
{ action: "users.list" }
|
|
7342
|
+
);
|
|
7343
|
+
const members = result.members ?? [];
|
|
7344
|
+
totalScanned += members.length;
|
|
7345
|
+
for (const member of members) {
|
|
7346
|
+
if (!includeDeleted && member.deleted) continue;
|
|
7347
|
+
if (!includeBots && member.is_bot) continue;
|
|
7348
|
+
if (member.id === "USLACKBOT") continue;
|
|
7349
|
+
const score = scoreMatch(member, queryLower);
|
|
7350
|
+
if (score > 0) {
|
|
7351
|
+
matches.push({ user: member, score });
|
|
7352
|
+
}
|
|
7353
|
+
}
|
|
7354
|
+
const nextCursor = result.response_metadata?.next_cursor;
|
|
7355
|
+
if (!nextCursor) {
|
|
7356
|
+
break;
|
|
7357
|
+
}
|
|
7358
|
+
cursor = nextCursor;
|
|
7359
|
+
}
|
|
7360
|
+
if (pages >= maxPages && cursor) {
|
|
7361
|
+
truncated = true;
|
|
7362
|
+
}
|
|
7363
|
+
matches.sort((a, b) => {
|
|
7364
|
+
if (b.score !== a.score) return b.score - a.score;
|
|
7365
|
+
return (a.user.name ?? "").localeCompare(b.user.name ?? "");
|
|
7366
|
+
});
|
|
7367
|
+
return {
|
|
7368
|
+
users: matches.slice(0, limit).map((m) => normalizeUser(m.user)),
|
|
7369
|
+
searched_pages: pages,
|
|
7370
|
+
searched_user_count: totalScanned,
|
|
7371
|
+
truncated
|
|
7372
|
+
};
|
|
7373
|
+
}
|
|
7374
|
+
|
|
7375
|
+
// src/chat/tools/slack/user-lookup.ts
|
|
7376
|
+
function createSlackUserLookupTool() {
|
|
7377
|
+
return tool({
|
|
7378
|
+
description: "Look up Slack user profiles by user ID, email, or name search. Use when you need to identify a user, resolve cross-platform identity, or look up profile details like title or status. Returns profile fields including custom fields. For user ID lookup, pass a Slack user ID (e.g. U039RR91S). For search, pass a name query.",
|
|
7379
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
7380
|
+
inputSchema: Type19.Object({
|
|
7381
|
+
user_id: Type19.Optional(
|
|
7382
|
+
Type19.String({
|
|
7383
|
+
minLength: 1,
|
|
7384
|
+
description: "Slack user ID to look up (e.g. U039RR91S). Mutually exclusive with email and query."
|
|
7385
|
+
})
|
|
7386
|
+
),
|
|
7387
|
+
email: Type19.Optional(
|
|
7388
|
+
Type19.String({
|
|
7389
|
+
minLength: 3,
|
|
7390
|
+
description: "Email address to look up. Mutually exclusive with user_id and query."
|
|
7391
|
+
})
|
|
7392
|
+
),
|
|
7393
|
+
query: Type19.Optional(
|
|
7394
|
+
Type19.String({
|
|
7395
|
+
minLength: 2,
|
|
7396
|
+
description: "Name to search for (matches against username, display name, real name). Mutually exclusive with user_id and email."
|
|
7397
|
+
})
|
|
7398
|
+
),
|
|
7399
|
+
limit: Type19.Optional(
|
|
7400
|
+
Type19.Integer({
|
|
7401
|
+
minimum: 1,
|
|
7402
|
+
maximum: 20,
|
|
7403
|
+
description: "Maximum number of results to return for name search. Defaults to 10."
|
|
7404
|
+
})
|
|
7405
|
+
),
|
|
7406
|
+
max_pages: Type19.Optional(
|
|
7407
|
+
Type19.Integer({
|
|
7408
|
+
minimum: 1,
|
|
7409
|
+
maximum: 5,
|
|
7410
|
+
description: "Maximum number of Slack API pages to scan for name search. Defaults to 3."
|
|
7411
|
+
})
|
|
7412
|
+
),
|
|
7413
|
+
include_bots: Type19.Optional(
|
|
7414
|
+
Type19.Boolean({
|
|
7415
|
+
description: "Include bot accounts in name search results. Defaults to false."
|
|
7416
|
+
})
|
|
7417
|
+
)
|
|
7418
|
+
}),
|
|
7419
|
+
execute: async ({
|
|
7420
|
+
user_id,
|
|
7421
|
+
email,
|
|
7422
|
+
query,
|
|
7423
|
+
limit,
|
|
7424
|
+
max_pages,
|
|
7425
|
+
include_bots
|
|
7426
|
+
}) => {
|
|
7427
|
+
const modes = [user_id, email, query].filter(Boolean);
|
|
7428
|
+
if (modes.length === 0) {
|
|
7429
|
+
return {
|
|
7430
|
+
ok: false,
|
|
7431
|
+
error: "Provide exactly one of user_id, email, or query to look up a Slack user."
|
|
7432
|
+
};
|
|
7433
|
+
}
|
|
7434
|
+
if (modes.length > 1) {
|
|
7435
|
+
return {
|
|
7436
|
+
ok: false,
|
|
7437
|
+
error: "Only one of user_id, email, or query can be provided."
|
|
7438
|
+
};
|
|
7439
|
+
}
|
|
7440
|
+
try {
|
|
7441
|
+
if (user_id) {
|
|
7442
|
+
return {
|
|
7443
|
+
ok: true,
|
|
7444
|
+
mode: "user_id",
|
|
7445
|
+
user: await lookupSlackUserProfile(user_id)
|
|
7446
|
+
};
|
|
7447
|
+
}
|
|
7448
|
+
if (email) {
|
|
7449
|
+
const profile = await lookupSlackUserByEmail(email);
|
|
7450
|
+
if (!profile) {
|
|
7451
|
+
return {
|
|
7452
|
+
ok: false,
|
|
7453
|
+
mode: "email",
|
|
7454
|
+
email,
|
|
7455
|
+
error: "No Slack user found with that email address."
|
|
7456
|
+
};
|
|
7457
|
+
}
|
|
7458
|
+
return { ok: true, mode: "email", user: profile };
|
|
7459
|
+
}
|
|
7460
|
+
const result = await searchSlackUsers({
|
|
7461
|
+
query,
|
|
7462
|
+
limit: limit ?? 10,
|
|
7463
|
+
maxPages: max_pages ?? 3,
|
|
7464
|
+
includeBots: include_bots ?? false
|
|
7465
|
+
});
|
|
7466
|
+
return {
|
|
7467
|
+
ok: true,
|
|
7468
|
+
mode: "query",
|
|
7469
|
+
query,
|
|
7470
|
+
count: result.users.length,
|
|
7471
|
+
searched_pages: result.searched_pages,
|
|
7472
|
+
searched_user_count: result.searched_user_count,
|
|
7473
|
+
truncated: result.truncated,
|
|
7474
|
+
users: result.users
|
|
7475
|
+
};
|
|
7476
|
+
} catch (error) {
|
|
7477
|
+
if (error instanceof SlackActionError) {
|
|
7478
|
+
return {
|
|
7479
|
+
ok: false,
|
|
7480
|
+
error: error.message,
|
|
7481
|
+
slack_error: error.apiError,
|
|
7482
|
+
code: error.code,
|
|
7483
|
+
...error.needed ? { needed_scope: error.needed } : {}
|
|
7484
|
+
};
|
|
7485
|
+
}
|
|
7486
|
+
throw error;
|
|
7487
|
+
}
|
|
7488
|
+
}
|
|
7489
|
+
});
|
|
7490
|
+
}
|
|
7491
|
+
|
|
7492
|
+
// src/chat/tools/system-time.ts
|
|
7493
|
+
import { Type as Type20 } from "@sinclair/typebox";
|
|
7359
7494
|
function createSystemTimeTool() {
|
|
7360
7495
|
return tool({
|
|
7361
7496
|
description: "Return current system time in UTC and local ISO formats. Use when the user asks for current time/date context. Do not use as a substitute for historical or timezone-conversion research.",
|
|
7362
7497
|
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
7363
|
-
inputSchema:
|
|
7498
|
+
inputSchema: Type20.Object({}),
|
|
7364
7499
|
execute: async () => {
|
|
7365
7500
|
const now = /* @__PURE__ */ new Date();
|
|
7366
7501
|
return {
|
|
@@ -7378,7 +7513,7 @@ function createSystemTimeTool() {
|
|
|
7378
7513
|
import {
|
|
7379
7514
|
Agent
|
|
7380
7515
|
} from "@mariozechner/pi-agent-core";
|
|
7381
|
-
import { Type as
|
|
7516
|
+
import { Type as Type21 } from "@sinclair/typebox";
|
|
7382
7517
|
|
|
7383
7518
|
// src/chat/respond-helpers.ts
|
|
7384
7519
|
var MAX_INLINE_ATTACHMENT_BASE64_CHARS = 12e4;
|
|
@@ -7666,12 +7801,12 @@ function createAdvisorTool(context) {
|
|
|
7666
7801
|
const spanContext = context.logContext ?? {};
|
|
7667
7802
|
return tool({
|
|
7668
7803
|
description: ADVISOR_TOOL_DESCRIPTION,
|
|
7669
|
-
inputSchema:
|
|
7670
|
-
question:
|
|
7804
|
+
inputSchema: Type21.Object({
|
|
7805
|
+
question: Type21.String({
|
|
7671
7806
|
minLength: 1,
|
|
7672
7807
|
description: "Focused advisor question or decision point."
|
|
7673
7808
|
}),
|
|
7674
|
-
context:
|
|
7809
|
+
context: Type21.String({
|
|
7675
7810
|
minLength: 1,
|
|
7676
7811
|
description: "Curated evidence packet: relevant requirements, constraints, current plan, alternatives, code snippets, diffs, command output, and open questions."
|
|
7677
7812
|
})
|
|
@@ -7783,7 +7918,7 @@ function createAdvisorTool(context) {
|
|
|
7783
7918
|
}
|
|
7784
7919
|
|
|
7785
7920
|
// src/chat/tools/web/fetch-tool.ts
|
|
7786
|
-
import { Type as
|
|
7921
|
+
import { Type as Type22 } from "@sinclair/typebox";
|
|
7787
7922
|
|
|
7788
7923
|
// src/chat/tools/web/constants.ts
|
|
7789
7924
|
var USER_AGENT = "junior-bot/0.1";
|
|
@@ -8136,13 +8271,13 @@ function createWebFetchTool(hooks) {
|
|
|
8136
8271
|
destructiveHint: false,
|
|
8137
8272
|
openWorldHint: true
|
|
8138
8273
|
},
|
|
8139
|
-
inputSchema:
|
|
8140
|
-
url:
|
|
8274
|
+
inputSchema: Type22.Object({
|
|
8275
|
+
url: Type22.String({
|
|
8141
8276
|
minLength: 1,
|
|
8142
8277
|
description: "HTTP(S) URL to fetch."
|
|
8143
8278
|
}),
|
|
8144
|
-
max_chars:
|
|
8145
|
-
|
|
8279
|
+
max_chars: Type22.Optional(
|
|
8280
|
+
Type22.Integer({
|
|
8146
8281
|
minimum: 500,
|
|
8147
8282
|
maximum: MAX_FETCH_CHARS,
|
|
8148
8283
|
description: "Optional maximum number of extracted characters to return."
|
|
@@ -8202,7 +8337,7 @@ function createWebFetchTool(hooks) {
|
|
|
8202
8337
|
// src/chat/tools/web/search.ts
|
|
8203
8338
|
import { generateText } from "ai";
|
|
8204
8339
|
import { createGatewayProvider } from "@ai-sdk/gateway";
|
|
8205
|
-
import { Type as
|
|
8340
|
+
import { Type as Type23 } from "@sinclair/typebox";
|
|
8206
8341
|
var SEARCH_TIMEOUT_MS = 6e4;
|
|
8207
8342
|
var MAX_RESULTS2 = 5;
|
|
8208
8343
|
var DEFAULT_SEARCH_MODEL = "xai/grok-4-fast-reasoning";
|
|
@@ -8250,14 +8385,14 @@ function createWebSearchTool() {
|
|
|
8250
8385
|
destructiveHint: false,
|
|
8251
8386
|
openWorldHint: true
|
|
8252
8387
|
},
|
|
8253
|
-
inputSchema:
|
|
8254
|
-
query:
|
|
8388
|
+
inputSchema: Type23.Object({
|
|
8389
|
+
query: Type23.String({
|
|
8255
8390
|
minLength: 1,
|
|
8256
8391
|
maxLength: 500,
|
|
8257
8392
|
description: "Search query."
|
|
8258
8393
|
}),
|
|
8259
|
-
max_results:
|
|
8260
|
-
|
|
8394
|
+
max_results: Type23.Optional(
|
|
8395
|
+
Type23.Integer({
|
|
8261
8396
|
minimum: 1,
|
|
8262
8397
|
maximum: MAX_RESULTS2,
|
|
8263
8398
|
description: "Max results to return."
|
|
@@ -8326,20 +8461,20 @@ function createWebSearchTool() {
|
|
|
8326
8461
|
}
|
|
8327
8462
|
|
|
8328
8463
|
// src/chat/tools/sandbox/write-file.ts
|
|
8329
|
-
import { Type as
|
|
8464
|
+
import { Type as Type24 } from "@sinclair/typebox";
|
|
8330
8465
|
function createWriteFileTool() {
|
|
8331
8466
|
return tool({
|
|
8332
8467
|
description: "Write UTF-8 content to a file in the sandbox workspace. Use for intentional file creation or replacement after validation. Do not use for exploratory analysis-only turns.",
|
|
8333
8468
|
promptSnippet: "new file or deliberate full-file replacement",
|
|
8334
8469
|
promptGuidelines: ["targeted existing-file changes: editFile"],
|
|
8335
8470
|
executionMode: "sequential",
|
|
8336
|
-
inputSchema:
|
|
8471
|
+
inputSchema: Type24.Object(
|
|
8337
8472
|
{
|
|
8338
|
-
path:
|
|
8473
|
+
path: Type24.String({
|
|
8339
8474
|
minLength: 1,
|
|
8340
8475
|
description: "Path to write in the sandbox workspace."
|
|
8341
8476
|
}),
|
|
8342
|
-
content:
|
|
8477
|
+
content: Type24.String({
|
|
8343
8478
|
description: "UTF-8 file content to write."
|
|
8344
8479
|
})
|
|
8345
8480
|
},
|
|
@@ -8413,6 +8548,7 @@ function createTools(availableSkills, hooks = {}, context) {
|
|
|
8413
8548
|
slackCanvasRead: createSlackCanvasReadTool(),
|
|
8414
8549
|
slackCanvasUpdate: createSlackCanvasUpdateTool(state, context),
|
|
8415
8550
|
slackThreadRead: createSlackThreadReadTool(context),
|
|
8551
|
+
slackUserLookup: createSlackUserLookupTool(),
|
|
8416
8552
|
slackListCreate: createSlackListCreateTool(state),
|
|
8417
8553
|
slackListAddItems: createSlackListAddItemsTool(state),
|
|
8418
8554
|
slackListGetItems: createSlackListGetItemsTool(state),
|
|
@@ -8463,6 +8599,175 @@ function resolveChannelCapabilities(channelId) {
|
|
|
8463
8599
|
// src/chat/sandbox/sandbox.ts
|
|
8464
8600
|
import fs4 from "fs/promises";
|
|
8465
8601
|
|
|
8602
|
+
// src/chat/sandbox/egress-policy.ts
|
|
8603
|
+
var SANDBOX_EGRESS_PROXY_PATH = "/api/internal/sandbox-egress";
|
|
8604
|
+
function matchesSandboxEgressDomain(host, domain) {
|
|
8605
|
+
return host.toLowerCase() === domain.toLowerCase();
|
|
8606
|
+
}
|
|
8607
|
+
function manifestDomains(manifest) {
|
|
8608
|
+
const domains = /* @__PURE__ */ new Set([
|
|
8609
|
+
...manifest.credentials?.domains ?? [],
|
|
8610
|
+
...manifest.domains ?? []
|
|
8611
|
+
]);
|
|
8612
|
+
return [...domains].sort((left, right) => left.localeCompare(right));
|
|
8613
|
+
}
|
|
8614
|
+
function providerEntries() {
|
|
8615
|
+
return getPluginProviders().map((plugin) => ({
|
|
8616
|
+
provider: plugin.manifest.name,
|
|
8617
|
+
domains: manifestDomains(plugin.manifest)
|
|
8618
|
+
})).filter((entry) => entry.domains.length > 0).sort((left, right) => left.provider.localeCompare(right.provider));
|
|
8619
|
+
}
|
|
8620
|
+
function resolveSandboxEgressProviderForHost(host) {
|
|
8621
|
+
return providerEntries().find(
|
|
8622
|
+
(entry) => entry.domains.some((domain) => matchesSandboxEgressDomain(host, domain))
|
|
8623
|
+
)?.provider;
|
|
8624
|
+
}
|
|
8625
|
+
function proxyUrl(egressId) {
|
|
8626
|
+
const baseUrl = resolveBaseUrl();
|
|
8627
|
+
if (!baseUrl) {
|
|
8628
|
+
return void 0;
|
|
8629
|
+
}
|
|
8630
|
+
const url = new URL(
|
|
8631
|
+
`${SANDBOX_EGRESS_PROXY_PATH}/${encodeURIComponent(egressId)}`,
|
|
8632
|
+
baseUrl
|
|
8633
|
+
);
|
|
8634
|
+
return url.toString();
|
|
8635
|
+
}
|
|
8636
|
+
function buildSandboxEgressNetworkPolicy(egressId) {
|
|
8637
|
+
const entries = providerEntries();
|
|
8638
|
+
if (entries.length === 0) {
|
|
8639
|
+
return void 0;
|
|
8640
|
+
}
|
|
8641
|
+
const forwardURL = proxyUrl(egressId);
|
|
8642
|
+
if (!forwardURL) {
|
|
8643
|
+
throw new Error(
|
|
8644
|
+
"Cannot determine base URL for sandbox credential egress (set JUNIOR_BASE_URL or deploy to Vercel)"
|
|
8645
|
+
);
|
|
8646
|
+
}
|
|
8647
|
+
const allow = {
|
|
8648
|
+
"*": []
|
|
8649
|
+
};
|
|
8650
|
+
for (const entry of entries) {
|
|
8651
|
+
for (const domain of entry.domains) {
|
|
8652
|
+
allow[domain] = [{ forwardURL }];
|
|
8653
|
+
}
|
|
8654
|
+
}
|
|
8655
|
+
return { allow };
|
|
8656
|
+
}
|
|
8657
|
+
async function resolveSandboxCommandEnvironment() {
|
|
8658
|
+
const env = {};
|
|
8659
|
+
for (const plugin of getPluginProviders().sort(
|
|
8660
|
+
(left, right) => left.manifest.name.localeCompare(right.manifest.name)
|
|
8661
|
+
)) {
|
|
8662
|
+
Object.assign(env, resolvePluginCommandEnv(plugin.manifest));
|
|
8663
|
+
const credentials = plugin.manifest.credentials;
|
|
8664
|
+
if (credentials) {
|
|
8665
|
+
env[credentials.authTokenEnv] = resolveAuthTokenPlaceholder(credentials);
|
|
8666
|
+
}
|
|
8667
|
+
}
|
|
8668
|
+
return env;
|
|
8669
|
+
}
|
|
8670
|
+
|
|
8671
|
+
// src/chat/sandbox/egress-session.ts
|
|
8672
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
8673
|
+
var SANDBOX_EGRESS_SESSION_PREFIX = "sandbox-egress-session";
|
|
8674
|
+
var SANDBOX_EGRESS_LEASE_PREFIX = "sandbox-egress-lease";
|
|
8675
|
+
var DEFAULT_SESSION_TTL_MS = 30 * 60 * 1e3;
|
|
8676
|
+
function sessionKey2(egressId) {
|
|
8677
|
+
return `${SANDBOX_EGRESS_SESSION_PREFIX}:${egressId}`;
|
|
8678
|
+
}
|
|
8679
|
+
function leaseKey(egressId, provider, session) {
|
|
8680
|
+
return `${SANDBOX_EGRESS_LEASE_PREFIX}:${egressId}:${provider}:${session.requesterId}:${session.activationId}`;
|
|
8681
|
+
}
|
|
8682
|
+
function parseSession(value) {
|
|
8683
|
+
if (!value || typeof value !== "object") {
|
|
8684
|
+
return void 0;
|
|
8685
|
+
}
|
|
8686
|
+
const record = value;
|
|
8687
|
+
if (typeof record.requesterId !== "string" || typeof record.expiresAtMs !== "number" || !Number.isFinite(record.expiresAtMs) || typeof record.activationId !== "string" || !record.activationId) {
|
|
8688
|
+
return void 0;
|
|
8689
|
+
}
|
|
8690
|
+
if (record.expiresAtMs <= Date.now()) {
|
|
8691
|
+
return void 0;
|
|
8692
|
+
}
|
|
8693
|
+
return {
|
|
8694
|
+
requesterId: record.requesterId,
|
|
8695
|
+
expiresAtMs: record.expiresAtMs,
|
|
8696
|
+
activationId: record.activationId
|
|
8697
|
+
};
|
|
8698
|
+
}
|
|
8699
|
+
function parseLease(value) {
|
|
8700
|
+
if (!value || typeof value !== "object") {
|
|
8701
|
+
return void 0;
|
|
8702
|
+
}
|
|
8703
|
+
const record = value;
|
|
8704
|
+
if (typeof record.provider !== "string" || typeof record.expiresAt !== "string" || !Array.isArray(record.headerTransforms)) {
|
|
8705
|
+
return void 0;
|
|
8706
|
+
}
|
|
8707
|
+
const expiresAtMs = Date.parse(record.expiresAt);
|
|
8708
|
+
if (!Number.isFinite(expiresAtMs) || expiresAtMs <= Date.now()) {
|
|
8709
|
+
return void 0;
|
|
8710
|
+
}
|
|
8711
|
+
const headerTransforms = record.headerTransforms.filter(
|
|
8712
|
+
(transform) => Boolean(
|
|
8713
|
+
transform && typeof transform.domain === "string" && transform.headers && typeof transform.headers === "object"
|
|
8714
|
+
)
|
|
8715
|
+
);
|
|
8716
|
+
if (headerTransforms.length === 0) {
|
|
8717
|
+
return void 0;
|
|
8718
|
+
}
|
|
8719
|
+
return {
|
|
8720
|
+
provider: record.provider,
|
|
8721
|
+
expiresAt: record.expiresAt,
|
|
8722
|
+
headerTransforms
|
|
8723
|
+
};
|
|
8724
|
+
}
|
|
8725
|
+
async function upsertSandboxEgressSession(input) {
|
|
8726
|
+
const state = getStateAdapter();
|
|
8727
|
+
await state.connect();
|
|
8728
|
+
const ttlMs = Math.max(1, input.ttlMs ?? DEFAULT_SESSION_TTL_MS);
|
|
8729
|
+
const now = Date.now();
|
|
8730
|
+
const session = {
|
|
8731
|
+
requesterId: input.requesterId,
|
|
8732
|
+
expiresAtMs: now + ttlMs,
|
|
8733
|
+
activationId: randomUUID3()
|
|
8734
|
+
};
|
|
8735
|
+
await state.set(sessionKey2(input.egressId), session, ttlMs);
|
|
8736
|
+
}
|
|
8737
|
+
async function clearSandboxEgressSession(egressId) {
|
|
8738
|
+
const state = getStateAdapter();
|
|
8739
|
+
await state.connect();
|
|
8740
|
+
await state.delete(sessionKey2(egressId));
|
|
8741
|
+
}
|
|
8742
|
+
async function getSandboxEgressSession(egressId) {
|
|
8743
|
+
const state = getStateAdapter();
|
|
8744
|
+
await state.connect();
|
|
8745
|
+
return parseSession(await state.get(sessionKey2(egressId)));
|
|
8746
|
+
}
|
|
8747
|
+
async function setSandboxEgressCredentialLease(egressId, session, lease) {
|
|
8748
|
+
const leaseExpiresAtMs = Date.parse(lease.expiresAt);
|
|
8749
|
+
if (!Number.isFinite(leaseExpiresAtMs) || leaseExpiresAtMs <= Date.now()) {
|
|
8750
|
+
return;
|
|
8751
|
+
}
|
|
8752
|
+
const ttlMs = Math.max(
|
|
8753
|
+
1,
|
|
8754
|
+
Math.min(leaseExpiresAtMs, session.expiresAtMs) - Date.now()
|
|
8755
|
+
);
|
|
8756
|
+
const state = getStateAdapter();
|
|
8757
|
+
await state.connect();
|
|
8758
|
+
await state.set(leaseKey(egressId, lease.provider, session), lease, ttlMs);
|
|
8759
|
+
}
|
|
8760
|
+
async function getSandboxEgressCredentialLease(egressId, provider, session) {
|
|
8761
|
+
const state = getStateAdapter();
|
|
8762
|
+
await state.connect();
|
|
8763
|
+
return parseLease(await state.get(leaseKey(egressId, provider, session)));
|
|
8764
|
+
}
|
|
8765
|
+
async function clearSandboxEgressCredentialLease(egressId, provider, session) {
|
|
8766
|
+
const state = getStateAdapter();
|
|
8767
|
+
await state.connect();
|
|
8768
|
+
await state.delete(leaseKey(egressId, provider, session));
|
|
8769
|
+
}
|
|
8770
|
+
|
|
8466
8771
|
// src/chat/sandbox/http-error-details.ts
|
|
8467
8772
|
var DEFAULT_PREVIEW_LIMIT = 512;
|
|
8468
8773
|
function toTrimmedString(value, maxChars) {
|
|
@@ -8665,6 +8970,7 @@ function throwSandboxOperationError(action, error, includeMissingPath = false) {
|
|
|
8665
8970
|
}
|
|
8666
8971
|
|
|
8667
8972
|
// src/chat/sandbox/session.ts
|
|
8973
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
8668
8974
|
import { Sandbox } from "@vercel/sandbox";
|
|
8669
8975
|
import { createBashTool as createBashTool2 } from "bash-tool";
|
|
8670
8976
|
|
|
@@ -9238,27 +9544,39 @@ var SANDBOX_RUNTIME = "node22";
|
|
|
9238
9544
|
var SANDBOX_RUNTIME_BIN_DIR = `${SANDBOX_WORKSPACE_ROOT}/.junior/bin`;
|
|
9239
9545
|
var SNAPSHOT_BOOT_RETRY_COUNT = 3;
|
|
9240
9546
|
var SNAPSHOT_BOOT_RETRY_DELAY_MS = 1e3;
|
|
9241
|
-
|
|
9242
|
-
|
|
9243
|
-
const existingAllowRaw = basePolicy.allow;
|
|
9244
|
-
const existingAllow = existingAllowRaw && typeof existingAllowRaw === "object" && !Array.isArray(existingAllowRaw) ? Object.fromEntries(
|
|
9245
|
-
Object.entries(existingAllowRaw).map(
|
|
9246
|
-
([domain, rules]) => [
|
|
9247
|
-
domain,
|
|
9248
|
-
Array.isArray(rules) ? [...rules] : []
|
|
9249
|
-
]
|
|
9250
|
-
)
|
|
9251
|
-
) : { "*": [] };
|
|
9252
|
-
for (const transform of headerTransforms) {
|
|
9253
|
-
const currentRules = existingAllow[transform.domain] ?? [];
|
|
9254
|
-
existingAllow[transform.domain] = [
|
|
9255
|
-
...currentRules,
|
|
9256
|
-
{ transform: [{ headers: transform.headers }] }
|
|
9257
|
-
];
|
|
9258
|
-
}
|
|
9547
|
+
var SANDBOX_NAME_PREFIX = "junior-";
|
|
9548
|
+
function createBashToolSandboxAdapter(sandbox) {
|
|
9259
9549
|
return {
|
|
9260
|
-
|
|
9261
|
-
|
|
9550
|
+
async executeCommand(command) {
|
|
9551
|
+
const result = await sandbox.runCommand({
|
|
9552
|
+
cmd: "bash",
|
|
9553
|
+
args: ["-c", command]
|
|
9554
|
+
});
|
|
9555
|
+
const [stdout, stderr] = await Promise.all([
|
|
9556
|
+
result.stdout(),
|
|
9557
|
+
result.stderr()
|
|
9558
|
+
]);
|
|
9559
|
+
return {
|
|
9560
|
+
stdout,
|
|
9561
|
+
stderr,
|
|
9562
|
+
exitCode: result.exitCode
|
|
9563
|
+
};
|
|
9564
|
+
},
|
|
9565
|
+
async readFile(filePath) {
|
|
9566
|
+
const content = await sandbox.readFileToBuffer({ path: filePath });
|
|
9567
|
+
if (content == null) {
|
|
9568
|
+
throw new Error(`File not found: ${filePath}`);
|
|
9569
|
+
}
|
|
9570
|
+
return content.toString("utf8");
|
|
9571
|
+
},
|
|
9572
|
+
async writeFiles(files) {
|
|
9573
|
+
await sandbox.writeFiles(
|
|
9574
|
+
files.map((file) => ({
|
|
9575
|
+
path: file.path,
|
|
9576
|
+
content: file.content
|
|
9577
|
+
}))
|
|
9578
|
+
);
|
|
9579
|
+
}
|
|
9262
9580
|
};
|
|
9263
9581
|
}
|
|
9264
9582
|
function truncateOutput(output, maxLength) {
|
|
@@ -9291,23 +9609,31 @@ function createSandboxSessionManager(options) {
|
|
|
9291
9609
|
let availableSkills = [];
|
|
9292
9610
|
let availableReferenceFiles = [];
|
|
9293
9611
|
let toolExecutors;
|
|
9612
|
+
let appliedNetworkPolicyKey;
|
|
9294
9613
|
const timeoutMs = options?.timeoutMs ?? 1e3 * 60 * 30;
|
|
9295
9614
|
const traceContext = options?.traceContext ?? {};
|
|
9296
9615
|
const dependencyProfileHash = getRuntimeDependencyProfileHash(SANDBOX_RUNTIME);
|
|
9616
|
+
const resolveCommandEnv = options?.commandEnv ?? (async () => ({}));
|
|
9297
9617
|
const withSandboxSpan = (name, op, attributes, callback) => withSpan(name, op, traceContext, callback, attributes);
|
|
9298
9618
|
const clearSession = () => {
|
|
9299
9619
|
sandbox = null;
|
|
9300
9620
|
sandboxIdHint = void 0;
|
|
9301
9621
|
toolExecutors = void 0;
|
|
9622
|
+
appliedNetworkPolicyKey = void 0;
|
|
9623
|
+
};
|
|
9624
|
+
const createSandboxName = () => `${SANDBOX_NAME_PREFIX}${randomUUID4()}`;
|
|
9625
|
+
const preflightNetworkPolicy = (sandboxName) => {
|
|
9626
|
+
return options?.createNetworkPolicy?.(sandboxName);
|
|
9302
9627
|
};
|
|
9303
9628
|
const rememberSandbox = async (nextSandbox) => {
|
|
9304
9629
|
sandbox = nextSandbox;
|
|
9305
9630
|
sandboxIdHint = nextSandbox.sandboxId;
|
|
9306
9631
|
toolExecutors = void 0;
|
|
9307
|
-
|
|
9308
|
-
sandboxId:
|
|
9632
|
+
const acquired = {
|
|
9633
|
+
sandboxId: sandboxIdHint,
|
|
9309
9634
|
...dependencyProfileHash ? { sandboxDependencyProfileHash: dependencyProfileHash } : {}
|
|
9310
|
-
}
|
|
9635
|
+
};
|
|
9636
|
+
await options?.onSandboxAcquired?.(acquired);
|
|
9311
9637
|
return nextSandbox;
|
|
9312
9638
|
};
|
|
9313
9639
|
const failSetup = (error) => {
|
|
@@ -9322,6 +9648,30 @@ function createSandboxSessionManager(options) {
|
|
|
9322
9648
|
runtimeBinDir: SANDBOX_RUNTIME_BIN_DIR
|
|
9323
9649
|
});
|
|
9324
9650
|
};
|
|
9651
|
+
const refreshNetworkPolicy = async (targetSandbox) => {
|
|
9652
|
+
const networkPolicy = options?.createNetworkPolicy?.(
|
|
9653
|
+
targetSandbox.sandboxEgressId
|
|
9654
|
+
);
|
|
9655
|
+
if (!networkPolicy) {
|
|
9656
|
+
return;
|
|
9657
|
+
}
|
|
9658
|
+
const networkPolicyKey = JSON.stringify(networkPolicy);
|
|
9659
|
+
if (appliedNetworkPolicyKey === networkPolicyKey) {
|
|
9660
|
+
return;
|
|
9661
|
+
}
|
|
9662
|
+
await withSandboxSpan(
|
|
9663
|
+
"sandbox.network_policy.update",
|
|
9664
|
+
"sandbox.update",
|
|
9665
|
+
{
|
|
9666
|
+
"app.sandbox.reused": true,
|
|
9667
|
+
"app.sandbox.source": "id_hint"
|
|
9668
|
+
},
|
|
9669
|
+
async () => {
|
|
9670
|
+
await targetSandbox.update({ networkPolicy });
|
|
9671
|
+
}
|
|
9672
|
+
);
|
|
9673
|
+
appliedNetworkPolicyKey = networkPolicyKey;
|
|
9674
|
+
};
|
|
9325
9675
|
const ensureSandboxReachable = async (targetSandbox, source) => {
|
|
9326
9676
|
await withSandboxSpan(
|
|
9327
9677
|
"sandbox.reuse_probe",
|
|
@@ -9341,23 +9691,6 @@ function createSandboxSessionManager(options) {
|
|
|
9341
9691
|
}
|
|
9342
9692
|
);
|
|
9343
9693
|
};
|
|
9344
|
-
const invalidateSandboxInstance = async (targetSandbox, reason) => {
|
|
9345
|
-
if (sandbox === targetSandbox) {
|
|
9346
|
-
clearSession();
|
|
9347
|
-
}
|
|
9348
|
-
logWarn(
|
|
9349
|
-
"sandbox_network_policy_restore_failed",
|
|
9350
|
-
traceContext,
|
|
9351
|
-
{
|
|
9352
|
-
"exception.message": reason instanceof Error ? reason.message : String(reason)
|
|
9353
|
-
},
|
|
9354
|
-
"Sandbox network policy restore failed; discarding sandbox instance"
|
|
9355
|
-
);
|
|
9356
|
-
try {
|
|
9357
|
-
await targetSandbox.stop({ blocking: true });
|
|
9358
|
-
} catch {
|
|
9359
|
-
}
|
|
9360
|
-
};
|
|
9361
9694
|
const recreateUnavailableSandbox = async (source) => {
|
|
9362
9695
|
setSpanAttributes({
|
|
9363
9696
|
"app.sandbox.recovery.attempted": true,
|
|
@@ -9370,17 +9703,22 @@ function createSandboxSessionManager(options) {
|
|
|
9370
9703
|
});
|
|
9371
9704
|
return replacement;
|
|
9372
9705
|
};
|
|
9373
|
-
const createSandboxFromSnapshot = async (snapshotId, sandboxCredentials) => {
|
|
9706
|
+
const createSandboxFromSnapshot = async (snapshotId, sandboxCredentials, initialSandboxName) => {
|
|
9374
9707
|
for (let attempt = 0; attempt < SNAPSHOT_BOOT_RETRY_COUNT; attempt += 1) {
|
|
9708
|
+
const sandboxName = attempt === 0 ? initialSandboxName : createSandboxName();
|
|
9709
|
+
const networkPolicy = preflightNetworkPolicy(sandboxName);
|
|
9375
9710
|
try {
|
|
9376
|
-
return
|
|
9377
|
-
|
|
9378
|
-
|
|
9379
|
-
|
|
9380
|
-
|
|
9381
|
-
|
|
9382
|
-
|
|
9383
|
-
|
|
9711
|
+
return createSandboxInstance(
|
|
9712
|
+
await Sandbox.create({
|
|
9713
|
+
timeout: timeoutMs,
|
|
9714
|
+
...networkPolicy ? { name: sandboxName, persistent: false, networkPolicy } : {},
|
|
9715
|
+
source: {
|
|
9716
|
+
type: "snapshot",
|
|
9717
|
+
snapshotId
|
|
9718
|
+
},
|
|
9719
|
+
...sandboxCredentials ?? {}
|
|
9720
|
+
})
|
|
9721
|
+
);
|
|
9384
9722
|
} catch (error) {
|
|
9385
9723
|
if (!isSnapshottingError(error) || attempt === SNAPSHOT_BOOT_RETRY_COUNT - 1) {
|
|
9386
9724
|
throw error;
|
|
@@ -9405,18 +9743,23 @@ function createSandboxSessionManager(options) {
|
|
|
9405
9743
|
});
|
|
9406
9744
|
};
|
|
9407
9745
|
const createSandboxFromResolvedSnapshot = async (params) => {
|
|
9408
|
-
const { runtime, snapshot, sandboxCredentials } = params;
|
|
9746
|
+
const { runtime, snapshot, sandboxCredentials, sandboxName } = params;
|
|
9409
9747
|
if (!snapshot.snapshotId) {
|
|
9410
|
-
|
|
9411
|
-
|
|
9412
|
-
|
|
9413
|
-
|
|
9414
|
-
|
|
9748
|
+
const networkPolicy = preflightNetworkPolicy(sandboxName);
|
|
9749
|
+
return createSandboxInstance(
|
|
9750
|
+
await Sandbox.create({
|
|
9751
|
+
timeout: timeoutMs,
|
|
9752
|
+
runtime,
|
|
9753
|
+
...networkPolicy ? { name: sandboxName, persistent: false, networkPolicy } : {},
|
|
9754
|
+
...sandboxCredentials ?? {}
|
|
9755
|
+
})
|
|
9756
|
+
);
|
|
9415
9757
|
}
|
|
9416
9758
|
try {
|
|
9417
9759
|
return await createSandboxFromSnapshot(
|
|
9418
9760
|
snapshot.snapshotId,
|
|
9419
|
-
sandboxCredentials
|
|
9761
|
+
sandboxCredentials,
|
|
9762
|
+
sandboxName
|
|
9420
9763
|
);
|
|
9421
9764
|
} catch (error) {
|
|
9422
9765
|
if (!isSnapshotMissingError(error)) {
|
|
@@ -9436,13 +9779,15 @@ function createSandboxSessionManager(options) {
|
|
|
9436
9779
|
}
|
|
9437
9780
|
return await createSandboxFromSnapshot(
|
|
9438
9781
|
rebuiltSnapshot.snapshotId,
|
|
9439
|
-
sandboxCredentials
|
|
9782
|
+
sandboxCredentials,
|
|
9783
|
+
sandboxName
|
|
9440
9784
|
);
|
|
9441
9785
|
}
|
|
9442
9786
|
};
|
|
9443
9787
|
const createFreshSandbox = async () => {
|
|
9444
9788
|
const runtime = SANDBOX_RUNTIME;
|
|
9445
9789
|
const sandboxCredentials = getVercelSandboxCredentials();
|
|
9790
|
+
const sandboxName = createSandboxName();
|
|
9446
9791
|
let createdSandbox;
|
|
9447
9792
|
try {
|
|
9448
9793
|
createdSandbox = await withSandboxSpan(
|
|
@@ -9462,7 +9807,8 @@ function createSandboxSessionManager(options) {
|
|
|
9462
9807
|
return await createSandboxFromResolvedSnapshot({
|
|
9463
9808
|
runtime,
|
|
9464
9809
|
snapshot,
|
|
9465
|
-
sandboxCredentials
|
|
9810
|
+
sandboxCredentials,
|
|
9811
|
+
sandboxName
|
|
9466
9812
|
});
|
|
9467
9813
|
}
|
|
9468
9814
|
);
|
|
@@ -9470,6 +9816,7 @@ function createSandboxSessionManager(options) {
|
|
|
9470
9816
|
return failSetup(error);
|
|
9471
9817
|
}
|
|
9472
9818
|
try {
|
|
9819
|
+
await refreshNetworkPolicy(createdSandbox);
|
|
9473
9820
|
await syncSkills(createdSandbox);
|
|
9474
9821
|
} catch (error) {
|
|
9475
9822
|
return failSetup(error);
|
|
@@ -9497,6 +9844,7 @@ function createSandboxSessionManager(options) {
|
|
|
9497
9844
|
}
|
|
9498
9845
|
try {
|
|
9499
9846
|
await ensureSandboxReachable(cachedSandbox, "memory");
|
|
9847
|
+
await refreshNetworkPolicy(cachedSandbox);
|
|
9500
9848
|
return cachedSandbox;
|
|
9501
9849
|
} catch (error) {
|
|
9502
9850
|
if (isSandboxUnavailableError(error)) {
|
|
@@ -9519,15 +9867,19 @@ function createSandboxSessionManager(options) {
|
|
|
9519
9867
|
"app.sandbox.reused": true,
|
|
9520
9868
|
"app.sandbox.source": "id_hint"
|
|
9521
9869
|
},
|
|
9522
|
-
async () =>
|
|
9523
|
-
|
|
9524
|
-
|
|
9525
|
-
|
|
9870
|
+
async () => createSandboxInstance(
|
|
9871
|
+
await Sandbox.get({
|
|
9872
|
+
name: sandboxIdHint,
|
|
9873
|
+
resume: true,
|
|
9874
|
+
...sandboxCredentials ?? {}
|
|
9875
|
+
})
|
|
9876
|
+
)
|
|
9526
9877
|
);
|
|
9527
9878
|
} catch {
|
|
9528
9879
|
return null;
|
|
9529
9880
|
}
|
|
9530
9881
|
try {
|
|
9882
|
+
await refreshNetworkPolicy(hintedSandbox);
|
|
9531
9883
|
await syncSkills(hintedSandbox);
|
|
9532
9884
|
return await rememberSandbox(hintedSandbox);
|
|
9533
9885
|
} catch (error) {
|
|
@@ -9582,37 +9934,6 @@ function createSandboxSessionManager(options) {
|
|
|
9582
9934
|
stderrTruncated: stderr.truncated
|
|
9583
9935
|
};
|
|
9584
9936
|
};
|
|
9585
|
-
const withTemporaryHeaderTransforms = async (sandboxInstance, headerTransforms, callback) => {
|
|
9586
|
-
if (!headerTransforms || headerTransforms.length === 0) {
|
|
9587
|
-
return await callback();
|
|
9588
|
-
}
|
|
9589
|
-
const restoreNetworkPolicy = sandboxInstance.networkPolicy ?? "allow-all";
|
|
9590
|
-
const policy = mergeNetworkPolicyWithHeaderTransforms(
|
|
9591
|
-
restoreNetworkPolicy,
|
|
9592
|
-
headerTransforms
|
|
9593
|
-
);
|
|
9594
|
-
await sandboxInstance.updateNetworkPolicy(policy);
|
|
9595
|
-
let callbackError;
|
|
9596
|
-
let restoreError;
|
|
9597
|
-
let result;
|
|
9598
|
-
try {
|
|
9599
|
-
result = await callback();
|
|
9600
|
-
} catch (error) {
|
|
9601
|
-
callbackError = error;
|
|
9602
|
-
throw error;
|
|
9603
|
-
} finally {
|
|
9604
|
-
try {
|
|
9605
|
-
await sandboxInstance.updateNetworkPolicy(restoreNetworkPolicy);
|
|
9606
|
-
} catch (error) {
|
|
9607
|
-
restoreError = error;
|
|
9608
|
-
await invalidateSandboxInstance(sandboxInstance, error);
|
|
9609
|
-
}
|
|
9610
|
-
}
|
|
9611
|
-
if (restoreError && !callbackError) {
|
|
9612
|
-
throw restoreError;
|
|
9613
|
-
}
|
|
9614
|
-
return result;
|
|
9615
|
-
};
|
|
9616
9937
|
const extendKeepAlive = async (activeSandbox) => {
|
|
9617
9938
|
const keepAliveMs = parseKeepAliveMs();
|
|
9618
9939
|
if (keepAliveMs === 0) {
|
|
@@ -9633,6 +9954,7 @@ function createSandboxSessionManager(options) {
|
|
|
9633
9954
|
}
|
|
9634
9955
|
};
|
|
9635
9956
|
const buildToolExecutors = async (sandboxInstance) => {
|
|
9957
|
+
const activeSandboxId = sandboxInstance.sandboxId;
|
|
9636
9958
|
const toolkit = await withSandboxSpan(
|
|
9637
9959
|
"sandbox.bash_tool.init",
|
|
9638
9960
|
"sandbox.tool.init",
|
|
@@ -9641,7 +9963,7 @@ function createSandboxSessionManager(options) {
|
|
|
9641
9963
|
"app.sandbox.destination": SANDBOX_WORKSPACE_ROOT
|
|
9642
9964
|
},
|
|
9643
9965
|
async () => await createBashTool2({
|
|
9644
|
-
sandbox: sandboxInstance,
|
|
9966
|
+
sandbox: createBashToolSandboxAdapter(sandboxInstance),
|
|
9645
9967
|
destination: SANDBOX_WORKSPACE_ROOT
|
|
9646
9968
|
})
|
|
9647
9969
|
);
|
|
@@ -9652,47 +9974,70 @@ function createSandboxSessionManager(options) {
|
|
|
9652
9974
|
}
|
|
9653
9975
|
return {
|
|
9654
9976
|
bash: async (input) => {
|
|
9655
|
-
const
|
|
9656
|
-
|
|
9657
|
-
pathPrefix: `${SANDBOX_RUNTIME_BIN_DIR}:$PATH`
|
|
9658
|
-
});
|
|
9659
|
-
const controller = input.timeoutMs && input.timeoutMs > 0 ? new AbortController() : void 0;
|
|
9977
|
+
const commandEgressId = sandboxInstance.sandboxEgressId;
|
|
9978
|
+
await options?.beforeCommand?.(commandEgressId);
|
|
9660
9979
|
let timedOut = false;
|
|
9661
|
-
|
|
9662
|
-
|
|
9663
|
-
|
|
9664
|
-
|
|
9665
|
-
|
|
9666
|
-
sandboxInstance,
|
|
9667
|
-
input.headerTransforms,
|
|
9668
|
-
async () => {
|
|
9669
|
-
try {
|
|
9670
|
-
const commandResult2 = await sandboxInstance.runCommand({
|
|
9671
|
-
cmd: "bash",
|
|
9672
|
-
args: ["-c", script],
|
|
9673
|
-
cwd: SANDBOX_WORKSPACE_ROOT,
|
|
9674
|
-
...controller ? { signal: controller.signal } : {}
|
|
9675
|
-
});
|
|
9676
|
-
return await readCommandOutput(commandResult2);
|
|
9677
|
-
} catch (error) {
|
|
9678
|
-
if (timedOut) {
|
|
9679
|
-
return {
|
|
9680
|
-
stdout: "",
|
|
9681
|
-
stderr: `Command timed out after ${input.timeoutMs}ms`,
|
|
9682
|
-
exitCode: 124,
|
|
9683
|
-
stdoutTruncated: false,
|
|
9684
|
-
stderrTruncated: false,
|
|
9685
|
-
timedOut: true
|
|
9686
|
-
};
|
|
9687
|
-
}
|
|
9688
|
-
throw error;
|
|
9689
|
-
} finally {
|
|
9690
|
-
if (timeoutId) {
|
|
9691
|
-
clearTimeout(timeoutId);
|
|
9692
|
-
}
|
|
9693
|
-
}
|
|
9980
|
+
let timeoutId;
|
|
9981
|
+
let commandFinished = false;
|
|
9982
|
+
const finishCommand = async () => {
|
|
9983
|
+
if (commandFinished) {
|
|
9984
|
+
return;
|
|
9694
9985
|
}
|
|
9695
|
-
|
|
9986
|
+
commandFinished = true;
|
|
9987
|
+
await options?.afterCommand?.(commandEgressId);
|
|
9988
|
+
};
|
|
9989
|
+
const finishCommandBestEffort = async () => {
|
|
9990
|
+
try {
|
|
9991
|
+
await finishCommand();
|
|
9992
|
+
} catch (error) {
|
|
9993
|
+
logWarn(
|
|
9994
|
+
"sandbox_command_cleanup_failed",
|
|
9995
|
+
traceContext,
|
|
9996
|
+
{
|
|
9997
|
+
"app.sandbox.id": activeSandboxId,
|
|
9998
|
+
"error.type": error instanceof Error ? error.name : "sandbox_command_cleanup_error"
|
|
9999
|
+
},
|
|
10000
|
+
"Sandbox command cleanup failed"
|
|
10001
|
+
);
|
|
10002
|
+
}
|
|
10003
|
+
};
|
|
10004
|
+
try {
|
|
10005
|
+
const sandboxCommandEnv = await resolveCommandEnv();
|
|
10006
|
+
const script = buildNonInteractiveShellScript(input.command, {
|
|
10007
|
+
env: { ...sandboxCommandEnv, ...input.env ?? {} },
|
|
10008
|
+
pathPrefix: `${SANDBOX_RUNTIME_BIN_DIR}:$PATH`
|
|
10009
|
+
});
|
|
10010
|
+
const controller = input.timeoutMs && input.timeoutMs > 0 ? new AbortController() : void 0;
|
|
10011
|
+
timeoutId = controller ? setTimeout(() => {
|
|
10012
|
+
timedOut = true;
|
|
10013
|
+
controller.abort();
|
|
10014
|
+
}, input.timeoutMs) : void 0;
|
|
10015
|
+
const commandResult2 = await sandboxInstance.runCommand({
|
|
10016
|
+
cmd: "bash",
|
|
10017
|
+
args: ["-c", script],
|
|
10018
|
+
cwd: SANDBOX_WORKSPACE_ROOT,
|
|
10019
|
+
...controller ? { signal: controller.signal } : {}
|
|
10020
|
+
});
|
|
10021
|
+
await finishCommandBestEffort();
|
|
10022
|
+
return await readCommandOutput(commandResult2);
|
|
10023
|
+
} catch (error) {
|
|
10024
|
+
if (timedOut) {
|
|
10025
|
+
return {
|
|
10026
|
+
stdout: "",
|
|
10027
|
+
stderr: `Command timed out after ${input.timeoutMs}ms`,
|
|
10028
|
+
exitCode: 124,
|
|
10029
|
+
stdoutTruncated: false,
|
|
10030
|
+
stderrTruncated: false,
|
|
10031
|
+
timedOut: true
|
|
10032
|
+
};
|
|
10033
|
+
}
|
|
10034
|
+
throw error;
|
|
10035
|
+
} finally {
|
|
10036
|
+
if (timeoutId) {
|
|
10037
|
+
clearTimeout(timeoutId);
|
|
10038
|
+
}
|
|
10039
|
+
await finishCommandBestEffort();
|
|
10040
|
+
}
|
|
9696
10041
|
},
|
|
9697
10042
|
readFile: async (input) => await executeReadFile(input, {
|
|
9698
10043
|
toolCallId: "sandbox-read-file",
|
|
@@ -9725,7 +10070,7 @@ function createSandboxSessionManager(options) {
|
|
|
9725
10070
|
availableReferenceFiles = [...files];
|
|
9726
10071
|
},
|
|
9727
10072
|
getSandboxId() {
|
|
9728
|
-
return sandbox
|
|
10073
|
+
return sandbox ? sandbox.sandboxId : sandboxIdHint;
|
|
9729
10074
|
},
|
|
9730
10075
|
getDependencyProfileHash() {
|
|
9731
10076
|
return dependencyProfileHash;
|
|
@@ -9748,7 +10093,7 @@ function createSandboxSessionManager(options) {
|
|
|
9748
10093
|
"app.sandbox.stop.blocking": true
|
|
9749
10094
|
},
|
|
9750
10095
|
async () => {
|
|
9751
|
-
await activeSandbox.stop(
|
|
10096
|
+
await activeSandbox.stop();
|
|
9752
10097
|
}
|
|
9753
10098
|
);
|
|
9754
10099
|
sandbox = null;
|
|
@@ -9767,21 +10112,6 @@ var SANDBOX_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
|
9767
10112
|
"listDir",
|
|
9768
10113
|
"writeFile"
|
|
9769
10114
|
]);
|
|
9770
|
-
function parseHeaderTransforms(raw) {
|
|
9771
|
-
if (!Array.isArray(raw)) {
|
|
9772
|
-
return void 0;
|
|
9773
|
-
}
|
|
9774
|
-
return raw.filter(
|
|
9775
|
-
(value) => Boolean(value && typeof value === "object")
|
|
9776
|
-
).map((transform) => ({
|
|
9777
|
-
domain: String(transform.domain ?? "").trim(),
|
|
9778
|
-
headers: transform.headers && typeof transform.headers === "object" && !Array.isArray(transform.headers) ? Object.fromEntries(
|
|
9779
|
-
Object.entries(transform.headers).filter(([, value]) => typeof value === "string").map(([key, value]) => [key, value])
|
|
9780
|
-
) : {}
|
|
9781
|
-
})).filter(
|
|
9782
|
-
(transform) => transform.domain.length > 0 && Object.keys(transform.headers).length > 0
|
|
9783
|
-
);
|
|
9784
|
-
}
|
|
9785
10115
|
function parseEnv(raw) {
|
|
9786
10116
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
9787
10117
|
return void 0;
|
|
@@ -9790,27 +10120,33 @@ function parseEnv(raw) {
|
|
|
9790
10120
|
Object.entries(raw).filter(([, value]) => typeof value === "string").map(([key, value]) => [key, value])
|
|
9791
10121
|
);
|
|
9792
10122
|
}
|
|
9793
|
-
function createSandboxWorkspace(sandbox) {
|
|
9794
|
-
return {
|
|
9795
|
-
sandboxId: sandbox.sandboxId,
|
|
9796
|
-
readFileToBuffer(input) {
|
|
9797
|
-
return sandbox.readFileToBuffer(input);
|
|
9798
|
-
},
|
|
9799
|
-
runCommand(input) {
|
|
9800
|
-
return sandbox.runCommand(input);
|
|
9801
|
-
}
|
|
9802
|
-
};
|
|
9803
|
-
}
|
|
9804
10123
|
function createSandboxExecutor(options) {
|
|
9805
10124
|
let availableSkills = [];
|
|
9806
10125
|
let referenceFiles = [];
|
|
9807
10126
|
const traceContext = options?.traceContext ?? {};
|
|
10127
|
+
const credentialEgress = options?.credentialEgress;
|
|
10128
|
+
const syncSandboxEgressSession = credentialEgress ? async (egressId) => {
|
|
10129
|
+
await upsertSandboxEgressSession({
|
|
10130
|
+
egressId,
|
|
10131
|
+
requesterId: credentialEgress.requesterId,
|
|
10132
|
+
ttlMs: options?.timeoutMs
|
|
10133
|
+
});
|
|
10134
|
+
} : void 0;
|
|
10135
|
+
const clearSandboxEgressSessionForCommand = credentialEgress ? async (egressId) => {
|
|
10136
|
+
await clearSandboxEgressSession(egressId);
|
|
10137
|
+
} : void 0;
|
|
9808
10138
|
const sessionManager = createSandboxSessionManager({
|
|
9809
10139
|
sandboxId: options?.sandboxId,
|
|
9810
10140
|
sandboxDependencyProfileHash: options?.sandboxDependencyProfileHash,
|
|
9811
10141
|
timeoutMs: options?.timeoutMs,
|
|
9812
10142
|
traceContext,
|
|
9813
|
-
|
|
10143
|
+
commandEnv: credentialEgress ? async () => await resolveSandboxCommandEnvironment() : void 0,
|
|
10144
|
+
createNetworkPolicy: credentialEgress ? buildSandboxEgressNetworkPolicy : void 0,
|
|
10145
|
+
beforeCommand: syncSandboxEgressSession,
|
|
10146
|
+
afterCommand: clearSandboxEgressSessionForCommand,
|
|
10147
|
+
onSandboxAcquired: async (sandbox) => {
|
|
10148
|
+
await options?.onSandboxAcquired?.(sandbox);
|
|
10149
|
+
}
|
|
9814
10150
|
});
|
|
9815
10151
|
const withSandboxSpan = (name, op, attributes, callback) => withSpan(name, op, traceContext, callback, attributes);
|
|
9816
10152
|
const logSandboxBootRequest = (trigger, details = {}) => {
|
|
@@ -9828,7 +10164,6 @@ function createSandboxExecutor(options) {
|
|
|
9828
10164
|
);
|
|
9829
10165
|
};
|
|
9830
10166
|
const executeBashTool = async (rawInput, command) => {
|
|
9831
|
-
const headerTransforms = parseHeaderTransforms(rawInput.headerTransforms);
|
|
9832
10167
|
const env = parseEnv(rawInput.env);
|
|
9833
10168
|
const timeoutMs = positiveInteger(rawInput.timeoutMs);
|
|
9834
10169
|
logSandboxBootRequest("tool.bash", {
|
|
@@ -9845,7 +10180,6 @@ function createSandboxExecutor(options) {
|
|
|
9845
10180
|
try {
|
|
9846
10181
|
const response = await executeBash({
|
|
9847
10182
|
command,
|
|
9848
|
-
...headerTransforms ? { headerTransforms } : {},
|
|
9849
10183
|
...env ? { env } : {},
|
|
9850
10184
|
...timeoutMs ? { timeoutMs } : {}
|
|
9851
10185
|
});
|
|
@@ -10149,7 +10483,7 @@ function createSandboxExecutor(options) {
|
|
|
10149
10483
|
return SANDBOX_TOOL_NAMES.has(toolName);
|
|
10150
10484
|
},
|
|
10151
10485
|
async createSandbox() {
|
|
10152
|
-
return
|
|
10486
|
+
return await sessionManager.createSandbox();
|
|
10153
10487
|
},
|
|
10154
10488
|
execute,
|
|
10155
10489
|
async dispose() {
|
|
@@ -10250,34 +10584,6 @@ function buildSandboxInput(toolName, params) {
|
|
|
10250
10584
|
return params;
|
|
10251
10585
|
}
|
|
10252
10586
|
|
|
10253
|
-
// src/chat/tools/execution/inject-credentials.ts
|
|
10254
|
-
function resolveCredentialInjection(toolName, command, capabilityRuntime, sandbox) {
|
|
10255
|
-
if (toolName !== "bash" || !capabilityRuntime) {
|
|
10256
|
-
return {};
|
|
10257
|
-
}
|
|
10258
|
-
const headerTransforms = capabilityRuntime.getTurnHeaderTransforms();
|
|
10259
|
-
const env = capabilityRuntime.getTurnEnv();
|
|
10260
|
-
const isCustomCommand = /^jr-rpc(?:\s|$)/.test(command.trim());
|
|
10261
|
-
const shouldLog = !isCustomCommand && Boolean(headerTransforms && headerTransforms.length > 0);
|
|
10262
|
-
if (shouldLog) {
|
|
10263
|
-
const headerDomains = (headerTransforms ?? []).map(
|
|
10264
|
-
(transform) => transform.domain
|
|
10265
|
-
);
|
|
10266
|
-
const skillName = sandbox.getActiveSkill()?.name;
|
|
10267
|
-
logInfo(
|
|
10268
|
-
"credential_inject_start",
|
|
10269
|
-
{},
|
|
10270
|
-
{
|
|
10271
|
-
"app.skill.name": skillName,
|
|
10272
|
-
"app.credential.delivery": "header_transform",
|
|
10273
|
-
"app.credential.header_domains": headerDomains
|
|
10274
|
-
},
|
|
10275
|
-
`Injecting scoped credential headers for sandbox command (${skillName ?? "unknown skill"} \u2192 ${headerDomains.join(", ")})`
|
|
10276
|
-
);
|
|
10277
|
-
}
|
|
10278
|
-
return { headerTransforms, env };
|
|
10279
|
-
}
|
|
10280
|
-
|
|
10281
10587
|
// src/chat/tools/execution/normalize-result.ts
|
|
10282
10588
|
function isStructuredToolExecutionResult(value) {
|
|
10283
10589
|
const content = value?.content;
|
|
@@ -10367,7 +10673,7 @@ function handleToolExecutionError(error, toolName, toolCallId, shouldTrace, trac
|
|
|
10367
10673
|
}
|
|
10368
10674
|
|
|
10369
10675
|
// src/chat/tools/agent-tools.ts
|
|
10370
|
-
function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor,
|
|
10676
|
+
function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor, pluginAuthOrchestration, onToolCall) {
|
|
10371
10677
|
const shouldTrace = shouldEmitDevAgentTrace();
|
|
10372
10678
|
return Object.entries(tools).map(([toolName, toolDef]) => ({
|
|
10373
10679
|
name: toolName,
|
|
@@ -10407,21 +10713,11 @@ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor
|
|
|
10407
10713
|
};
|
|
10408
10714
|
}
|
|
10409
10715
|
const bashCommand = toolName === "bash" && typeof parsed.command === "string" ? parsed.command.trim() : "";
|
|
10410
|
-
const injection = resolveCredentialInjection(
|
|
10411
|
-
toolName,
|
|
10412
|
-
bashCommand,
|
|
10413
|
-
capabilityRuntime,
|
|
10414
|
-
sandbox
|
|
10415
|
-
);
|
|
10416
10716
|
const sandboxInput = buildSandboxInput(toolName, parsed);
|
|
10417
10717
|
const isSandbox = Boolean(sandboxExecutor?.canExecute(toolName));
|
|
10418
10718
|
const result = isSandbox ? await sandboxExecutor.execute({
|
|
10419
10719
|
toolName,
|
|
10420
|
-
input:
|
|
10421
|
-
...sandboxInput,
|
|
10422
|
-
...injection.headerTransforms ? { headerTransforms: injection.headerTransforms } : {},
|
|
10423
|
-
...injection.env ? { env: injection.env } : {}
|
|
10424
|
-
} : sandboxInput
|
|
10720
|
+
input: sandboxInput
|
|
10425
10721
|
}) : await toolDef.execute(parsed, {
|
|
10426
10722
|
experimental_context: sandbox
|
|
10427
10723
|
});
|
|
@@ -11284,6 +11580,7 @@ ${typeof result.stderr === "string" ? result.stderr : ""}`.toLowerCase();
|
|
|
11284
11580
|
return false;
|
|
11285
11581
|
}
|
|
11286
11582
|
return [
|
|
11583
|
+
/\bjunior-auth-required\b/,
|
|
11287
11584
|
/\b401\b/,
|
|
11288
11585
|
/\bunauthorized\b/,
|
|
11289
11586
|
/\bbad credentials\b/,
|
|
@@ -11296,6 +11593,33 @@ ${typeof result.stderr === "string" ? result.stderr : ""}`.toLowerCase();
|
|
|
11296
11593
|
/\breauthoriz/
|
|
11297
11594
|
].some((pattern) => pattern.test(text));
|
|
11298
11595
|
}
|
|
11596
|
+
function commandText(details) {
|
|
11597
|
+
if (!details || typeof details !== "object") {
|
|
11598
|
+
return "";
|
|
11599
|
+
}
|
|
11600
|
+
const result = details;
|
|
11601
|
+
return `${typeof result.stdout === "string" ? result.stdout : ""}
|
|
11602
|
+
${typeof result.stderr === "string" ? result.stderr : ""}`;
|
|
11603
|
+
}
|
|
11604
|
+
function explicitAuthRequiredProvider(details) {
|
|
11605
|
+
const match = /\bjunior-auth-required\s+provider=([a-z0-9-]+)\b/.exec(
|
|
11606
|
+
commandText(details).toLowerCase()
|
|
11607
|
+
);
|
|
11608
|
+
return match?.[1];
|
|
11609
|
+
}
|
|
11610
|
+
function registeredProviderNames() {
|
|
11611
|
+
const providers = /* @__PURE__ */ new Set();
|
|
11612
|
+
for (const plugin of getPluginProviders()) {
|
|
11613
|
+
const domains = [
|
|
11614
|
+
...plugin.manifest.credentials?.domains ?? [],
|
|
11615
|
+
...plugin.manifest.domains ?? []
|
|
11616
|
+
];
|
|
11617
|
+
if (domains.length > 0) {
|
|
11618
|
+
providers.add(plugin.manifest.name);
|
|
11619
|
+
}
|
|
11620
|
+
}
|
|
11621
|
+
return [...providers].sort((left, right) => left.localeCompare(right));
|
|
11622
|
+
}
|
|
11299
11623
|
function commandTargetsProvider(provider, command, details) {
|
|
11300
11624
|
const normalizedCommand = command.trim().toLowerCase();
|
|
11301
11625
|
if (!normalizedCommand) {
|
|
@@ -11310,16 +11634,15 @@ function commandTargetsProvider(provider, command, details) {
|
|
|
11310
11634
|
const credentials = manifest?.credentials;
|
|
11311
11635
|
if (credentials) {
|
|
11312
11636
|
candidates.add(credentials.authTokenEnv.toLowerCase());
|
|
11313
|
-
for (const domain of credentials.
|
|
11637
|
+
for (const domain of credentials.domains) {
|
|
11314
11638
|
candidates.add(domain.toLowerCase());
|
|
11315
11639
|
}
|
|
11316
11640
|
}
|
|
11317
|
-
for (const domain of manifest?.
|
|
11641
|
+
for (const domain of manifest?.domains ?? []) {
|
|
11318
11642
|
candidates.add(domain.toLowerCase());
|
|
11319
11643
|
}
|
|
11320
11644
|
const combinedText = `${normalizedCommand}
|
|
11321
|
-
${details.
|
|
11322
|
-
${details.stderr?.toLowerCase() ?? ""}`;
|
|
11645
|
+
${commandText(details).toLowerCase()}`;
|
|
11323
11646
|
return [...candidates].some((candidate) => combinedText.includes(candidate));
|
|
11324
11647
|
}
|
|
11325
11648
|
function createPluginAuthOrchestration(deps, abortAgent) {
|
|
@@ -11377,23 +11700,22 @@ function createPluginAuthOrchestration(deps, abortAgent) {
|
|
|
11377
11700
|
abortAgent();
|
|
11378
11701
|
throw pendingPause;
|
|
11379
11702
|
};
|
|
11380
|
-
const handleCredentialUnavailable = async (input) => {
|
|
11381
|
-
if (pendingPause) {
|
|
11382
|
-
throw pendingPause;
|
|
11383
|
-
}
|
|
11384
|
-
if (!deps.requesterId || !getPluginOAuthConfig(input.error.provider)) {
|
|
11385
|
-
throw input.error;
|
|
11386
|
-
}
|
|
11387
|
-
return await startAuthorizationPause(
|
|
11388
|
-
input.error.provider,
|
|
11389
|
-
input.activeSkill
|
|
11390
|
-
);
|
|
11391
|
-
};
|
|
11392
11703
|
return {
|
|
11393
|
-
handleCredentialUnavailable,
|
|
11394
11704
|
handleCommandFailure: async (input) => {
|
|
11395
|
-
const
|
|
11396
|
-
|
|
11705
|
+
const providers = registeredProviderNames();
|
|
11706
|
+
const authFailure = isCommandAuthFailure(input.details);
|
|
11707
|
+
if (!authFailure) {
|
|
11708
|
+
return;
|
|
11709
|
+
}
|
|
11710
|
+
const explicitProvider = explicitAuthRequiredProvider(input.details);
|
|
11711
|
+
const provider = explicitProvider && providers.includes(explicitProvider) ? explicitProvider : providers.find(
|
|
11712
|
+
(availableProvider) => commandTargetsProvider(
|
|
11713
|
+
availableProvider,
|
|
11714
|
+
input.command,
|
|
11715
|
+
input.details
|
|
11716
|
+
)
|
|
11717
|
+
);
|
|
11718
|
+
if (!provider || !deps.requesterId || !deps.userTokenStore || !getPluginOAuthConfig(provider)) {
|
|
11397
11719
|
return;
|
|
11398
11720
|
}
|
|
11399
11721
|
await startAuthorizationPause(provider, input.activeSkill, {
|
|
@@ -11659,14 +11981,15 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
11659
11981
|
...context.configuration ?? {},
|
|
11660
11982
|
...persistedConfigurationValues
|
|
11661
11983
|
};
|
|
11662
|
-
const
|
|
11663
|
-
requesterId: context.requester?.userId
|
|
11664
|
-
});
|
|
11984
|
+
const requesterId = context.requester?.userId;
|
|
11665
11985
|
const userTokenStore = createUserTokenStore();
|
|
11666
11986
|
sandboxExecutor = createSandboxExecutor({
|
|
11667
11987
|
sandboxId: context.sandbox?.sandboxId,
|
|
11668
11988
|
sandboxDependencyProfileHash: context.sandbox?.sandboxDependencyProfileHash,
|
|
11669
11989
|
traceContext: spanContext,
|
|
11990
|
+
credentialEgress: requesterId ? {
|
|
11991
|
+
requesterId
|
|
11992
|
+
} : void 0,
|
|
11670
11993
|
onSandboxAcquired: async (sandbox2) => {
|
|
11671
11994
|
lastKnownSandboxId = sandbox2.sandboxId;
|
|
11672
11995
|
lastKnownSandboxDependencyProfileHash = sandbox2.sandboxDependencyProfileHash;
|
|
@@ -11830,25 +12153,6 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
11830
12153
|
const syncResumeState = () => {
|
|
11831
12154
|
loadedSkillNamesForResume = activeSkills.map((skill) => skill.name);
|
|
11832
12155
|
};
|
|
11833
|
-
const enableSkillCredentials = async (skill, reason) => {
|
|
11834
|
-
if (!skill?.pluginProvider) {
|
|
11835
|
-
return;
|
|
11836
|
-
}
|
|
11837
|
-
try {
|
|
11838
|
-
await capabilityRuntime.enableCredentialsForTurn({
|
|
11839
|
-
activeSkill: skill,
|
|
11840
|
-
reason
|
|
11841
|
-
});
|
|
11842
|
-
} catch (error) {
|
|
11843
|
-
if (error instanceof CredentialUnavailableError && context.requester?.userId) {
|
|
11844
|
-
await pluginAuth.handleCredentialUnavailable({
|
|
11845
|
-
activeSkill: skill,
|
|
11846
|
-
error
|
|
11847
|
-
});
|
|
11848
|
-
}
|
|
11849
|
-
throw error;
|
|
11850
|
-
}
|
|
11851
|
-
};
|
|
11852
12156
|
setTags({
|
|
11853
12157
|
conversationId: spanContext.conversationId,
|
|
11854
12158
|
slackThreadId: context.correlation?.threadId,
|
|
@@ -11890,10 +12194,6 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
11890
12194
|
if (mcpAuth.getPendingPause()) {
|
|
11891
12195
|
return void 0;
|
|
11892
12196
|
}
|
|
11893
|
-
await enableSkillCredentials(
|
|
11894
|
-
effective,
|
|
11895
|
-
`skill:${effective.name}:turn:load`
|
|
11896
|
-
);
|
|
11897
12197
|
if (!effective.pluginProvider) {
|
|
11898
12198
|
return void 0;
|
|
11899
12199
|
}
|
|
@@ -11946,7 +12246,6 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
11946
12246
|
timeoutResumeMessages = existingCheckpoint?.piMessages ?? [];
|
|
11947
12247
|
throw mcpAuth.getPendingPause();
|
|
11948
12248
|
}
|
|
11949
|
-
await enableSkillCredentials(skill, `skill:${skill.name}:turn:resume`);
|
|
11950
12249
|
}
|
|
11951
12250
|
syncResumeState();
|
|
11952
12251
|
const activeMcpCatalogs = toActiveMcpCatalogSummaries(
|
|
@@ -12007,7 +12306,6 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
12007
12306
|
spanContext,
|
|
12008
12307
|
context.onStatus,
|
|
12009
12308
|
sandboxExecutor,
|
|
12010
|
-
capabilityRuntime,
|
|
12011
12309
|
pluginAuth,
|
|
12012
12310
|
onToolCall
|
|
12013
12311
|
);
|
|
@@ -12017,7 +12315,6 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
12017
12315
|
spanContext,
|
|
12018
12316
|
context.onStatus,
|
|
12019
12317
|
sandboxExecutor,
|
|
12020
|
-
capabilityRuntime,
|
|
12021
12318
|
pluginAuth,
|
|
12022
12319
|
onToolCall
|
|
12023
12320
|
);
|
|
@@ -14368,6 +14665,328 @@ async function GET5(request, provider, waitUntil) {
|
|
|
14368
14665
|
});
|
|
14369
14666
|
}
|
|
14370
14667
|
|
|
14668
|
+
// src/chat/sandbox/egress-oidc.ts
|
|
14669
|
+
import {
|
|
14670
|
+
createRemoteJWKSet,
|
|
14671
|
+
decodeJwt,
|
|
14672
|
+
jwtVerify
|
|
14673
|
+
} from "jose";
|
|
14674
|
+
var OIDC_DISCOVERY_CACHE_TTL_MS = 60 * 60 * 1e3;
|
|
14675
|
+
var OIDC_DISCOVERY_CACHE_MAX_ENTRIES = 8;
|
|
14676
|
+
var jwksByIssuer = /* @__PURE__ */ new Map();
|
|
14677
|
+
function buildDiscoveryUrl(issuer) {
|
|
14678
|
+
const url = new URL(issuer);
|
|
14679
|
+
if (url.protocol !== "https:" || url.hostname !== "oidc.vercel.com") {
|
|
14680
|
+
throw new Error("Unexpected Vercel OIDC issuer");
|
|
14681
|
+
}
|
|
14682
|
+
url.pathname = `${url.pathname.replace(/\/$/, "")}/.well-known/openid-configuration`;
|
|
14683
|
+
url.search = "";
|
|
14684
|
+
url.hash = "";
|
|
14685
|
+
return url;
|
|
14686
|
+
}
|
|
14687
|
+
function buildJwksUrl(value) {
|
|
14688
|
+
const url = new URL(value);
|
|
14689
|
+
if (url.protocol !== "https:") {
|
|
14690
|
+
throw new Error("Vercel OIDC discovery jwks_uri must use HTTPS");
|
|
14691
|
+
}
|
|
14692
|
+
return url;
|
|
14693
|
+
}
|
|
14694
|
+
async function getJwks(issuer) {
|
|
14695
|
+
const now = Date.now();
|
|
14696
|
+
const cached = jwksByIssuer.get(issuer);
|
|
14697
|
+
if (cached && cached.expiresAtMs > now) {
|
|
14698
|
+
return cached.jwks;
|
|
14699
|
+
}
|
|
14700
|
+
if (cached) {
|
|
14701
|
+
jwksByIssuer.delete(issuer);
|
|
14702
|
+
}
|
|
14703
|
+
const response = await fetch(buildDiscoveryUrl(issuer), {
|
|
14704
|
+
redirect: "error"
|
|
14705
|
+
});
|
|
14706
|
+
if (!response.ok) {
|
|
14707
|
+
throw new Error("Unable to load Vercel OIDC discovery metadata");
|
|
14708
|
+
}
|
|
14709
|
+
const config = await response.json();
|
|
14710
|
+
if (!config.jwks_uri) {
|
|
14711
|
+
throw new Error("Vercel OIDC discovery metadata did not include jwks_uri");
|
|
14712
|
+
}
|
|
14713
|
+
const jwks = createRemoteJWKSet(buildJwksUrl(config.jwks_uri));
|
|
14714
|
+
if (!jwksByIssuer.has(issuer) && jwksByIssuer.size >= OIDC_DISCOVERY_CACHE_MAX_ENTRIES) {
|
|
14715
|
+
const oldestIssuer = jwksByIssuer.keys().next().value;
|
|
14716
|
+
if (oldestIssuer) {
|
|
14717
|
+
jwksByIssuer.delete(oldestIssuer);
|
|
14718
|
+
}
|
|
14719
|
+
}
|
|
14720
|
+
jwksByIssuer.set(issuer, {
|
|
14721
|
+
jwks,
|
|
14722
|
+
expiresAtMs: now + OIDC_DISCOVERY_CACHE_TTL_MS
|
|
14723
|
+
});
|
|
14724
|
+
return jwks;
|
|
14725
|
+
}
|
|
14726
|
+
function validateSandboxClaim(payload, egressId) {
|
|
14727
|
+
if (payload.sandbox_id !== egressId) {
|
|
14728
|
+
throw new Error("Vercel OIDC token belongs to a different sandbox");
|
|
14729
|
+
}
|
|
14730
|
+
}
|
|
14731
|
+
async function verifyVercelSandboxOidcToken(token, egressId) {
|
|
14732
|
+
const unverified = decodeJwt(token);
|
|
14733
|
+
if (typeof unverified.iss !== "string") {
|
|
14734
|
+
throw new Error("Vercel OIDC token did not include an issuer");
|
|
14735
|
+
}
|
|
14736
|
+
const jwks = await getJwks(unverified.iss);
|
|
14737
|
+
const verified = await jwtVerify(token, jwks, {
|
|
14738
|
+
issuer: unverified.iss
|
|
14739
|
+
});
|
|
14740
|
+
validateSandboxClaim(verified.payload, egressId);
|
|
14741
|
+
return verified.payload;
|
|
14742
|
+
}
|
|
14743
|
+
|
|
14744
|
+
// src/chat/sandbox/egress-proxy.ts
|
|
14745
|
+
var OIDC_TOKEN_HEADER = "vercel-sandbox-oidc-token";
|
|
14746
|
+
var FORWARDED_HOST_HEADER = "vercel-forwarded-host";
|
|
14747
|
+
var FORWARDED_SCHEME_HEADER = "vercel-forwarded-scheme";
|
|
14748
|
+
var FORWARDED_PORT_HEADER = "vercel-forwarded-port";
|
|
14749
|
+
var ROUTE_PREFIX = "/api/internal/sandbox-egress";
|
|
14750
|
+
var HOP_BY_HOP_HEADERS = /* @__PURE__ */ new Set([
|
|
14751
|
+
"connection",
|
|
14752
|
+
"host",
|
|
14753
|
+
"keep-alive",
|
|
14754
|
+
"proxy-authenticate",
|
|
14755
|
+
"proxy-authorization",
|
|
14756
|
+
"te",
|
|
14757
|
+
"trailer",
|
|
14758
|
+
"transfer-encoding",
|
|
14759
|
+
"upgrade"
|
|
14760
|
+
]);
|
|
14761
|
+
var PROXY_ONLY_HEADERS = /* @__PURE__ */ new Set([
|
|
14762
|
+
OIDC_TOKEN_HEADER,
|
|
14763
|
+
FORWARDED_HOST_HEADER,
|
|
14764
|
+
FORWARDED_SCHEME_HEADER,
|
|
14765
|
+
FORWARDED_PORT_HEADER
|
|
14766
|
+
]);
|
|
14767
|
+
var AUTH_REJECTION_STATUS = /* @__PURE__ */ new Set([401, 403]);
|
|
14768
|
+
function jsonError(message, status) {
|
|
14769
|
+
return Response.json({ error: message }, { status });
|
|
14770
|
+
}
|
|
14771
|
+
function normalizeHost(value) {
|
|
14772
|
+
const trimmed = value.trim().toLowerCase();
|
|
14773
|
+
if (!trimmed || trimmed.includes("/") || trimmed.includes("\\") || trimmed.includes(":")) {
|
|
14774
|
+
return void 0;
|
|
14775
|
+
}
|
|
14776
|
+
return trimmed.replace(/\.$/, "");
|
|
14777
|
+
}
|
|
14778
|
+
function normalizeScheme(value) {
|
|
14779
|
+
return value.trim().toLowerCase() === "https" ? "https" : void 0;
|
|
14780
|
+
}
|
|
14781
|
+
function normalizePort(value) {
|
|
14782
|
+
if (!value) {
|
|
14783
|
+
return void 0;
|
|
14784
|
+
}
|
|
14785
|
+
const trimmed = value.trim();
|
|
14786
|
+
if (!/^\d{1,5}$/.test(trimmed)) {
|
|
14787
|
+
return void 0;
|
|
14788
|
+
}
|
|
14789
|
+
const port = Number.parseInt(trimmed, 10);
|
|
14790
|
+
return port >= 1 && port <= 65535 ? trimmed : void 0;
|
|
14791
|
+
}
|
|
14792
|
+
function upstreamPath(request, egressId) {
|
|
14793
|
+
const url = new URL(request.url);
|
|
14794
|
+
const prefix = `${ROUTE_PREFIX}/${encodeURIComponent(egressId)}`;
|
|
14795
|
+
if (url.pathname === prefix) {
|
|
14796
|
+
return `/${url.search}`;
|
|
14797
|
+
}
|
|
14798
|
+
if (url.pathname.startsWith(`${prefix}/`)) {
|
|
14799
|
+
return `${url.pathname.slice(prefix.length)}${url.search}`;
|
|
14800
|
+
}
|
|
14801
|
+
return void 0;
|
|
14802
|
+
}
|
|
14803
|
+
function buildUpstreamUrl(request, egressId) {
|
|
14804
|
+
const forwardedHost = request.headers.get(FORWARDED_HOST_HEADER);
|
|
14805
|
+
if (!forwardedHost?.trim()) {
|
|
14806
|
+
return { ok: false, error: "Missing forwarded host" };
|
|
14807
|
+
}
|
|
14808
|
+
const host = normalizeHost(forwardedHost);
|
|
14809
|
+
if (!host) {
|
|
14810
|
+
return { ok: false, error: "Invalid forwarded host" };
|
|
14811
|
+
}
|
|
14812
|
+
const forwardedScheme = request.headers.get(FORWARDED_SCHEME_HEADER);
|
|
14813
|
+
if (!forwardedScheme?.trim()) {
|
|
14814
|
+
return { ok: false, error: "Missing forwarded scheme" };
|
|
14815
|
+
}
|
|
14816
|
+
const scheme = normalizeScheme(forwardedScheme);
|
|
14817
|
+
if (!scheme) {
|
|
14818
|
+
return { ok: false, error: "Forwarded scheme must be https" };
|
|
14819
|
+
}
|
|
14820
|
+
const forwardedPort = request.headers.get(FORWARDED_PORT_HEADER);
|
|
14821
|
+
const port = normalizePort(forwardedPort);
|
|
14822
|
+
if (forwardedPort && !port) {
|
|
14823
|
+
return { ok: false, error: "Invalid forwarded port" };
|
|
14824
|
+
}
|
|
14825
|
+
const path11 = upstreamPath(request, egressId);
|
|
14826
|
+
if (!path11) {
|
|
14827
|
+
return { ok: false, error: "Invalid egress route" };
|
|
14828
|
+
}
|
|
14829
|
+
try {
|
|
14830
|
+
const url = new URL(`${scheme}://${host}${port ? `:${port}` : ""}${path11}`);
|
|
14831
|
+
return { ok: true, url };
|
|
14832
|
+
} catch {
|
|
14833
|
+
return { ok: false, error: "Invalid forwarded URL" };
|
|
14834
|
+
}
|
|
14835
|
+
}
|
|
14836
|
+
async function requestBodyBytes(request) {
|
|
14837
|
+
if (request.method === "GET" || request.method === "HEAD" || request.body === null) {
|
|
14838
|
+
return void 0;
|
|
14839
|
+
}
|
|
14840
|
+
return await request.arrayBuffer();
|
|
14841
|
+
}
|
|
14842
|
+
function requestHeaders(request, lease, upstreamHost) {
|
|
14843
|
+
const headers = new Headers();
|
|
14844
|
+
request.headers.forEach((value, key) => {
|
|
14845
|
+
const normalized = key.toLowerCase();
|
|
14846
|
+
if (HOP_BY_HOP_HEADERS.has(normalized) || PROXY_ONLY_HEADERS.has(normalized)) {
|
|
14847
|
+
return;
|
|
14848
|
+
}
|
|
14849
|
+
headers.append(key, value);
|
|
14850
|
+
});
|
|
14851
|
+
for (const transform of lease.headerTransforms) {
|
|
14852
|
+
if (!matchesSandboxEgressDomain(upstreamHost, transform.domain)) {
|
|
14853
|
+
continue;
|
|
14854
|
+
}
|
|
14855
|
+
for (const [key, value] of Object.entries(transform.headers)) {
|
|
14856
|
+
headers.set(key, value);
|
|
14857
|
+
}
|
|
14858
|
+
}
|
|
14859
|
+
return headers;
|
|
14860
|
+
}
|
|
14861
|
+
function responseHeaders(upstream) {
|
|
14862
|
+
const headers = new Headers();
|
|
14863
|
+
upstream.headers.forEach((value, key) => {
|
|
14864
|
+
const normalized = key.toLowerCase();
|
|
14865
|
+
if (!HOP_BY_HOP_HEADERS.has(normalized)) {
|
|
14866
|
+
headers.append(key, value);
|
|
14867
|
+
}
|
|
14868
|
+
});
|
|
14869
|
+
return headers;
|
|
14870
|
+
}
|
|
14871
|
+
async function credentialLease(egressId, provider, session) {
|
|
14872
|
+
const cached = await getSandboxEgressCredentialLease(
|
|
14873
|
+
egressId,
|
|
14874
|
+
provider,
|
|
14875
|
+
session
|
|
14876
|
+
);
|
|
14877
|
+
if (cached) {
|
|
14878
|
+
return cached;
|
|
14879
|
+
}
|
|
14880
|
+
const lease = await issueProviderCredentialLease({
|
|
14881
|
+
provider,
|
|
14882
|
+
requesterId: session.requesterId,
|
|
14883
|
+
reason: `sandbox-egress:${provider}`
|
|
14884
|
+
});
|
|
14885
|
+
const headerTransforms = lease.headerTransforms ?? [];
|
|
14886
|
+
if (headerTransforms.length === 0) {
|
|
14887
|
+
throw new Error(
|
|
14888
|
+
`Credential lease for ${provider} did not include header transforms`
|
|
14889
|
+
);
|
|
14890
|
+
}
|
|
14891
|
+
const cachedLease = {
|
|
14892
|
+
provider,
|
|
14893
|
+
expiresAt: lease.expiresAt,
|
|
14894
|
+
headerTransforms
|
|
14895
|
+
};
|
|
14896
|
+
await setSandboxEgressCredentialLease(egressId, session, cachedLease);
|
|
14897
|
+
return cachedLease;
|
|
14898
|
+
}
|
|
14899
|
+
function hasTransformForHost(lease, host) {
|
|
14900
|
+
return lease.headerTransforms.some(
|
|
14901
|
+
(transform) => matchesSandboxEgressDomain(host, transform.domain)
|
|
14902
|
+
);
|
|
14903
|
+
}
|
|
14904
|
+
async function proxySandboxEgressRequest(request, egressId, deps = {}) {
|
|
14905
|
+
const oidcToken = request.headers.get(OIDC_TOKEN_HEADER)?.trim();
|
|
14906
|
+
if (!oidcToken) {
|
|
14907
|
+
return jsonError("Missing Vercel Sandbox OIDC token", 401);
|
|
14908
|
+
}
|
|
14909
|
+
try {
|
|
14910
|
+
await (deps.verifyOidc ?? verifyVercelSandboxOidcToken)(
|
|
14911
|
+
oidcToken,
|
|
14912
|
+
egressId
|
|
14913
|
+
);
|
|
14914
|
+
} catch (error) {
|
|
14915
|
+
logWarn(
|
|
14916
|
+
"sandbox_egress_oidc_verification_failed",
|
|
14917
|
+
{},
|
|
14918
|
+
{
|
|
14919
|
+
"app.sandbox.oidc_error": error instanceof Error ? error.message : String(error)
|
|
14920
|
+
},
|
|
14921
|
+
"Sandbox egress OIDC verification failed"
|
|
14922
|
+
);
|
|
14923
|
+
return jsonError("Invalid Vercel Sandbox OIDC token", 401);
|
|
14924
|
+
}
|
|
14925
|
+
const upstreamResult = buildUpstreamUrl(request, egressId);
|
|
14926
|
+
if (!upstreamResult.ok) {
|
|
14927
|
+
return jsonError(upstreamResult.error, 400);
|
|
14928
|
+
}
|
|
14929
|
+
const upstreamUrl = upstreamResult.url;
|
|
14930
|
+
const provider = resolveSandboxEgressProviderForHost(upstreamUrl.hostname);
|
|
14931
|
+
if (!provider) {
|
|
14932
|
+
return jsonError("No provider owns forwarded host", 403);
|
|
14933
|
+
}
|
|
14934
|
+
const session = await getSandboxEgressSession(egressId);
|
|
14935
|
+
if (!session) {
|
|
14936
|
+
return jsonError("Sandbox egress session is not authorized", 403);
|
|
14937
|
+
}
|
|
14938
|
+
let lease;
|
|
14939
|
+
try {
|
|
14940
|
+
lease = await credentialLease(egressId, provider, session);
|
|
14941
|
+
} catch (error) {
|
|
14942
|
+
if (error instanceof CredentialUnavailableError) {
|
|
14943
|
+
return new Response(
|
|
14944
|
+
`junior-auth-required provider=${error.provider} 401 unauthorized
|
|
14945
|
+
${error.message}`,
|
|
14946
|
+
{
|
|
14947
|
+
status: 401,
|
|
14948
|
+
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
14949
|
+
}
|
|
14950
|
+
);
|
|
14951
|
+
}
|
|
14952
|
+
throw error;
|
|
14953
|
+
}
|
|
14954
|
+
if (!hasTransformForHost(lease, upstreamUrl.hostname)) {
|
|
14955
|
+
return jsonError("Credential lease does not cover forwarded host", 403);
|
|
14956
|
+
}
|
|
14957
|
+
const body = await requestBodyBytes(request);
|
|
14958
|
+
const upstream = await (deps.fetch ?? fetch)(upstreamUrl, {
|
|
14959
|
+
method: request.method,
|
|
14960
|
+
headers: requestHeaders(request, lease, upstreamUrl.hostname),
|
|
14961
|
+
...body ? { body } : {},
|
|
14962
|
+
redirect: "manual"
|
|
14963
|
+
});
|
|
14964
|
+
if (AUTH_REJECTION_STATUS.has(upstream.status)) {
|
|
14965
|
+
logWarn(
|
|
14966
|
+
"sandbox_egress_upstream_auth_rejected",
|
|
14967
|
+
{},
|
|
14968
|
+
{
|
|
14969
|
+
"app.credential.provider": provider,
|
|
14970
|
+
"http.request.method": request.method,
|
|
14971
|
+
"http.response.status_code": upstream.status,
|
|
14972
|
+
"server.address": upstreamUrl.hostname
|
|
14973
|
+
},
|
|
14974
|
+
"Sandbox egress upstream auth rejected"
|
|
14975
|
+
);
|
|
14976
|
+
await clearSandboxEgressCredentialLease(egressId, provider, session);
|
|
14977
|
+
}
|
|
14978
|
+
return new Response(upstream.body, {
|
|
14979
|
+
status: upstream.status,
|
|
14980
|
+
statusText: upstream.statusText,
|
|
14981
|
+
headers: responseHeaders(upstream)
|
|
14982
|
+
});
|
|
14983
|
+
}
|
|
14984
|
+
|
|
14985
|
+
// src/handlers/sandbox-egress-proxy.ts
|
|
14986
|
+
async function ALL(request, egressId) {
|
|
14987
|
+
return await proxySandboxEgressRequest(request, egressId);
|
|
14988
|
+
}
|
|
14989
|
+
|
|
14371
14990
|
// src/chat/slack/context.ts
|
|
14372
14991
|
function toTrimmedSlackString(value) {
|
|
14373
14992
|
const normalized = toOptionalString(value);
|
|
@@ -17744,6 +18363,12 @@ async function createApp(options) {
|
|
|
17744
18363
|
app.post("/api/internal/turn-resume", (c) => {
|
|
17745
18364
|
return POST(c.req.raw, waitUntil);
|
|
17746
18365
|
});
|
|
18366
|
+
app.all("/api/internal/sandbox-egress/:egressId", (c) => {
|
|
18367
|
+
return ALL(c.req.raw, c.req.param("egressId"));
|
|
18368
|
+
});
|
|
18369
|
+
app.all("/api/internal/sandbox-egress/:egressId/*", (c) => {
|
|
18370
|
+
return ALL(c.req.raw, c.req.param("egressId"));
|
|
18371
|
+
});
|
|
17747
18372
|
app.post("/api/webhooks/:platform", (c) => {
|
|
17748
18373
|
return POST2(c.req.raw, c.req.param("platform"), waitUntil);
|
|
17749
18374
|
});
|