@sentry/junior 0.42.0 → 0.43.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 +1079 -444
- package/dist/{chunk-SCE5C645.js → chunk-BCG3I2T2.js} +197 -101
- package/dist/{chunk-Y3UO7NR6.js → chunk-YSXHRIWR.js} +47 -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-YSXHRIWR.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,173 @@ 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(sandboxId) {
|
|
8626
|
+
const baseUrl = resolveBaseUrl();
|
|
8627
|
+
if (!baseUrl) {
|
|
8628
|
+
return void 0;
|
|
8629
|
+
}
|
|
8630
|
+
const url = new URL(
|
|
8631
|
+
`${SANDBOX_EGRESS_PROXY_PATH}/${encodeURIComponent(sandboxId)}`,
|
|
8632
|
+
baseUrl
|
|
8633
|
+
);
|
|
8634
|
+
return url.toString();
|
|
8635
|
+
}
|
|
8636
|
+
function buildSandboxEgressNetworkPolicy(sandboxId) {
|
|
8637
|
+
const forwardURL = proxyUrl(sandboxId);
|
|
8638
|
+
if (!forwardURL) {
|
|
8639
|
+
return void 0;
|
|
8640
|
+
}
|
|
8641
|
+
const entries = providerEntries();
|
|
8642
|
+
if (entries.length === 0) {
|
|
8643
|
+
return void 0;
|
|
8644
|
+
}
|
|
8645
|
+
const allow = {
|
|
8646
|
+
"*": []
|
|
8647
|
+
};
|
|
8648
|
+
for (const entry of entries) {
|
|
8649
|
+
for (const domain of entry.domains) {
|
|
8650
|
+
allow[domain] = [{ forwardURL }];
|
|
8651
|
+
}
|
|
8652
|
+
}
|
|
8653
|
+
return { allow };
|
|
8654
|
+
}
|
|
8655
|
+
async function resolveSandboxCommandEnvironment() {
|
|
8656
|
+
const env = {};
|
|
8657
|
+
for (const plugin of getPluginProviders().sort(
|
|
8658
|
+
(left, right) => left.manifest.name.localeCompare(right.manifest.name)
|
|
8659
|
+
)) {
|
|
8660
|
+
Object.assign(env, resolvePluginCommandEnv(plugin.manifest));
|
|
8661
|
+
const credentials = plugin.manifest.credentials;
|
|
8662
|
+
if (credentials) {
|
|
8663
|
+
env[credentials.authTokenEnv] = resolveAuthTokenPlaceholder(credentials);
|
|
8664
|
+
}
|
|
8665
|
+
}
|
|
8666
|
+
return env;
|
|
8667
|
+
}
|
|
8668
|
+
|
|
8669
|
+
// src/chat/sandbox/egress-session.ts
|
|
8670
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
8671
|
+
var SANDBOX_EGRESS_SESSION_PREFIX = "sandbox-egress-session";
|
|
8672
|
+
var SANDBOX_EGRESS_LEASE_PREFIX = "sandbox-egress-lease";
|
|
8673
|
+
var DEFAULT_SESSION_TTL_MS = 30 * 60 * 1e3;
|
|
8674
|
+
function sessionKey2(sandboxId) {
|
|
8675
|
+
return `${SANDBOX_EGRESS_SESSION_PREFIX}:${sandboxId}`;
|
|
8676
|
+
}
|
|
8677
|
+
function leaseKey(sandboxId, provider, session) {
|
|
8678
|
+
return `${SANDBOX_EGRESS_LEASE_PREFIX}:${sandboxId}:${provider}:${session.requesterId}:${session.activationId}`;
|
|
8679
|
+
}
|
|
8680
|
+
function parseSession(value) {
|
|
8681
|
+
if (!value || typeof value !== "object") {
|
|
8682
|
+
return void 0;
|
|
8683
|
+
}
|
|
8684
|
+
const record = value;
|
|
8685
|
+
if (typeof record.requesterId !== "string" || typeof record.expiresAtMs !== "number" || !Number.isFinite(record.expiresAtMs) || typeof record.activationId !== "string" || !record.activationId) {
|
|
8686
|
+
return void 0;
|
|
8687
|
+
}
|
|
8688
|
+
if (record.expiresAtMs <= Date.now()) {
|
|
8689
|
+
return void 0;
|
|
8690
|
+
}
|
|
8691
|
+
return {
|
|
8692
|
+
requesterId: record.requesterId,
|
|
8693
|
+
expiresAtMs: record.expiresAtMs,
|
|
8694
|
+
activationId: record.activationId
|
|
8695
|
+
};
|
|
8696
|
+
}
|
|
8697
|
+
function parseLease(value) {
|
|
8698
|
+
if (!value || typeof value !== "object") {
|
|
8699
|
+
return void 0;
|
|
8700
|
+
}
|
|
8701
|
+
const record = value;
|
|
8702
|
+
if (typeof record.provider !== "string" || typeof record.expiresAt !== "string" || !Array.isArray(record.headerTransforms)) {
|
|
8703
|
+
return void 0;
|
|
8704
|
+
}
|
|
8705
|
+
const expiresAtMs = Date.parse(record.expiresAt);
|
|
8706
|
+
if (!Number.isFinite(expiresAtMs) || expiresAtMs <= Date.now()) {
|
|
8707
|
+
return void 0;
|
|
8708
|
+
}
|
|
8709
|
+
const headerTransforms = record.headerTransforms.filter(
|
|
8710
|
+
(transform) => Boolean(
|
|
8711
|
+
transform && typeof transform.domain === "string" && transform.headers && typeof transform.headers === "object"
|
|
8712
|
+
)
|
|
8713
|
+
);
|
|
8714
|
+
if (headerTransforms.length === 0) {
|
|
8715
|
+
return void 0;
|
|
8716
|
+
}
|
|
8717
|
+
return {
|
|
8718
|
+
provider: record.provider,
|
|
8719
|
+
expiresAt: record.expiresAt,
|
|
8720
|
+
headerTransforms
|
|
8721
|
+
};
|
|
8722
|
+
}
|
|
8723
|
+
async function upsertSandboxEgressSession(input) {
|
|
8724
|
+
const state = getStateAdapter();
|
|
8725
|
+
await state.connect();
|
|
8726
|
+
const ttlMs = Math.max(1, input.ttlMs ?? DEFAULT_SESSION_TTL_MS);
|
|
8727
|
+
const now = Date.now();
|
|
8728
|
+
const session = {
|
|
8729
|
+
requesterId: input.requesterId,
|
|
8730
|
+
expiresAtMs: now + ttlMs,
|
|
8731
|
+
activationId: randomUUID3()
|
|
8732
|
+
};
|
|
8733
|
+
await state.set(sessionKey2(input.sandboxId), session, ttlMs);
|
|
8734
|
+
}
|
|
8735
|
+
async function clearSandboxEgressSession(sandboxId) {
|
|
8736
|
+
const state = getStateAdapter();
|
|
8737
|
+
await state.connect();
|
|
8738
|
+
await state.delete(sessionKey2(sandboxId));
|
|
8739
|
+
}
|
|
8740
|
+
async function getSandboxEgressSession(sandboxId) {
|
|
8741
|
+
const state = getStateAdapter();
|
|
8742
|
+
await state.connect();
|
|
8743
|
+
return parseSession(await state.get(sessionKey2(sandboxId)));
|
|
8744
|
+
}
|
|
8745
|
+
async function setSandboxEgressCredentialLease(sandboxId, session, lease) {
|
|
8746
|
+
const leaseExpiresAtMs = Date.parse(lease.expiresAt);
|
|
8747
|
+
if (!Number.isFinite(leaseExpiresAtMs) || leaseExpiresAtMs <= Date.now()) {
|
|
8748
|
+
return;
|
|
8749
|
+
}
|
|
8750
|
+
const ttlMs = Math.max(
|
|
8751
|
+
1,
|
|
8752
|
+
Math.min(leaseExpiresAtMs, session.expiresAtMs) - Date.now()
|
|
8753
|
+
);
|
|
8754
|
+
const state = getStateAdapter();
|
|
8755
|
+
await state.connect();
|
|
8756
|
+
await state.set(leaseKey(sandboxId, lease.provider, session), lease, ttlMs);
|
|
8757
|
+
}
|
|
8758
|
+
async function getSandboxEgressCredentialLease(sandboxId, provider, session) {
|
|
8759
|
+
const state = getStateAdapter();
|
|
8760
|
+
await state.connect();
|
|
8761
|
+
return parseLease(await state.get(leaseKey(sandboxId, provider, session)));
|
|
8762
|
+
}
|
|
8763
|
+
async function clearSandboxEgressCredentialLease(sandboxId, provider, session) {
|
|
8764
|
+
const state = getStateAdapter();
|
|
8765
|
+
await state.connect();
|
|
8766
|
+
await state.delete(leaseKey(sandboxId, provider, session));
|
|
8767
|
+
}
|
|
8768
|
+
|
|
8466
8769
|
// src/chat/sandbox/http-error-details.ts
|
|
8467
8770
|
var DEFAULT_PREVIEW_LIMIT = 512;
|
|
8468
8771
|
function toTrimmedString(value, maxChars) {
|
|
@@ -8665,6 +8968,7 @@ function throwSandboxOperationError(action, error, includeMissingPath = false) {
|
|
|
8665
8968
|
}
|
|
8666
8969
|
|
|
8667
8970
|
// src/chat/sandbox/session.ts
|
|
8971
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
8668
8972
|
import { Sandbox } from "@vercel/sandbox";
|
|
8669
8973
|
import { createBashTool as createBashTool2 } from "bash-tool";
|
|
8670
8974
|
|
|
@@ -9238,27 +9542,39 @@ var SANDBOX_RUNTIME = "node22";
|
|
|
9238
9542
|
var SANDBOX_RUNTIME_BIN_DIR = `${SANDBOX_WORKSPACE_ROOT}/.junior/bin`;
|
|
9239
9543
|
var SNAPSHOT_BOOT_RETRY_COUNT = 3;
|
|
9240
9544
|
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
|
-
}
|
|
9545
|
+
var SANDBOX_NAME_PREFIX = "junior-";
|
|
9546
|
+
function createBashToolSandboxAdapter(sandbox) {
|
|
9259
9547
|
return {
|
|
9260
|
-
|
|
9261
|
-
|
|
9548
|
+
async executeCommand(command) {
|
|
9549
|
+
const result = await sandbox.runCommand({
|
|
9550
|
+
cmd: "bash",
|
|
9551
|
+
args: ["-c", command]
|
|
9552
|
+
});
|
|
9553
|
+
const [stdout, stderr] = await Promise.all([
|
|
9554
|
+
result.stdout(),
|
|
9555
|
+
result.stderr()
|
|
9556
|
+
]);
|
|
9557
|
+
return {
|
|
9558
|
+
stdout,
|
|
9559
|
+
stderr,
|
|
9560
|
+
exitCode: result.exitCode
|
|
9561
|
+
};
|
|
9562
|
+
},
|
|
9563
|
+
async readFile(filePath) {
|
|
9564
|
+
const content = await sandbox.readFileToBuffer({ path: filePath });
|
|
9565
|
+
if (content == null) {
|
|
9566
|
+
throw new Error(`File not found: ${filePath}`);
|
|
9567
|
+
}
|
|
9568
|
+
return content.toString("utf8");
|
|
9569
|
+
},
|
|
9570
|
+
async writeFiles(files) {
|
|
9571
|
+
await sandbox.writeFiles(
|
|
9572
|
+
files.map((file) => ({
|
|
9573
|
+
path: file.path,
|
|
9574
|
+
content: file.content
|
|
9575
|
+
}))
|
|
9576
|
+
);
|
|
9577
|
+
}
|
|
9262
9578
|
};
|
|
9263
9579
|
}
|
|
9264
9580
|
function truncateOutput(output, maxLength) {
|
|
@@ -9291,23 +9607,36 @@ function createSandboxSessionManager(options) {
|
|
|
9291
9607
|
let availableSkills = [];
|
|
9292
9608
|
let availableReferenceFiles = [];
|
|
9293
9609
|
let toolExecutors;
|
|
9610
|
+
let appliedNetworkPolicyKey;
|
|
9294
9611
|
const timeoutMs = options?.timeoutMs ?? 1e3 * 60 * 30;
|
|
9295
9612
|
const traceContext = options?.traceContext ?? {};
|
|
9296
9613
|
const dependencyProfileHash = getRuntimeDependencyProfileHash(SANDBOX_RUNTIME);
|
|
9614
|
+
const resolveCommandEnv = options?.commandEnv ?? (async () => ({}));
|
|
9297
9615
|
const withSandboxSpan = (name, op, attributes, callback) => withSpan(name, op, traceContext, callback, attributes);
|
|
9298
9616
|
const clearSession = () => {
|
|
9299
9617
|
sandbox = null;
|
|
9300
9618
|
sandboxIdHint = void 0;
|
|
9301
9619
|
toolExecutors = void 0;
|
|
9620
|
+
appliedNetworkPolicyKey = void 0;
|
|
9302
9621
|
};
|
|
9303
|
-
const
|
|
9622
|
+
const createSandboxName = () => `${SANDBOX_NAME_PREFIX}${randomUUID4()}`;
|
|
9623
|
+
const rememberNetworkPolicy = (networkPolicy) => {
|
|
9624
|
+
appliedNetworkPolicyKey = networkPolicy ? JSON.stringify(networkPolicy) : void 0;
|
|
9625
|
+
};
|
|
9626
|
+
const rememberSandbox = async (nextSandbox, rememberOptions) => {
|
|
9304
9627
|
sandbox = nextSandbox;
|
|
9305
9628
|
sandboxIdHint = nextSandbox.sandboxId;
|
|
9306
9629
|
toolExecutors = void 0;
|
|
9307
|
-
|
|
9308
|
-
sandboxId:
|
|
9630
|
+
const acquired = {
|
|
9631
|
+
sandboxId: sandboxIdHint,
|
|
9309
9632
|
...dependencyProfileHash ? { sandboxDependencyProfileHash: dependencyProfileHash } : {}
|
|
9310
|
-
}
|
|
9633
|
+
};
|
|
9634
|
+
await options?.onSandboxAcquired?.(acquired);
|
|
9635
|
+
if (rememberOptions?.recordNetworkPolicy) {
|
|
9636
|
+
rememberNetworkPolicy(
|
|
9637
|
+
options?.createNetworkPolicy?.(nextSandbox.sandboxId)
|
|
9638
|
+
);
|
|
9639
|
+
}
|
|
9311
9640
|
return nextSandbox;
|
|
9312
9641
|
};
|
|
9313
9642
|
const failSetup = (error) => {
|
|
@@ -9322,6 +9651,30 @@ function createSandboxSessionManager(options) {
|
|
|
9322
9651
|
runtimeBinDir: SANDBOX_RUNTIME_BIN_DIR
|
|
9323
9652
|
});
|
|
9324
9653
|
};
|
|
9654
|
+
const refreshNetworkPolicy = async (targetSandbox) => {
|
|
9655
|
+
const networkPolicy = options?.createNetworkPolicy?.(
|
|
9656
|
+
targetSandbox.sandboxId
|
|
9657
|
+
);
|
|
9658
|
+
if (!networkPolicy) {
|
|
9659
|
+
return;
|
|
9660
|
+
}
|
|
9661
|
+
const networkPolicyKey = JSON.stringify(networkPolicy);
|
|
9662
|
+
if (appliedNetworkPolicyKey === networkPolicyKey) {
|
|
9663
|
+
return;
|
|
9664
|
+
}
|
|
9665
|
+
await withSandboxSpan(
|
|
9666
|
+
"sandbox.network_policy.update",
|
|
9667
|
+
"sandbox.update",
|
|
9668
|
+
{
|
|
9669
|
+
"app.sandbox.reused": true,
|
|
9670
|
+
"app.sandbox.source": "id_hint"
|
|
9671
|
+
},
|
|
9672
|
+
async () => {
|
|
9673
|
+
await targetSandbox.update({ networkPolicy });
|
|
9674
|
+
}
|
|
9675
|
+
);
|
|
9676
|
+
appliedNetworkPolicyKey = networkPolicyKey;
|
|
9677
|
+
};
|
|
9325
9678
|
const ensureSandboxReachable = async (targetSandbox, source) => {
|
|
9326
9679
|
await withSandboxSpan(
|
|
9327
9680
|
"sandbox.reuse_probe",
|
|
@@ -9341,23 +9694,6 @@ function createSandboxSessionManager(options) {
|
|
|
9341
9694
|
}
|
|
9342
9695
|
);
|
|
9343
9696
|
};
|
|
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
9697
|
const recreateUnavailableSandbox = async (source) => {
|
|
9362
9698
|
setSpanAttributes({
|
|
9363
9699
|
"app.sandbox.recovery.attempted": true,
|
|
@@ -9370,17 +9706,22 @@ function createSandboxSessionManager(options) {
|
|
|
9370
9706
|
});
|
|
9371
9707
|
return replacement;
|
|
9372
9708
|
};
|
|
9373
|
-
const createSandboxFromSnapshot = async (snapshotId, sandboxCredentials) => {
|
|
9709
|
+
const createSandboxFromSnapshot = async (snapshotId, sandboxCredentials, initialSandboxName) => {
|
|
9374
9710
|
for (let attempt = 0; attempt < SNAPSHOT_BOOT_RETRY_COUNT; attempt += 1) {
|
|
9711
|
+
const sandboxName = attempt === 0 ? initialSandboxName : createSandboxName();
|
|
9712
|
+
const networkPolicy = options?.createNetworkPolicy?.(sandboxName);
|
|
9375
9713
|
try {
|
|
9376
|
-
return
|
|
9377
|
-
|
|
9378
|
-
|
|
9379
|
-
|
|
9380
|
-
|
|
9381
|
-
|
|
9382
|
-
|
|
9383
|
-
|
|
9714
|
+
return createSandboxInstance(
|
|
9715
|
+
await Sandbox.create({
|
|
9716
|
+
timeout: timeoutMs,
|
|
9717
|
+
...networkPolicy ? { name: sandboxName, persistent: false, networkPolicy } : {},
|
|
9718
|
+
source: {
|
|
9719
|
+
type: "snapshot",
|
|
9720
|
+
snapshotId
|
|
9721
|
+
},
|
|
9722
|
+
...sandboxCredentials ?? {}
|
|
9723
|
+
})
|
|
9724
|
+
);
|
|
9384
9725
|
} catch (error) {
|
|
9385
9726
|
if (!isSnapshottingError(error) || attempt === SNAPSHOT_BOOT_RETRY_COUNT - 1) {
|
|
9386
9727
|
throw error;
|
|
@@ -9405,18 +9746,23 @@ function createSandboxSessionManager(options) {
|
|
|
9405
9746
|
});
|
|
9406
9747
|
};
|
|
9407
9748
|
const createSandboxFromResolvedSnapshot = async (params) => {
|
|
9408
|
-
const { runtime, snapshot, sandboxCredentials } = params;
|
|
9749
|
+
const { runtime, snapshot, sandboxCredentials, sandboxName } = params;
|
|
9409
9750
|
if (!snapshot.snapshotId) {
|
|
9410
|
-
|
|
9411
|
-
|
|
9412
|
-
|
|
9413
|
-
|
|
9414
|
-
|
|
9751
|
+
const networkPolicy = options?.createNetworkPolicy?.(sandboxName);
|
|
9752
|
+
return createSandboxInstance(
|
|
9753
|
+
await Sandbox.create({
|
|
9754
|
+
timeout: timeoutMs,
|
|
9755
|
+
runtime,
|
|
9756
|
+
...networkPolicy ? { name: sandboxName, persistent: false, networkPolicy } : {},
|
|
9757
|
+
...sandboxCredentials ?? {}
|
|
9758
|
+
})
|
|
9759
|
+
);
|
|
9415
9760
|
}
|
|
9416
9761
|
try {
|
|
9417
9762
|
return await createSandboxFromSnapshot(
|
|
9418
9763
|
snapshot.snapshotId,
|
|
9419
|
-
sandboxCredentials
|
|
9764
|
+
sandboxCredentials,
|
|
9765
|
+
sandboxName
|
|
9420
9766
|
);
|
|
9421
9767
|
} catch (error) {
|
|
9422
9768
|
if (!isSnapshotMissingError(error)) {
|
|
@@ -9436,13 +9782,15 @@ function createSandboxSessionManager(options) {
|
|
|
9436
9782
|
}
|
|
9437
9783
|
return await createSandboxFromSnapshot(
|
|
9438
9784
|
rebuiltSnapshot.snapshotId,
|
|
9439
|
-
sandboxCredentials
|
|
9785
|
+
sandboxCredentials,
|
|
9786
|
+
sandboxName
|
|
9440
9787
|
);
|
|
9441
9788
|
}
|
|
9442
9789
|
};
|
|
9443
9790
|
const createFreshSandbox = async () => {
|
|
9444
9791
|
const runtime = SANDBOX_RUNTIME;
|
|
9445
9792
|
const sandboxCredentials = getVercelSandboxCredentials();
|
|
9793
|
+
const sandboxName = createSandboxName();
|
|
9446
9794
|
let createdSandbox;
|
|
9447
9795
|
try {
|
|
9448
9796
|
createdSandbox = await withSandboxSpan(
|
|
@@ -9462,7 +9810,8 @@ function createSandboxSessionManager(options) {
|
|
|
9462
9810
|
return await createSandboxFromResolvedSnapshot({
|
|
9463
9811
|
runtime,
|
|
9464
9812
|
snapshot,
|
|
9465
|
-
sandboxCredentials
|
|
9813
|
+
sandboxCredentials,
|
|
9814
|
+
sandboxName
|
|
9466
9815
|
});
|
|
9467
9816
|
}
|
|
9468
9817
|
);
|
|
@@ -9474,7 +9823,7 @@ function createSandboxSessionManager(options) {
|
|
|
9474
9823
|
} catch (error) {
|
|
9475
9824
|
return failSetup(error);
|
|
9476
9825
|
}
|
|
9477
|
-
return await rememberSandbox(createdSandbox);
|
|
9826
|
+
return await rememberSandbox(createdSandbox, { recordNetworkPolicy: true });
|
|
9478
9827
|
};
|
|
9479
9828
|
const discardHintIfProfileChanged = () => {
|
|
9480
9829
|
if (sandbox || !sandboxIdHint || dependencyProfileHash === options?.sandboxDependencyProfileHash) {
|
|
@@ -9497,6 +9846,7 @@ function createSandboxSessionManager(options) {
|
|
|
9497
9846
|
}
|
|
9498
9847
|
try {
|
|
9499
9848
|
await ensureSandboxReachable(cachedSandbox, "memory");
|
|
9849
|
+
await refreshNetworkPolicy(cachedSandbox);
|
|
9500
9850
|
return cachedSandbox;
|
|
9501
9851
|
} catch (error) {
|
|
9502
9852
|
if (isSandboxUnavailableError(error)) {
|
|
@@ -9519,15 +9869,19 @@ function createSandboxSessionManager(options) {
|
|
|
9519
9869
|
"app.sandbox.reused": true,
|
|
9520
9870
|
"app.sandbox.source": "id_hint"
|
|
9521
9871
|
},
|
|
9522
|
-
async () =>
|
|
9523
|
-
|
|
9524
|
-
|
|
9525
|
-
|
|
9872
|
+
async () => createSandboxInstance(
|
|
9873
|
+
await Sandbox.get({
|
|
9874
|
+
name: sandboxIdHint,
|
|
9875
|
+
resume: true,
|
|
9876
|
+
...sandboxCredentials ?? {}
|
|
9877
|
+
})
|
|
9878
|
+
)
|
|
9526
9879
|
);
|
|
9527
9880
|
} catch {
|
|
9528
9881
|
return null;
|
|
9529
9882
|
}
|
|
9530
9883
|
try {
|
|
9884
|
+
await refreshNetworkPolicy(hintedSandbox);
|
|
9531
9885
|
await syncSkills(hintedSandbox);
|
|
9532
9886
|
return await rememberSandbox(hintedSandbox);
|
|
9533
9887
|
} catch (error) {
|
|
@@ -9582,37 +9936,6 @@ function createSandboxSessionManager(options) {
|
|
|
9582
9936
|
stderrTruncated: stderr.truncated
|
|
9583
9937
|
};
|
|
9584
9938
|
};
|
|
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
9939
|
const extendKeepAlive = async (activeSandbox) => {
|
|
9617
9940
|
const keepAliveMs = parseKeepAliveMs();
|
|
9618
9941
|
if (keepAliveMs === 0) {
|
|
@@ -9633,6 +9956,7 @@ function createSandboxSessionManager(options) {
|
|
|
9633
9956
|
}
|
|
9634
9957
|
};
|
|
9635
9958
|
const buildToolExecutors = async (sandboxInstance) => {
|
|
9959
|
+
const activeSandboxId = sandboxInstance.sandboxId;
|
|
9636
9960
|
const toolkit = await withSandboxSpan(
|
|
9637
9961
|
"sandbox.bash_tool.init",
|
|
9638
9962
|
"sandbox.tool.init",
|
|
@@ -9641,7 +9965,7 @@ function createSandboxSessionManager(options) {
|
|
|
9641
9965
|
"app.sandbox.destination": SANDBOX_WORKSPACE_ROOT
|
|
9642
9966
|
},
|
|
9643
9967
|
async () => await createBashTool2({
|
|
9644
|
-
sandbox: sandboxInstance,
|
|
9968
|
+
sandbox: createBashToolSandboxAdapter(sandboxInstance),
|
|
9645
9969
|
destination: SANDBOX_WORKSPACE_ROOT
|
|
9646
9970
|
})
|
|
9647
9971
|
);
|
|
@@ -9652,47 +9976,69 @@ function createSandboxSessionManager(options) {
|
|
|
9652
9976
|
}
|
|
9653
9977
|
return {
|
|
9654
9978
|
bash: async (input) => {
|
|
9655
|
-
|
|
9656
|
-
env: input.env,
|
|
9657
|
-
pathPrefix: `${SANDBOX_RUNTIME_BIN_DIR}:$PATH`
|
|
9658
|
-
});
|
|
9659
|
-
const controller = input.timeoutMs && input.timeoutMs > 0 ? new AbortController() : void 0;
|
|
9979
|
+
await options?.beforeCommand?.(activeSandboxId);
|
|
9660
9980
|
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
|
-
}
|
|
9981
|
+
let timeoutId;
|
|
9982
|
+
let commandFinished = false;
|
|
9983
|
+
const finishCommand = async () => {
|
|
9984
|
+
if (commandFinished) {
|
|
9985
|
+
return;
|
|
9694
9986
|
}
|
|
9695
|
-
|
|
9987
|
+
commandFinished = true;
|
|
9988
|
+
await options?.afterCommand?.(activeSandboxId);
|
|
9989
|
+
};
|
|
9990
|
+
const finishCommandBestEffort = async () => {
|
|
9991
|
+
try {
|
|
9992
|
+
await finishCommand();
|
|
9993
|
+
} catch (error) {
|
|
9994
|
+
logWarn(
|
|
9995
|
+
"sandbox_command_cleanup_failed",
|
|
9996
|
+
traceContext,
|
|
9997
|
+
{
|
|
9998
|
+
"app.sandbox.id": activeSandboxId,
|
|
9999
|
+
"error.type": error instanceof Error ? error.name : "sandbox_command_cleanup_error"
|
|
10000
|
+
},
|
|
10001
|
+
"Sandbox command cleanup failed"
|
|
10002
|
+
);
|
|
10003
|
+
}
|
|
10004
|
+
};
|
|
10005
|
+
try {
|
|
10006
|
+
const sandboxCommandEnv = await resolveCommandEnv();
|
|
10007
|
+
const script = buildNonInteractiveShellScript(input.command, {
|
|
10008
|
+
env: { ...sandboxCommandEnv, ...input.env ?? {} },
|
|
10009
|
+
pathPrefix: `${SANDBOX_RUNTIME_BIN_DIR}:$PATH`
|
|
10010
|
+
});
|
|
10011
|
+
const controller = input.timeoutMs && input.timeoutMs > 0 ? new AbortController() : void 0;
|
|
10012
|
+
timeoutId = controller ? setTimeout(() => {
|
|
10013
|
+
timedOut = true;
|
|
10014
|
+
controller.abort();
|
|
10015
|
+
}, input.timeoutMs) : void 0;
|
|
10016
|
+
const commandResult2 = await sandboxInstance.runCommand({
|
|
10017
|
+
cmd: "bash",
|
|
10018
|
+
args: ["-c", script],
|
|
10019
|
+
cwd: SANDBOX_WORKSPACE_ROOT,
|
|
10020
|
+
...controller ? { signal: controller.signal } : {}
|
|
10021
|
+
});
|
|
10022
|
+
await finishCommandBestEffort();
|
|
10023
|
+
return await readCommandOutput(commandResult2);
|
|
10024
|
+
} catch (error) {
|
|
10025
|
+
if (timedOut) {
|
|
10026
|
+
return {
|
|
10027
|
+
stdout: "",
|
|
10028
|
+
stderr: `Command timed out after ${input.timeoutMs}ms`,
|
|
10029
|
+
exitCode: 124,
|
|
10030
|
+
stdoutTruncated: false,
|
|
10031
|
+
stderrTruncated: false,
|
|
10032
|
+
timedOut: true
|
|
10033
|
+
};
|
|
10034
|
+
}
|
|
10035
|
+
throw error;
|
|
10036
|
+
} finally {
|
|
10037
|
+
if (timeoutId) {
|
|
10038
|
+
clearTimeout(timeoutId);
|
|
10039
|
+
}
|
|
10040
|
+
await finishCommandBestEffort();
|
|
10041
|
+
}
|
|
9696
10042
|
},
|
|
9697
10043
|
readFile: async (input) => await executeReadFile(input, {
|
|
9698
10044
|
toolCallId: "sandbox-read-file",
|
|
@@ -9725,7 +10071,7 @@ function createSandboxSessionManager(options) {
|
|
|
9725
10071
|
availableReferenceFiles = [...files];
|
|
9726
10072
|
},
|
|
9727
10073
|
getSandboxId() {
|
|
9728
|
-
return sandbox
|
|
10074
|
+
return sandbox ? sandbox.sandboxId : sandboxIdHint;
|
|
9729
10075
|
},
|
|
9730
10076
|
getDependencyProfileHash() {
|
|
9731
10077
|
return dependencyProfileHash;
|
|
@@ -9748,7 +10094,7 @@ function createSandboxSessionManager(options) {
|
|
|
9748
10094
|
"app.sandbox.stop.blocking": true
|
|
9749
10095
|
},
|
|
9750
10096
|
async () => {
|
|
9751
|
-
await activeSandbox.stop(
|
|
10097
|
+
await activeSandbox.stop();
|
|
9752
10098
|
}
|
|
9753
10099
|
);
|
|
9754
10100
|
sandbox = null;
|
|
@@ -9767,21 +10113,6 @@ var SANDBOX_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
|
9767
10113
|
"listDir",
|
|
9768
10114
|
"writeFile"
|
|
9769
10115
|
]);
|
|
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
10116
|
function parseEnv(raw) {
|
|
9786
10117
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
9787
10118
|
return void 0;
|
|
@@ -9790,27 +10121,33 @@ function parseEnv(raw) {
|
|
|
9790
10121
|
Object.entries(raw).filter(([, value]) => typeof value === "string").map(([key, value]) => [key, value])
|
|
9791
10122
|
);
|
|
9792
10123
|
}
|
|
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
10124
|
function createSandboxExecutor(options) {
|
|
9805
10125
|
let availableSkills = [];
|
|
9806
10126
|
let referenceFiles = [];
|
|
9807
10127
|
const traceContext = options?.traceContext ?? {};
|
|
10128
|
+
const credentialEgress = options?.credentialEgress;
|
|
10129
|
+
const syncSandboxEgressSession = credentialEgress ? async (sandboxId) => {
|
|
10130
|
+
await upsertSandboxEgressSession({
|
|
10131
|
+
sandboxId,
|
|
10132
|
+
requesterId: credentialEgress.requesterId,
|
|
10133
|
+
ttlMs: options?.timeoutMs
|
|
10134
|
+
});
|
|
10135
|
+
} : void 0;
|
|
10136
|
+
const clearSandboxEgressSessionForCommand = credentialEgress ? async (sandboxId) => {
|
|
10137
|
+
await clearSandboxEgressSession(sandboxId);
|
|
10138
|
+
} : void 0;
|
|
9808
10139
|
const sessionManager = createSandboxSessionManager({
|
|
9809
10140
|
sandboxId: options?.sandboxId,
|
|
9810
10141
|
sandboxDependencyProfileHash: options?.sandboxDependencyProfileHash,
|
|
9811
10142
|
timeoutMs: options?.timeoutMs,
|
|
9812
10143
|
traceContext,
|
|
9813
|
-
|
|
10144
|
+
commandEnv: credentialEgress ? async () => await resolveSandboxCommandEnvironment() : void 0,
|
|
10145
|
+
createNetworkPolicy: credentialEgress ? buildSandboxEgressNetworkPolicy : void 0,
|
|
10146
|
+
beforeCommand: syncSandboxEgressSession,
|
|
10147
|
+
afterCommand: clearSandboxEgressSessionForCommand,
|
|
10148
|
+
onSandboxAcquired: async (sandbox) => {
|
|
10149
|
+
await options?.onSandboxAcquired?.(sandbox);
|
|
10150
|
+
}
|
|
9814
10151
|
});
|
|
9815
10152
|
const withSandboxSpan = (name, op, attributes, callback) => withSpan(name, op, traceContext, callback, attributes);
|
|
9816
10153
|
const logSandboxBootRequest = (trigger, details = {}) => {
|
|
@@ -9828,7 +10165,6 @@ function createSandboxExecutor(options) {
|
|
|
9828
10165
|
);
|
|
9829
10166
|
};
|
|
9830
10167
|
const executeBashTool = async (rawInput, command) => {
|
|
9831
|
-
const headerTransforms = parseHeaderTransforms(rawInput.headerTransforms);
|
|
9832
10168
|
const env = parseEnv(rawInput.env);
|
|
9833
10169
|
const timeoutMs = positiveInteger(rawInput.timeoutMs);
|
|
9834
10170
|
logSandboxBootRequest("tool.bash", {
|
|
@@ -9845,7 +10181,6 @@ function createSandboxExecutor(options) {
|
|
|
9845
10181
|
try {
|
|
9846
10182
|
const response = await executeBash({
|
|
9847
10183
|
command,
|
|
9848
|
-
...headerTransforms ? { headerTransforms } : {},
|
|
9849
10184
|
...env ? { env } : {},
|
|
9850
10185
|
...timeoutMs ? { timeoutMs } : {}
|
|
9851
10186
|
});
|
|
@@ -10149,7 +10484,7 @@ function createSandboxExecutor(options) {
|
|
|
10149
10484
|
return SANDBOX_TOOL_NAMES.has(toolName);
|
|
10150
10485
|
},
|
|
10151
10486
|
async createSandbox() {
|
|
10152
|
-
return
|
|
10487
|
+
return await sessionManager.createSandbox();
|
|
10153
10488
|
},
|
|
10154
10489
|
execute,
|
|
10155
10490
|
async dispose() {
|
|
@@ -10250,34 +10585,6 @@ function buildSandboxInput(toolName, params) {
|
|
|
10250
10585
|
return params;
|
|
10251
10586
|
}
|
|
10252
10587
|
|
|
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
10588
|
// src/chat/tools/execution/normalize-result.ts
|
|
10282
10589
|
function isStructuredToolExecutionResult(value) {
|
|
10283
10590
|
const content = value?.content;
|
|
@@ -10367,7 +10674,7 @@ function handleToolExecutionError(error, toolName, toolCallId, shouldTrace, trac
|
|
|
10367
10674
|
}
|
|
10368
10675
|
|
|
10369
10676
|
// src/chat/tools/agent-tools.ts
|
|
10370
|
-
function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor,
|
|
10677
|
+
function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor, pluginAuthOrchestration, onToolCall) {
|
|
10371
10678
|
const shouldTrace = shouldEmitDevAgentTrace();
|
|
10372
10679
|
return Object.entries(tools).map(([toolName, toolDef]) => ({
|
|
10373
10680
|
name: toolName,
|
|
@@ -10407,21 +10714,11 @@ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor
|
|
|
10407
10714
|
};
|
|
10408
10715
|
}
|
|
10409
10716
|
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
10717
|
const sandboxInput = buildSandboxInput(toolName, parsed);
|
|
10417
10718
|
const isSandbox = Boolean(sandboxExecutor?.canExecute(toolName));
|
|
10418
10719
|
const result = isSandbox ? await sandboxExecutor.execute({
|
|
10419
10720
|
toolName,
|
|
10420
|
-
input:
|
|
10421
|
-
...sandboxInput,
|
|
10422
|
-
...injection.headerTransforms ? { headerTransforms: injection.headerTransforms } : {},
|
|
10423
|
-
...injection.env ? { env: injection.env } : {}
|
|
10424
|
-
} : sandboxInput
|
|
10721
|
+
input: sandboxInput
|
|
10425
10722
|
}) : await toolDef.execute(parsed, {
|
|
10426
10723
|
experimental_context: sandbox
|
|
10427
10724
|
});
|
|
@@ -11284,6 +11581,7 @@ ${typeof result.stderr === "string" ? result.stderr : ""}`.toLowerCase();
|
|
|
11284
11581
|
return false;
|
|
11285
11582
|
}
|
|
11286
11583
|
return [
|
|
11584
|
+
/\bjunior-auth-required\b/,
|
|
11287
11585
|
/\b401\b/,
|
|
11288
11586
|
/\bunauthorized\b/,
|
|
11289
11587
|
/\bbad credentials\b/,
|
|
@@ -11296,6 +11594,33 @@ ${typeof result.stderr === "string" ? result.stderr : ""}`.toLowerCase();
|
|
|
11296
11594
|
/\breauthoriz/
|
|
11297
11595
|
].some((pattern) => pattern.test(text));
|
|
11298
11596
|
}
|
|
11597
|
+
function commandText(details) {
|
|
11598
|
+
if (!details || typeof details !== "object") {
|
|
11599
|
+
return "";
|
|
11600
|
+
}
|
|
11601
|
+
const result = details;
|
|
11602
|
+
return `${typeof result.stdout === "string" ? result.stdout : ""}
|
|
11603
|
+
${typeof result.stderr === "string" ? result.stderr : ""}`;
|
|
11604
|
+
}
|
|
11605
|
+
function explicitAuthRequiredProvider(details) {
|
|
11606
|
+
const match = /\bjunior-auth-required\s+provider=([a-z0-9-]+)\b/.exec(
|
|
11607
|
+
commandText(details).toLowerCase()
|
|
11608
|
+
);
|
|
11609
|
+
return match?.[1];
|
|
11610
|
+
}
|
|
11611
|
+
function registeredProviderNames() {
|
|
11612
|
+
const providers = /* @__PURE__ */ new Set();
|
|
11613
|
+
for (const plugin of getPluginProviders()) {
|
|
11614
|
+
const domains = [
|
|
11615
|
+
...plugin.manifest.credentials?.domains ?? [],
|
|
11616
|
+
...plugin.manifest.domains ?? []
|
|
11617
|
+
];
|
|
11618
|
+
if (domains.length > 0) {
|
|
11619
|
+
providers.add(plugin.manifest.name);
|
|
11620
|
+
}
|
|
11621
|
+
}
|
|
11622
|
+
return [...providers].sort((left, right) => left.localeCompare(right));
|
|
11623
|
+
}
|
|
11299
11624
|
function commandTargetsProvider(provider, command, details) {
|
|
11300
11625
|
const normalizedCommand = command.trim().toLowerCase();
|
|
11301
11626
|
if (!normalizedCommand) {
|
|
@@ -11310,16 +11635,15 @@ function commandTargetsProvider(provider, command, details) {
|
|
|
11310
11635
|
const credentials = manifest?.credentials;
|
|
11311
11636
|
if (credentials) {
|
|
11312
11637
|
candidates.add(credentials.authTokenEnv.toLowerCase());
|
|
11313
|
-
for (const domain of credentials.
|
|
11638
|
+
for (const domain of credentials.domains) {
|
|
11314
11639
|
candidates.add(domain.toLowerCase());
|
|
11315
11640
|
}
|
|
11316
11641
|
}
|
|
11317
|
-
for (const domain of manifest?.
|
|
11642
|
+
for (const domain of manifest?.domains ?? []) {
|
|
11318
11643
|
candidates.add(domain.toLowerCase());
|
|
11319
11644
|
}
|
|
11320
11645
|
const combinedText = `${normalizedCommand}
|
|
11321
|
-
${details.
|
|
11322
|
-
${details.stderr?.toLowerCase() ?? ""}`;
|
|
11646
|
+
${commandText(details).toLowerCase()}`;
|
|
11323
11647
|
return [...candidates].some((candidate) => combinedText.includes(candidate));
|
|
11324
11648
|
}
|
|
11325
11649
|
function createPluginAuthOrchestration(deps, abortAgent) {
|
|
@@ -11377,23 +11701,22 @@ function createPluginAuthOrchestration(deps, abortAgent) {
|
|
|
11377
11701
|
abortAgent();
|
|
11378
11702
|
throw pendingPause;
|
|
11379
11703
|
};
|
|
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
11704
|
return {
|
|
11393
|
-
handleCredentialUnavailable,
|
|
11394
11705
|
handleCommandFailure: async (input) => {
|
|
11395
|
-
const
|
|
11396
|
-
|
|
11706
|
+
const providers = registeredProviderNames();
|
|
11707
|
+
const authFailure = isCommandAuthFailure(input.details);
|
|
11708
|
+
if (!authFailure) {
|
|
11709
|
+
return;
|
|
11710
|
+
}
|
|
11711
|
+
const explicitProvider = explicitAuthRequiredProvider(input.details);
|
|
11712
|
+
const provider = explicitProvider && providers.includes(explicitProvider) ? explicitProvider : providers.find(
|
|
11713
|
+
(availableProvider) => commandTargetsProvider(
|
|
11714
|
+
availableProvider,
|
|
11715
|
+
input.command,
|
|
11716
|
+
input.details
|
|
11717
|
+
)
|
|
11718
|
+
);
|
|
11719
|
+
if (!provider || !deps.requesterId || !deps.userTokenStore || !getPluginOAuthConfig(provider)) {
|
|
11397
11720
|
return;
|
|
11398
11721
|
}
|
|
11399
11722
|
await startAuthorizationPause(provider, input.activeSkill, {
|
|
@@ -11659,14 +11982,15 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
11659
11982
|
...context.configuration ?? {},
|
|
11660
11983
|
...persistedConfigurationValues
|
|
11661
11984
|
};
|
|
11662
|
-
const
|
|
11663
|
-
requesterId: context.requester?.userId
|
|
11664
|
-
});
|
|
11985
|
+
const requesterId = context.requester?.userId;
|
|
11665
11986
|
const userTokenStore = createUserTokenStore();
|
|
11666
11987
|
sandboxExecutor = createSandboxExecutor({
|
|
11667
11988
|
sandboxId: context.sandbox?.sandboxId,
|
|
11668
11989
|
sandboxDependencyProfileHash: context.sandbox?.sandboxDependencyProfileHash,
|
|
11669
11990
|
traceContext: spanContext,
|
|
11991
|
+
credentialEgress: requesterId ? {
|
|
11992
|
+
requesterId
|
|
11993
|
+
} : void 0,
|
|
11670
11994
|
onSandboxAcquired: async (sandbox2) => {
|
|
11671
11995
|
lastKnownSandboxId = sandbox2.sandboxId;
|
|
11672
11996
|
lastKnownSandboxDependencyProfileHash = sandbox2.sandboxDependencyProfileHash;
|
|
@@ -11830,25 +12154,6 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
11830
12154
|
const syncResumeState = () => {
|
|
11831
12155
|
loadedSkillNamesForResume = activeSkills.map((skill) => skill.name);
|
|
11832
12156
|
};
|
|
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
12157
|
setTags({
|
|
11853
12158
|
conversationId: spanContext.conversationId,
|
|
11854
12159
|
slackThreadId: context.correlation?.threadId,
|
|
@@ -11890,10 +12195,6 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
11890
12195
|
if (mcpAuth.getPendingPause()) {
|
|
11891
12196
|
return void 0;
|
|
11892
12197
|
}
|
|
11893
|
-
await enableSkillCredentials(
|
|
11894
|
-
effective,
|
|
11895
|
-
`skill:${effective.name}:turn:load`
|
|
11896
|
-
);
|
|
11897
12198
|
if (!effective.pluginProvider) {
|
|
11898
12199
|
return void 0;
|
|
11899
12200
|
}
|
|
@@ -11946,7 +12247,6 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
11946
12247
|
timeoutResumeMessages = existingCheckpoint?.piMessages ?? [];
|
|
11947
12248
|
throw mcpAuth.getPendingPause();
|
|
11948
12249
|
}
|
|
11949
|
-
await enableSkillCredentials(skill, `skill:${skill.name}:turn:resume`);
|
|
11950
12250
|
}
|
|
11951
12251
|
syncResumeState();
|
|
11952
12252
|
const activeMcpCatalogs = toActiveMcpCatalogSummaries(
|
|
@@ -12007,7 +12307,6 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
12007
12307
|
spanContext,
|
|
12008
12308
|
context.onStatus,
|
|
12009
12309
|
sandboxExecutor,
|
|
12010
|
-
capabilityRuntime,
|
|
12011
12310
|
pluginAuth,
|
|
12012
12311
|
onToolCall
|
|
12013
12312
|
);
|
|
@@ -12017,7 +12316,6 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
12017
12316
|
spanContext,
|
|
12018
12317
|
context.onStatus,
|
|
12019
12318
|
sandboxExecutor,
|
|
12020
|
-
capabilityRuntime,
|
|
12021
12319
|
pluginAuth,
|
|
12022
12320
|
onToolCall
|
|
12023
12321
|
);
|
|
@@ -14368,6 +14666,337 @@ async function GET5(request, provider, waitUntil) {
|
|
|
14368
14666
|
});
|
|
14369
14667
|
}
|
|
14370
14668
|
|
|
14669
|
+
// src/chat/sandbox/egress-oidc.ts
|
|
14670
|
+
import {
|
|
14671
|
+
createRemoteJWKSet,
|
|
14672
|
+
decodeJwt,
|
|
14673
|
+
jwtVerify
|
|
14674
|
+
} from "jose";
|
|
14675
|
+
var OIDC_DISCOVERY_CACHE_TTL_MS = 60 * 60 * 1e3;
|
|
14676
|
+
var OIDC_DISCOVERY_CACHE_MAX_ENTRIES = 8;
|
|
14677
|
+
var jwksByIssuer = /* @__PURE__ */ new Map();
|
|
14678
|
+
function buildDiscoveryUrl(issuer) {
|
|
14679
|
+
const url = new URL(issuer);
|
|
14680
|
+
if (url.protocol !== "https:" || url.hostname !== "oidc.vercel.com") {
|
|
14681
|
+
throw new Error("Unexpected Vercel OIDC issuer");
|
|
14682
|
+
}
|
|
14683
|
+
url.pathname = `${url.pathname.replace(/\/$/, "")}/.well-known/openid-configuration`;
|
|
14684
|
+
url.search = "";
|
|
14685
|
+
url.hash = "";
|
|
14686
|
+
return url;
|
|
14687
|
+
}
|
|
14688
|
+
function buildJwksUrl(value) {
|
|
14689
|
+
const url = new URL(value);
|
|
14690
|
+
if (url.protocol !== "https:") {
|
|
14691
|
+
throw new Error("Vercel OIDC discovery jwks_uri must use HTTPS");
|
|
14692
|
+
}
|
|
14693
|
+
return url;
|
|
14694
|
+
}
|
|
14695
|
+
async function getJwks(issuer) {
|
|
14696
|
+
const now = Date.now();
|
|
14697
|
+
const cached = jwksByIssuer.get(issuer);
|
|
14698
|
+
if (cached && cached.expiresAtMs > now) {
|
|
14699
|
+
return cached.jwks;
|
|
14700
|
+
}
|
|
14701
|
+
if (cached) {
|
|
14702
|
+
jwksByIssuer.delete(issuer);
|
|
14703
|
+
}
|
|
14704
|
+
const response = await fetch(buildDiscoveryUrl(issuer), {
|
|
14705
|
+
redirect: "error"
|
|
14706
|
+
});
|
|
14707
|
+
if (!response.ok) {
|
|
14708
|
+
throw new Error("Unable to load Vercel OIDC discovery metadata");
|
|
14709
|
+
}
|
|
14710
|
+
const config = await response.json();
|
|
14711
|
+
if (!config.jwks_uri) {
|
|
14712
|
+
throw new Error("Vercel OIDC discovery metadata did not include jwks_uri");
|
|
14713
|
+
}
|
|
14714
|
+
const jwks = createRemoteJWKSet(buildJwksUrl(config.jwks_uri));
|
|
14715
|
+
if (!jwksByIssuer.has(issuer) && jwksByIssuer.size >= OIDC_DISCOVERY_CACHE_MAX_ENTRIES) {
|
|
14716
|
+
const oldestIssuer = jwksByIssuer.keys().next().value;
|
|
14717
|
+
if (oldestIssuer) {
|
|
14718
|
+
jwksByIssuer.delete(oldestIssuer);
|
|
14719
|
+
}
|
|
14720
|
+
}
|
|
14721
|
+
jwksByIssuer.set(issuer, {
|
|
14722
|
+
jwks,
|
|
14723
|
+
expiresAtMs: now + OIDC_DISCOVERY_CACHE_TTL_MS
|
|
14724
|
+
});
|
|
14725
|
+
return jwks;
|
|
14726
|
+
}
|
|
14727
|
+
function expectedVercelOidcAudience() {
|
|
14728
|
+
const audience = process.env.VERCEL_OIDC_AUDIENCE?.trim();
|
|
14729
|
+
if (!audience) {
|
|
14730
|
+
throw new Error("VERCEL_OIDC_AUDIENCE is required for sandbox egress OIDC");
|
|
14731
|
+
}
|
|
14732
|
+
return audience;
|
|
14733
|
+
}
|
|
14734
|
+
function validateVercelSandboxOidcClaims(payload, sandboxId) {
|
|
14735
|
+
const expectedTeamId = process.env.VERCEL_TEAM_ID?.trim();
|
|
14736
|
+
const expectedProjectId = process.env.VERCEL_PROJECT_ID?.trim();
|
|
14737
|
+
if (!expectedProjectId) {
|
|
14738
|
+
throw new Error("VERCEL_PROJECT_ID is required for sandbox egress OIDC");
|
|
14739
|
+
}
|
|
14740
|
+
if (expectedTeamId && (typeof payload.owner_id !== "string" || payload.owner_id !== expectedTeamId)) {
|
|
14741
|
+
throw new Error("Vercel OIDC token belongs to a different team");
|
|
14742
|
+
}
|
|
14743
|
+
if (typeof payload.project_id !== "string" || payload.project_id !== expectedProjectId) {
|
|
14744
|
+
throw new Error("Vercel OIDC token belongs to a different project");
|
|
14745
|
+
}
|
|
14746
|
+
if (payload.sandbox_id !== sandboxId) {
|
|
14747
|
+
throw new Error("Vercel OIDC token belongs to a different sandbox");
|
|
14748
|
+
}
|
|
14749
|
+
}
|
|
14750
|
+
async function verifyVercelSandboxOidcToken(token, sandboxId) {
|
|
14751
|
+
const unverified = decodeJwt(token);
|
|
14752
|
+
if (typeof unverified.iss !== "string") {
|
|
14753
|
+
throw new Error("Vercel OIDC token did not include an issuer");
|
|
14754
|
+
}
|
|
14755
|
+
const audience = expectedVercelOidcAudience();
|
|
14756
|
+
const jwks = await getJwks(unverified.iss);
|
|
14757
|
+
const verified = await jwtVerify(token, jwks, {
|
|
14758
|
+
issuer: unverified.iss,
|
|
14759
|
+
audience
|
|
14760
|
+
});
|
|
14761
|
+
validateVercelSandboxOidcClaims(verified.payload, sandboxId);
|
|
14762
|
+
return verified.payload;
|
|
14763
|
+
}
|
|
14764
|
+
|
|
14765
|
+
// src/chat/sandbox/egress-proxy.ts
|
|
14766
|
+
var OIDC_TOKEN_HEADER = "vercel-sandbox-oidc-token";
|
|
14767
|
+
var FORWARDED_HOST_HEADER = "vercel-forwarded-host";
|
|
14768
|
+
var FORWARDED_SCHEME_HEADER = "vercel-forwarded-scheme";
|
|
14769
|
+
var FORWARDED_PORT_HEADER = "vercel-forwarded-port";
|
|
14770
|
+
var ROUTE_PREFIX = "/api/internal/sandbox-egress";
|
|
14771
|
+
var HOP_BY_HOP_HEADERS = /* @__PURE__ */ new Set([
|
|
14772
|
+
"connection",
|
|
14773
|
+
"host",
|
|
14774
|
+
"keep-alive",
|
|
14775
|
+
"proxy-authenticate",
|
|
14776
|
+
"proxy-authorization",
|
|
14777
|
+
"te",
|
|
14778
|
+
"trailer",
|
|
14779
|
+
"transfer-encoding",
|
|
14780
|
+
"upgrade"
|
|
14781
|
+
]);
|
|
14782
|
+
var PROXY_ONLY_HEADERS = /* @__PURE__ */ new Set([
|
|
14783
|
+
OIDC_TOKEN_HEADER,
|
|
14784
|
+
FORWARDED_HOST_HEADER,
|
|
14785
|
+
FORWARDED_SCHEME_HEADER,
|
|
14786
|
+
FORWARDED_PORT_HEADER
|
|
14787
|
+
]);
|
|
14788
|
+
var AUTH_REJECTION_STATUS = /* @__PURE__ */ new Set([401, 403]);
|
|
14789
|
+
function jsonError(message, status) {
|
|
14790
|
+
return Response.json({ error: message }, { status });
|
|
14791
|
+
}
|
|
14792
|
+
function normalizeHost(value) {
|
|
14793
|
+
const trimmed = value.trim().toLowerCase();
|
|
14794
|
+
if (!trimmed || trimmed.includes("/") || trimmed.includes("\\") || trimmed.includes(":")) {
|
|
14795
|
+
return void 0;
|
|
14796
|
+
}
|
|
14797
|
+
return trimmed.replace(/\.$/, "");
|
|
14798
|
+
}
|
|
14799
|
+
function normalizeScheme(value) {
|
|
14800
|
+
return value.trim().toLowerCase() === "https" ? "https" : void 0;
|
|
14801
|
+
}
|
|
14802
|
+
function normalizePort(value) {
|
|
14803
|
+
if (!value) {
|
|
14804
|
+
return void 0;
|
|
14805
|
+
}
|
|
14806
|
+
const trimmed = value.trim();
|
|
14807
|
+
if (!/^\d{1,5}$/.test(trimmed)) {
|
|
14808
|
+
return void 0;
|
|
14809
|
+
}
|
|
14810
|
+
const port = Number.parseInt(trimmed, 10);
|
|
14811
|
+
return port >= 1 && port <= 65535 ? trimmed : void 0;
|
|
14812
|
+
}
|
|
14813
|
+
function upstreamPath(request, sandboxId) {
|
|
14814
|
+
const url = new URL(request.url);
|
|
14815
|
+
const prefix = `${ROUTE_PREFIX}/${encodeURIComponent(sandboxId)}`;
|
|
14816
|
+
if (url.pathname === prefix) {
|
|
14817
|
+
return `/${url.search}`;
|
|
14818
|
+
}
|
|
14819
|
+
if (url.pathname.startsWith(`${prefix}/`)) {
|
|
14820
|
+
return `${url.pathname.slice(prefix.length)}${url.search}`;
|
|
14821
|
+
}
|
|
14822
|
+
return void 0;
|
|
14823
|
+
}
|
|
14824
|
+
function buildUpstreamUrl(request, sandboxId) {
|
|
14825
|
+
const forwardedHost = request.headers.get(FORWARDED_HOST_HEADER);
|
|
14826
|
+
if (!forwardedHost?.trim()) {
|
|
14827
|
+
return { ok: false, error: "Missing forwarded host" };
|
|
14828
|
+
}
|
|
14829
|
+
const host = normalizeHost(forwardedHost);
|
|
14830
|
+
if (!host) {
|
|
14831
|
+
return { ok: false, error: "Invalid forwarded host" };
|
|
14832
|
+
}
|
|
14833
|
+
const forwardedScheme = request.headers.get(FORWARDED_SCHEME_HEADER);
|
|
14834
|
+
if (!forwardedScheme?.trim()) {
|
|
14835
|
+
return { ok: false, error: "Missing forwarded scheme" };
|
|
14836
|
+
}
|
|
14837
|
+
const scheme = normalizeScheme(forwardedScheme);
|
|
14838
|
+
if (!scheme) {
|
|
14839
|
+
return { ok: false, error: "Forwarded scheme must be https" };
|
|
14840
|
+
}
|
|
14841
|
+
const forwardedPort = request.headers.get(FORWARDED_PORT_HEADER);
|
|
14842
|
+
const port = normalizePort(forwardedPort);
|
|
14843
|
+
if (forwardedPort && !port) {
|
|
14844
|
+
return { ok: false, error: "Invalid forwarded port" };
|
|
14845
|
+
}
|
|
14846
|
+
const path11 = upstreamPath(request, sandboxId);
|
|
14847
|
+
if (!path11) {
|
|
14848
|
+
return { ok: false, error: "Invalid egress route" };
|
|
14849
|
+
}
|
|
14850
|
+
try {
|
|
14851
|
+
const url = new URL(`${scheme}://${host}${port ? `:${port}` : ""}${path11}`);
|
|
14852
|
+
return { ok: true, url };
|
|
14853
|
+
} catch {
|
|
14854
|
+
return { ok: false, error: "Invalid forwarded URL" };
|
|
14855
|
+
}
|
|
14856
|
+
}
|
|
14857
|
+
async function requestBodyBytes(request) {
|
|
14858
|
+
if (request.method === "GET" || request.method === "HEAD" || request.body === null) {
|
|
14859
|
+
return void 0;
|
|
14860
|
+
}
|
|
14861
|
+
return await request.arrayBuffer();
|
|
14862
|
+
}
|
|
14863
|
+
function requestHeaders(request, lease, upstreamHost) {
|
|
14864
|
+
const headers = new Headers();
|
|
14865
|
+
request.headers.forEach((value, key) => {
|
|
14866
|
+
const normalized = key.toLowerCase();
|
|
14867
|
+
if (HOP_BY_HOP_HEADERS.has(normalized) || PROXY_ONLY_HEADERS.has(normalized)) {
|
|
14868
|
+
return;
|
|
14869
|
+
}
|
|
14870
|
+
headers.append(key, value);
|
|
14871
|
+
});
|
|
14872
|
+
for (const transform of lease.headerTransforms) {
|
|
14873
|
+
if (!matchesSandboxEgressDomain(upstreamHost, transform.domain)) {
|
|
14874
|
+
continue;
|
|
14875
|
+
}
|
|
14876
|
+
for (const [key, value] of Object.entries(transform.headers)) {
|
|
14877
|
+
headers.set(key, value);
|
|
14878
|
+
}
|
|
14879
|
+
}
|
|
14880
|
+
return headers;
|
|
14881
|
+
}
|
|
14882
|
+
function responseHeaders(upstream) {
|
|
14883
|
+
const headers = new Headers();
|
|
14884
|
+
upstream.headers.forEach((value, key) => {
|
|
14885
|
+
const normalized = key.toLowerCase();
|
|
14886
|
+
if (!HOP_BY_HOP_HEADERS.has(normalized)) {
|
|
14887
|
+
headers.append(key, value);
|
|
14888
|
+
}
|
|
14889
|
+
});
|
|
14890
|
+
return headers;
|
|
14891
|
+
}
|
|
14892
|
+
async function credentialLease(sandboxId, provider, session) {
|
|
14893
|
+
const cached = await getSandboxEgressCredentialLease(
|
|
14894
|
+
sandboxId,
|
|
14895
|
+
provider,
|
|
14896
|
+
session
|
|
14897
|
+
);
|
|
14898
|
+
if (cached) {
|
|
14899
|
+
return cached;
|
|
14900
|
+
}
|
|
14901
|
+
const lease = await issueProviderCredentialLease({
|
|
14902
|
+
provider,
|
|
14903
|
+
requesterId: session.requesterId,
|
|
14904
|
+
reason: `sandbox-egress:${provider}`
|
|
14905
|
+
});
|
|
14906
|
+
const headerTransforms = lease.headerTransforms ?? [];
|
|
14907
|
+
if (headerTransforms.length === 0) {
|
|
14908
|
+
throw new Error(
|
|
14909
|
+
`Credential lease for ${provider} did not include header transforms`
|
|
14910
|
+
);
|
|
14911
|
+
}
|
|
14912
|
+
const cachedLease = {
|
|
14913
|
+
provider,
|
|
14914
|
+
expiresAt: lease.expiresAt,
|
|
14915
|
+
headerTransforms
|
|
14916
|
+
};
|
|
14917
|
+
await setSandboxEgressCredentialLease(sandboxId, session, cachedLease);
|
|
14918
|
+
return cachedLease;
|
|
14919
|
+
}
|
|
14920
|
+
function hasTransformForHost(lease, host) {
|
|
14921
|
+
return lease.headerTransforms.some(
|
|
14922
|
+
(transform) => matchesSandboxEgressDomain(host, transform.domain)
|
|
14923
|
+
);
|
|
14924
|
+
}
|
|
14925
|
+
async function proxySandboxEgressRequest(request, sandboxId, deps = {}) {
|
|
14926
|
+
const oidcToken = request.headers.get(OIDC_TOKEN_HEADER)?.trim();
|
|
14927
|
+
if (!oidcToken) {
|
|
14928
|
+
return jsonError("Missing Vercel Sandbox OIDC token", 401);
|
|
14929
|
+
}
|
|
14930
|
+
try {
|
|
14931
|
+
await (deps.verifyOidc ?? verifyVercelSandboxOidcToken)(
|
|
14932
|
+
oidcToken,
|
|
14933
|
+
sandboxId
|
|
14934
|
+
);
|
|
14935
|
+
} catch (error) {
|
|
14936
|
+
logWarn(
|
|
14937
|
+
"sandbox_egress_oidc_verification_failed",
|
|
14938
|
+
{},
|
|
14939
|
+
{
|
|
14940
|
+
"app.sandbox.oidc_error": error instanceof Error ? error.message : String(error)
|
|
14941
|
+
},
|
|
14942
|
+
"Sandbox egress OIDC verification failed"
|
|
14943
|
+
);
|
|
14944
|
+
return jsonError("Invalid Vercel Sandbox OIDC token", 401);
|
|
14945
|
+
}
|
|
14946
|
+
const upstreamResult = buildUpstreamUrl(request, sandboxId);
|
|
14947
|
+
if (!upstreamResult.ok) {
|
|
14948
|
+
return jsonError(upstreamResult.error, 400);
|
|
14949
|
+
}
|
|
14950
|
+
const upstreamUrl = upstreamResult.url;
|
|
14951
|
+
const provider = resolveSandboxEgressProviderForHost(upstreamUrl.hostname);
|
|
14952
|
+
if (!provider) {
|
|
14953
|
+
return jsonError("No provider owns forwarded host", 403);
|
|
14954
|
+
}
|
|
14955
|
+
const session = await getSandboxEgressSession(sandboxId);
|
|
14956
|
+
if (!session) {
|
|
14957
|
+
return jsonError("Sandbox egress session is not authorized", 403);
|
|
14958
|
+
}
|
|
14959
|
+
let lease;
|
|
14960
|
+
try {
|
|
14961
|
+
lease = await credentialLease(sandboxId, provider, session);
|
|
14962
|
+
} catch (error) {
|
|
14963
|
+
if (error instanceof CredentialUnavailableError) {
|
|
14964
|
+
return new Response(
|
|
14965
|
+
`junior-auth-required provider=${error.provider} 401 unauthorized
|
|
14966
|
+
${error.message}`,
|
|
14967
|
+
{
|
|
14968
|
+
status: 401,
|
|
14969
|
+
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
14970
|
+
}
|
|
14971
|
+
);
|
|
14972
|
+
}
|
|
14973
|
+
throw error;
|
|
14974
|
+
}
|
|
14975
|
+
if (!hasTransformForHost(lease, upstreamUrl.hostname)) {
|
|
14976
|
+
return jsonError("Credential lease does not cover forwarded host", 403);
|
|
14977
|
+
}
|
|
14978
|
+
const body = await requestBodyBytes(request);
|
|
14979
|
+
const upstream = await (deps.fetch ?? fetch)(upstreamUrl, {
|
|
14980
|
+
method: request.method,
|
|
14981
|
+
headers: requestHeaders(request, lease, upstreamUrl.hostname),
|
|
14982
|
+
...body ? { body } : {},
|
|
14983
|
+
redirect: "manual"
|
|
14984
|
+
});
|
|
14985
|
+
if (AUTH_REJECTION_STATUS.has(upstream.status)) {
|
|
14986
|
+
await clearSandboxEgressCredentialLease(sandboxId, provider, session);
|
|
14987
|
+
}
|
|
14988
|
+
return new Response(upstream.body, {
|
|
14989
|
+
status: upstream.status,
|
|
14990
|
+
statusText: upstream.statusText,
|
|
14991
|
+
headers: responseHeaders(upstream)
|
|
14992
|
+
});
|
|
14993
|
+
}
|
|
14994
|
+
|
|
14995
|
+
// src/handlers/sandbox-egress-proxy.ts
|
|
14996
|
+
async function ALL(request, sandboxId) {
|
|
14997
|
+
return await proxySandboxEgressRequest(request, sandboxId);
|
|
14998
|
+
}
|
|
14999
|
+
|
|
14371
15000
|
// src/chat/slack/context.ts
|
|
14372
15001
|
function toTrimmedSlackString(value) {
|
|
14373
15002
|
const normalized = toOptionalString(value);
|
|
@@ -17744,6 +18373,12 @@ async function createApp(options) {
|
|
|
17744
18373
|
app.post("/api/internal/turn-resume", (c) => {
|
|
17745
18374
|
return POST(c.req.raw, waitUntil);
|
|
17746
18375
|
});
|
|
18376
|
+
app.all("/api/internal/sandbox-egress/:sandboxId", (c) => {
|
|
18377
|
+
return ALL(c.req.raw, c.req.param("sandboxId"));
|
|
18378
|
+
});
|
|
18379
|
+
app.all("/api/internal/sandbox-egress/:sandboxId/*", (c) => {
|
|
18380
|
+
return ALL(c.req.raw, c.req.param("sandboxId"));
|
|
18381
|
+
});
|
|
17747
18382
|
app.post("/api/webhooks/:platform", (c) => {
|
|
17748
18383
|
return POST2(c.req.raw, c.req.param("platform"), waitUntil);
|
|
17749
18384
|
});
|