@sentry/junior 0.41.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 CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  findSkillByName,
4
4
  loadSkillsByName,
5
5
  parseSkillInvocation
6
- } from "./chunk-7QYONRLH.js";
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-Y3UO7NR6.js";
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-SCE5C645.js";
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 { apiDomains, apiHeaders } = manifest;
3427
- if (!apiDomains || !apiHeaders) {
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 apiDomains.map((domain) => ({ domain, headers }));
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 createSkillCapabilityRuntime(options = {}) {
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 { apiHeaders, credentials, name } = plugin.manifest;
3447
- if (!credentials && !apiHeaders) {
3331
+ const { name } = plugin.manifest;
3332
+ if (!plugin.manifest.credentials && !plugin.manifest.apiHeaders) {
3448
3333
  continue;
3449
3334
  }
3450
- if (!credentials) {
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
- const router = new ProviderCredentialRouter({ brokersByProvider });
3472
- return new SkillCapabilityRuntime({
3473
- router,
3474
- requesterId: options.requesterId
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
@@ -7103,9 +6979,86 @@ function parseSlackMessageReference(input) {
7103
6979
  };
7104
6980
  }
7105
6981
 
6982
+ // src/chat/slack/legacy-attachments.ts
6983
+ var MAX_ATTACHMENTS = 10;
6984
+ var MAX_FIELDS_PER_ATTACHMENT = 20;
6985
+ var MAX_FIELD_CHARS = 1e3;
6986
+ var MAX_ATTACHMENT_TEXT_CHARS = 4e3;
6987
+ function toStr(value) {
6988
+ return typeof value === "string" && value.length > 0 ? value : void 0;
6989
+ }
6990
+ function getAttachmentPayload(input) {
6991
+ if (Array.isArray(input)) return input;
6992
+ if (!input || typeof input !== "object") return [];
6993
+ const attachments = input.attachments;
6994
+ return Array.isArray(attachments) ? attachments : [];
6995
+ }
6996
+ function renderField(raw) {
6997
+ if (!raw || typeof raw !== "object") return void 0;
6998
+ const obj = raw;
6999
+ const title = toStr(obj.title);
7000
+ const value = toStr(obj.value)?.slice(0, MAX_FIELD_CHARS);
7001
+ if (title && value) return `${title}: ${value}`;
7002
+ return title || value;
7003
+ }
7004
+ function renderAttachment(raw) {
7005
+ if (!raw || typeof raw !== "object") return "";
7006
+ const obj = raw;
7007
+ const parts = [];
7008
+ const seen = /* @__PURE__ */ new Set();
7009
+ const fallback = toStr(obj.fallback);
7010
+ const pretext = toStr(obj.pretext);
7011
+ const authorName = toStr(obj.author_name);
7012
+ const title = toStr(obj.title);
7013
+ const titleLink = toStr(obj.title_link);
7014
+ const text = toStr(obj.text);
7015
+ const footer = toStr(obj.footer);
7016
+ const fields = Array.isArray(obj.fields) ? obj.fields : [];
7017
+ const add = (value) => {
7018
+ if (!value) return;
7019
+ const normalized = value.trim();
7020
+ if (!normalized || seen.has(normalized)) return;
7021
+ seen.add(normalized);
7022
+ parts.push(normalized);
7023
+ };
7024
+ const hasRichContent = pretext || title || text;
7025
+ if (!hasRichContent) {
7026
+ add(fallback);
7027
+ }
7028
+ add(pretext);
7029
+ add(authorName);
7030
+ if (title && titleLink) {
7031
+ add(`${title} (${titleLink})`);
7032
+ seen.add(title.trim());
7033
+ } else {
7034
+ add(title);
7035
+ }
7036
+ add(text);
7037
+ for (const field of fields.slice(0, MAX_FIELDS_PER_ATTACHMENT)) {
7038
+ add(renderField(field));
7039
+ }
7040
+ add(footer);
7041
+ return parts.join(" | ");
7042
+ }
7043
+ function renderSlackLegacyAttachmentText(input) {
7044
+ const rendered = getAttachmentPayload(input).slice(0, MAX_ATTACHMENTS).map(renderAttachment).filter((line) => line.length > 0).map((line) => `[attachment] ${line}`).join("\n");
7045
+ return rendered.slice(0, MAX_ATTACHMENT_TEXT_CHARS);
7046
+ }
7047
+ function appendSlackLegacyAttachmentText(baseText, rawMessageOrAttachments) {
7048
+ const base = baseText?.trim() ?? "";
7049
+ const attachmentText = renderSlackLegacyAttachmentText(
7050
+ rawMessageOrAttachments
7051
+ );
7052
+ if (!attachmentText) return base;
7053
+ if (!base) return attachmentText;
7054
+ return `${base}
7055
+ ${attachmentText}`;
7056
+ }
7057
+
7106
7058
  // src/chat/tools/slack/thread-read.ts
7107
7059
  var MAX_THREAD_READ_CHARS = 4e4;
7108
7060
  function sanitizeMessage(msg) {
7061
+ const attachmentText = renderSlackLegacyAttachmentText(msg.attachments);
7109
7062
  return {
7110
7063
  ts: msg.ts,
7111
7064
  user: msg.user,
@@ -7114,6 +7067,7 @@ function sanitizeMessage(msg) {
7114
7067
  subtype: msg.subtype,
7115
7068
  bot_id: msg.bot_id,
7116
7069
  type: msg.type,
7070
+ ...attachmentText ? { attachment_text: attachmentText } : {},
7117
7071
  ...msg.files?.length ? {
7118
7072
  files: msg.files.map((f) => ({
7119
7073
  id: f.id,
@@ -7128,7 +7082,7 @@ function truncateMessages(messages, maxChars) {
7128
7082
  let chars = 0;
7129
7083
  const kept = [];
7130
7084
  for (const msg of messages) {
7131
- const textLen = msg.text?.length ?? 0;
7085
+ const textLen = (msg.text?.length ?? 0) + (msg.attachment_text?.length ?? 0);
7132
7086
  if (kept.length > 0 && chars + textLen > maxChars) {
7133
7087
  break;
7134
7088
  }
@@ -7276,13 +7230,272 @@ function createSlackThreadReadTool(context) {
7276
7230
  });
7277
7231
  }
7278
7232
 
7279
- // src/chat/tools/system-time.ts
7233
+ // src/chat/tools/slack/user-lookup.ts
7280
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";
7281
7494
  function createSystemTimeTool() {
7282
7495
  return tool({
7283
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.",
7284
7497
  annotations: { readOnlyHint: true, destructiveHint: false },
7285
- inputSchema: Type19.Object({}),
7498
+ inputSchema: Type20.Object({}),
7286
7499
  execute: async () => {
7287
7500
  const now = /* @__PURE__ */ new Date();
7288
7501
  return {
@@ -7300,7 +7513,7 @@ function createSystemTimeTool() {
7300
7513
  import {
7301
7514
  Agent
7302
7515
  } from "@mariozechner/pi-agent-core";
7303
- import { Type as Type20 } from "@sinclair/typebox";
7516
+ import { Type as Type21 } from "@sinclair/typebox";
7304
7517
 
7305
7518
  // src/chat/respond-helpers.ts
7306
7519
  var MAX_INLINE_ATTACHMENT_BASE64_CHARS = 12e4;
@@ -7588,12 +7801,12 @@ function createAdvisorTool(context) {
7588
7801
  const spanContext = context.logContext ?? {};
7589
7802
  return tool({
7590
7803
  description: ADVISOR_TOOL_DESCRIPTION,
7591
- inputSchema: Type20.Object({
7592
- question: Type20.String({
7804
+ inputSchema: Type21.Object({
7805
+ question: Type21.String({
7593
7806
  minLength: 1,
7594
7807
  description: "Focused advisor question or decision point."
7595
7808
  }),
7596
- context: Type20.String({
7809
+ context: Type21.String({
7597
7810
  minLength: 1,
7598
7811
  description: "Curated evidence packet: relevant requirements, constraints, current plan, alternatives, code snippets, diffs, command output, and open questions."
7599
7812
  })
@@ -7705,7 +7918,7 @@ function createAdvisorTool(context) {
7705
7918
  }
7706
7919
 
7707
7920
  // src/chat/tools/web/fetch-tool.ts
7708
- import { Type as Type21 } from "@sinclair/typebox";
7921
+ import { Type as Type22 } from "@sinclair/typebox";
7709
7922
 
7710
7923
  // src/chat/tools/web/constants.ts
7711
7924
  var USER_AGENT = "junior-bot/0.1";
@@ -8058,13 +8271,13 @@ function createWebFetchTool(hooks) {
8058
8271
  destructiveHint: false,
8059
8272
  openWorldHint: true
8060
8273
  },
8061
- inputSchema: Type21.Object({
8062
- url: Type21.String({
8274
+ inputSchema: Type22.Object({
8275
+ url: Type22.String({
8063
8276
  minLength: 1,
8064
8277
  description: "HTTP(S) URL to fetch."
8065
8278
  }),
8066
- max_chars: Type21.Optional(
8067
- Type21.Integer({
8279
+ max_chars: Type22.Optional(
8280
+ Type22.Integer({
8068
8281
  minimum: 500,
8069
8282
  maximum: MAX_FETCH_CHARS,
8070
8283
  description: "Optional maximum number of extracted characters to return."
@@ -8124,7 +8337,7 @@ function createWebFetchTool(hooks) {
8124
8337
  // src/chat/tools/web/search.ts
8125
8338
  import { generateText } from "ai";
8126
8339
  import { createGatewayProvider } from "@ai-sdk/gateway";
8127
- import { Type as Type22 } from "@sinclair/typebox";
8340
+ import { Type as Type23 } from "@sinclair/typebox";
8128
8341
  var SEARCH_TIMEOUT_MS = 6e4;
8129
8342
  var MAX_RESULTS2 = 5;
8130
8343
  var DEFAULT_SEARCH_MODEL = "xai/grok-4-fast-reasoning";
@@ -8172,14 +8385,14 @@ function createWebSearchTool() {
8172
8385
  destructiveHint: false,
8173
8386
  openWorldHint: true
8174
8387
  },
8175
- inputSchema: Type22.Object({
8176
- query: Type22.String({
8388
+ inputSchema: Type23.Object({
8389
+ query: Type23.String({
8177
8390
  minLength: 1,
8178
8391
  maxLength: 500,
8179
8392
  description: "Search query."
8180
8393
  }),
8181
- max_results: Type22.Optional(
8182
- Type22.Integer({
8394
+ max_results: Type23.Optional(
8395
+ Type23.Integer({
8183
8396
  minimum: 1,
8184
8397
  maximum: MAX_RESULTS2,
8185
8398
  description: "Max results to return."
@@ -8248,20 +8461,20 @@ function createWebSearchTool() {
8248
8461
  }
8249
8462
 
8250
8463
  // src/chat/tools/sandbox/write-file.ts
8251
- import { Type as Type23 } from "@sinclair/typebox";
8464
+ import { Type as Type24 } from "@sinclair/typebox";
8252
8465
  function createWriteFileTool() {
8253
8466
  return tool({
8254
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.",
8255
8468
  promptSnippet: "new file or deliberate full-file replacement",
8256
8469
  promptGuidelines: ["targeted existing-file changes: editFile"],
8257
8470
  executionMode: "sequential",
8258
- inputSchema: Type23.Object(
8471
+ inputSchema: Type24.Object(
8259
8472
  {
8260
- path: Type23.String({
8473
+ path: Type24.String({
8261
8474
  minLength: 1,
8262
8475
  description: "Path to write in the sandbox workspace."
8263
8476
  }),
8264
- content: Type23.String({
8477
+ content: Type24.String({
8265
8478
  description: "UTF-8 file content to write."
8266
8479
  })
8267
8480
  },
@@ -8335,6 +8548,7 @@ function createTools(availableSkills, hooks = {}, context) {
8335
8548
  slackCanvasRead: createSlackCanvasReadTool(),
8336
8549
  slackCanvasUpdate: createSlackCanvasUpdateTool(state, context),
8337
8550
  slackThreadRead: createSlackThreadReadTool(context),
8551
+ slackUserLookup: createSlackUserLookupTool(),
8338
8552
  slackListCreate: createSlackListCreateTool(state),
8339
8553
  slackListAddItems: createSlackListAddItemsTool(state),
8340
8554
  slackListGetItems: createSlackListGetItemsTool(state),
@@ -8385,6 +8599,173 @@ function resolveChannelCapabilities(channelId) {
8385
8599
  // src/chat/sandbox/sandbox.ts
8386
8600
  import fs4 from "fs/promises";
8387
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
+
8388
8769
  // src/chat/sandbox/http-error-details.ts
8389
8770
  var DEFAULT_PREVIEW_LIMIT = 512;
8390
8771
  function toTrimmedString(value, maxChars) {
@@ -8587,6 +8968,7 @@ function throwSandboxOperationError(action, error, includeMissingPath = false) {
8587
8968
  }
8588
8969
 
8589
8970
  // src/chat/sandbox/session.ts
8971
+ import { randomUUID as randomUUID4 } from "crypto";
8590
8972
  import { Sandbox } from "@vercel/sandbox";
8591
8973
  import { createBashTool as createBashTool2 } from "bash-tool";
8592
8974
 
@@ -9160,27 +9542,39 @@ var SANDBOX_RUNTIME = "node22";
9160
9542
  var SANDBOX_RUNTIME_BIN_DIR = `${SANDBOX_WORKSPACE_ROOT}/.junior/bin`;
9161
9543
  var SNAPSHOT_BOOT_RETRY_COUNT = 3;
9162
9544
  var SNAPSHOT_BOOT_RETRY_DELAY_MS = 1e3;
9163
- function mergeNetworkPolicyWithHeaderTransforms(networkPolicy, headerTransforms) {
9164
- const basePolicy = networkPolicy && typeof networkPolicy === "object" && !Array.isArray(networkPolicy) ? { ...networkPolicy } : {};
9165
- const existingAllowRaw = basePolicy.allow;
9166
- const existingAllow = existingAllowRaw && typeof existingAllowRaw === "object" && !Array.isArray(existingAllowRaw) ? Object.fromEntries(
9167
- Object.entries(existingAllowRaw).map(
9168
- ([domain, rules]) => [
9169
- domain,
9170
- Array.isArray(rules) ? [...rules] : []
9171
- ]
9172
- )
9173
- ) : { "*": [] };
9174
- for (const transform of headerTransforms) {
9175
- const currentRules = existingAllow[transform.domain] ?? [];
9176
- existingAllow[transform.domain] = [
9177
- ...currentRules,
9178
- { transform: [{ headers: transform.headers }] }
9179
- ];
9180
- }
9545
+ var SANDBOX_NAME_PREFIX = "junior-";
9546
+ function createBashToolSandboxAdapter(sandbox) {
9181
9547
  return {
9182
- ...basePolicy,
9183
- allow: existingAllow
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
+ }
9184
9578
  };
9185
9579
  }
9186
9580
  function truncateOutput(output, maxLength) {
@@ -9213,23 +9607,36 @@ function createSandboxSessionManager(options) {
9213
9607
  let availableSkills = [];
9214
9608
  let availableReferenceFiles = [];
9215
9609
  let toolExecutors;
9610
+ let appliedNetworkPolicyKey;
9216
9611
  const timeoutMs = options?.timeoutMs ?? 1e3 * 60 * 30;
9217
9612
  const traceContext = options?.traceContext ?? {};
9218
9613
  const dependencyProfileHash = getRuntimeDependencyProfileHash(SANDBOX_RUNTIME);
9614
+ const resolveCommandEnv = options?.commandEnv ?? (async () => ({}));
9219
9615
  const withSandboxSpan = (name, op, attributes, callback) => withSpan(name, op, traceContext, callback, attributes);
9220
9616
  const clearSession = () => {
9221
9617
  sandbox = null;
9222
9618
  sandboxIdHint = void 0;
9223
9619
  toolExecutors = void 0;
9620
+ appliedNetworkPolicyKey = void 0;
9621
+ };
9622
+ const createSandboxName = () => `${SANDBOX_NAME_PREFIX}${randomUUID4()}`;
9623
+ const rememberNetworkPolicy = (networkPolicy) => {
9624
+ appliedNetworkPolicyKey = networkPolicy ? JSON.stringify(networkPolicy) : void 0;
9224
9625
  };
9225
- const rememberSandbox = async (nextSandbox) => {
9626
+ const rememberSandbox = async (nextSandbox, rememberOptions) => {
9226
9627
  sandbox = nextSandbox;
9227
9628
  sandboxIdHint = nextSandbox.sandboxId;
9228
9629
  toolExecutors = void 0;
9229
- await options?.onSandboxAcquired?.({
9230
- sandboxId: nextSandbox.sandboxId,
9630
+ const acquired = {
9631
+ sandboxId: sandboxIdHint,
9231
9632
  ...dependencyProfileHash ? { sandboxDependencyProfileHash: dependencyProfileHash } : {}
9232
- });
9633
+ };
9634
+ await options?.onSandboxAcquired?.(acquired);
9635
+ if (rememberOptions?.recordNetworkPolicy) {
9636
+ rememberNetworkPolicy(
9637
+ options?.createNetworkPolicy?.(nextSandbox.sandboxId)
9638
+ );
9639
+ }
9233
9640
  return nextSandbox;
9234
9641
  };
9235
9642
  const failSetup = (error) => {
@@ -9244,6 +9651,30 @@ function createSandboxSessionManager(options) {
9244
9651
  runtimeBinDir: SANDBOX_RUNTIME_BIN_DIR
9245
9652
  });
9246
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
+ };
9247
9678
  const ensureSandboxReachable = async (targetSandbox, source) => {
9248
9679
  await withSandboxSpan(
9249
9680
  "sandbox.reuse_probe",
@@ -9263,23 +9694,6 @@ function createSandboxSessionManager(options) {
9263
9694
  }
9264
9695
  );
9265
9696
  };
9266
- const invalidateSandboxInstance = async (targetSandbox, reason) => {
9267
- if (sandbox === targetSandbox) {
9268
- clearSession();
9269
- }
9270
- logWarn(
9271
- "sandbox_network_policy_restore_failed",
9272
- traceContext,
9273
- {
9274
- "exception.message": reason instanceof Error ? reason.message : String(reason)
9275
- },
9276
- "Sandbox network policy restore failed; discarding sandbox instance"
9277
- );
9278
- try {
9279
- await targetSandbox.stop({ blocking: true });
9280
- } catch {
9281
- }
9282
- };
9283
9697
  const recreateUnavailableSandbox = async (source) => {
9284
9698
  setSpanAttributes({
9285
9699
  "app.sandbox.recovery.attempted": true,
@@ -9292,17 +9706,22 @@ function createSandboxSessionManager(options) {
9292
9706
  });
9293
9707
  return replacement;
9294
9708
  };
9295
- const createSandboxFromSnapshot = async (snapshotId, sandboxCredentials) => {
9709
+ const createSandboxFromSnapshot = async (snapshotId, sandboxCredentials, initialSandboxName) => {
9296
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);
9297
9713
  try {
9298
- return await Sandbox.create({
9299
- timeout: timeoutMs,
9300
- source: {
9301
- type: "snapshot",
9302
- snapshotId
9303
- },
9304
- ...sandboxCredentials ?? {}
9305
- });
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
+ );
9306
9725
  } catch (error) {
9307
9726
  if (!isSnapshottingError(error) || attempt === SNAPSHOT_BOOT_RETRY_COUNT - 1) {
9308
9727
  throw error;
@@ -9327,18 +9746,23 @@ function createSandboxSessionManager(options) {
9327
9746
  });
9328
9747
  };
9329
9748
  const createSandboxFromResolvedSnapshot = async (params) => {
9330
- const { runtime, snapshot, sandboxCredentials } = params;
9749
+ const { runtime, snapshot, sandboxCredentials, sandboxName } = params;
9331
9750
  if (!snapshot.snapshotId) {
9332
- return await Sandbox.create({
9333
- timeout: timeoutMs,
9334
- runtime,
9335
- ...sandboxCredentials ?? {}
9336
- });
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
+ );
9337
9760
  }
9338
9761
  try {
9339
9762
  return await createSandboxFromSnapshot(
9340
9763
  snapshot.snapshotId,
9341
- sandboxCredentials
9764
+ sandboxCredentials,
9765
+ sandboxName
9342
9766
  );
9343
9767
  } catch (error) {
9344
9768
  if (!isSnapshotMissingError(error)) {
@@ -9358,13 +9782,15 @@ function createSandboxSessionManager(options) {
9358
9782
  }
9359
9783
  return await createSandboxFromSnapshot(
9360
9784
  rebuiltSnapshot.snapshotId,
9361
- sandboxCredentials
9785
+ sandboxCredentials,
9786
+ sandboxName
9362
9787
  );
9363
9788
  }
9364
9789
  };
9365
9790
  const createFreshSandbox = async () => {
9366
9791
  const runtime = SANDBOX_RUNTIME;
9367
9792
  const sandboxCredentials = getVercelSandboxCredentials();
9793
+ const sandboxName = createSandboxName();
9368
9794
  let createdSandbox;
9369
9795
  try {
9370
9796
  createdSandbox = await withSandboxSpan(
@@ -9384,7 +9810,8 @@ function createSandboxSessionManager(options) {
9384
9810
  return await createSandboxFromResolvedSnapshot({
9385
9811
  runtime,
9386
9812
  snapshot,
9387
- sandboxCredentials
9813
+ sandboxCredentials,
9814
+ sandboxName
9388
9815
  });
9389
9816
  }
9390
9817
  );
@@ -9396,7 +9823,7 @@ function createSandboxSessionManager(options) {
9396
9823
  } catch (error) {
9397
9824
  return failSetup(error);
9398
9825
  }
9399
- return await rememberSandbox(createdSandbox);
9826
+ return await rememberSandbox(createdSandbox, { recordNetworkPolicy: true });
9400
9827
  };
9401
9828
  const discardHintIfProfileChanged = () => {
9402
9829
  if (sandbox || !sandboxIdHint || dependencyProfileHash === options?.sandboxDependencyProfileHash) {
@@ -9419,6 +9846,7 @@ function createSandboxSessionManager(options) {
9419
9846
  }
9420
9847
  try {
9421
9848
  await ensureSandboxReachable(cachedSandbox, "memory");
9849
+ await refreshNetworkPolicy(cachedSandbox);
9422
9850
  return cachedSandbox;
9423
9851
  } catch (error) {
9424
9852
  if (isSandboxUnavailableError(error)) {
@@ -9441,15 +9869,19 @@ function createSandboxSessionManager(options) {
9441
9869
  "app.sandbox.reused": true,
9442
9870
  "app.sandbox.source": "id_hint"
9443
9871
  },
9444
- async () => await Sandbox.get({
9445
- sandboxId: sandboxIdHint,
9446
- ...sandboxCredentials ?? {}
9447
- })
9872
+ async () => createSandboxInstance(
9873
+ await Sandbox.get({
9874
+ name: sandboxIdHint,
9875
+ resume: true,
9876
+ ...sandboxCredentials ?? {}
9877
+ })
9878
+ )
9448
9879
  );
9449
9880
  } catch {
9450
9881
  return null;
9451
9882
  }
9452
9883
  try {
9884
+ await refreshNetworkPolicy(hintedSandbox);
9453
9885
  await syncSkills(hintedSandbox);
9454
9886
  return await rememberSandbox(hintedSandbox);
9455
9887
  } catch (error) {
@@ -9504,37 +9936,6 @@ function createSandboxSessionManager(options) {
9504
9936
  stderrTruncated: stderr.truncated
9505
9937
  };
9506
9938
  };
9507
- const withTemporaryHeaderTransforms = async (sandboxInstance, headerTransforms, callback) => {
9508
- if (!headerTransforms || headerTransforms.length === 0) {
9509
- return await callback();
9510
- }
9511
- const restoreNetworkPolicy = sandboxInstance.networkPolicy ?? "allow-all";
9512
- const policy = mergeNetworkPolicyWithHeaderTransforms(
9513
- restoreNetworkPolicy,
9514
- headerTransforms
9515
- );
9516
- await sandboxInstance.updateNetworkPolicy(policy);
9517
- let callbackError;
9518
- let restoreError;
9519
- let result;
9520
- try {
9521
- result = await callback();
9522
- } catch (error) {
9523
- callbackError = error;
9524
- throw error;
9525
- } finally {
9526
- try {
9527
- await sandboxInstance.updateNetworkPolicy(restoreNetworkPolicy);
9528
- } catch (error) {
9529
- restoreError = error;
9530
- await invalidateSandboxInstance(sandboxInstance, error);
9531
- }
9532
- }
9533
- if (restoreError && !callbackError) {
9534
- throw restoreError;
9535
- }
9536
- return result;
9537
- };
9538
9939
  const extendKeepAlive = async (activeSandbox) => {
9539
9940
  const keepAliveMs = parseKeepAliveMs();
9540
9941
  if (keepAliveMs === 0) {
@@ -9555,6 +9956,7 @@ function createSandboxSessionManager(options) {
9555
9956
  }
9556
9957
  };
9557
9958
  const buildToolExecutors = async (sandboxInstance) => {
9959
+ const activeSandboxId = sandboxInstance.sandboxId;
9558
9960
  const toolkit = await withSandboxSpan(
9559
9961
  "sandbox.bash_tool.init",
9560
9962
  "sandbox.tool.init",
@@ -9563,7 +9965,7 @@ function createSandboxSessionManager(options) {
9563
9965
  "app.sandbox.destination": SANDBOX_WORKSPACE_ROOT
9564
9966
  },
9565
9967
  async () => await createBashTool2({
9566
- sandbox: sandboxInstance,
9968
+ sandbox: createBashToolSandboxAdapter(sandboxInstance),
9567
9969
  destination: SANDBOX_WORKSPACE_ROOT
9568
9970
  })
9569
9971
  );
@@ -9574,47 +9976,69 @@ function createSandboxSessionManager(options) {
9574
9976
  }
9575
9977
  return {
9576
9978
  bash: async (input) => {
9577
- const script = buildNonInteractiveShellScript(input.command, {
9578
- env: input.env,
9579
- pathPrefix: `${SANDBOX_RUNTIME_BIN_DIR}:$PATH`
9580
- });
9581
- const controller = input.timeoutMs && input.timeoutMs > 0 ? new AbortController() : void 0;
9979
+ await options?.beforeCommand?.(activeSandboxId);
9582
9980
  let timedOut = false;
9583
- const timeoutId = controller ? setTimeout(() => {
9584
- timedOut = true;
9585
- controller.abort();
9586
- }, input.timeoutMs) : void 0;
9587
- return await withTemporaryHeaderTransforms(
9588
- sandboxInstance,
9589
- input.headerTransforms,
9590
- async () => {
9591
- try {
9592
- const commandResult2 = await sandboxInstance.runCommand({
9593
- cmd: "bash",
9594
- args: ["-c", script],
9595
- cwd: SANDBOX_WORKSPACE_ROOT,
9596
- ...controller ? { signal: controller.signal } : {}
9597
- });
9598
- return await readCommandOutput(commandResult2);
9599
- } catch (error) {
9600
- if (timedOut) {
9601
- return {
9602
- stdout: "",
9603
- stderr: `Command timed out after ${input.timeoutMs}ms`,
9604
- exitCode: 124,
9605
- stdoutTruncated: false,
9606
- stderrTruncated: false,
9607
- timedOut: true
9608
- };
9609
- }
9610
- throw error;
9611
- } finally {
9612
- if (timeoutId) {
9613
- clearTimeout(timeoutId);
9614
- }
9615
- }
9981
+ let timeoutId;
9982
+ let commandFinished = false;
9983
+ const finishCommand = async () => {
9984
+ if (commandFinished) {
9985
+ return;
9616
9986
  }
9617
- );
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
+ }
9618
10042
  },
9619
10043
  readFile: async (input) => await executeReadFile(input, {
9620
10044
  toolCallId: "sandbox-read-file",
@@ -9647,7 +10071,7 @@ function createSandboxSessionManager(options) {
9647
10071
  availableReferenceFiles = [...files];
9648
10072
  },
9649
10073
  getSandboxId() {
9650
- return sandbox?.sandboxId ?? sandboxIdHint;
10074
+ return sandbox ? sandbox.sandboxId : sandboxIdHint;
9651
10075
  },
9652
10076
  getDependencyProfileHash() {
9653
10077
  return dependencyProfileHash;
@@ -9670,7 +10094,7 @@ function createSandboxSessionManager(options) {
9670
10094
  "app.sandbox.stop.blocking": true
9671
10095
  },
9672
10096
  async () => {
9673
- await activeSandbox.stop({ blocking: true });
10097
+ await activeSandbox.stop();
9674
10098
  }
9675
10099
  );
9676
10100
  sandbox = null;
@@ -9689,21 +10113,6 @@ var SANDBOX_TOOL_NAMES = /* @__PURE__ */ new Set([
9689
10113
  "listDir",
9690
10114
  "writeFile"
9691
10115
  ]);
9692
- function parseHeaderTransforms(raw) {
9693
- if (!Array.isArray(raw)) {
9694
- return void 0;
9695
- }
9696
- return raw.filter(
9697
- (value) => Boolean(value && typeof value === "object")
9698
- ).map((transform) => ({
9699
- domain: String(transform.domain ?? "").trim(),
9700
- headers: transform.headers && typeof transform.headers === "object" && !Array.isArray(transform.headers) ? Object.fromEntries(
9701
- Object.entries(transform.headers).filter(([, value]) => typeof value === "string").map(([key, value]) => [key, value])
9702
- ) : {}
9703
- })).filter(
9704
- (transform) => transform.domain.length > 0 && Object.keys(transform.headers).length > 0
9705
- );
9706
- }
9707
10116
  function parseEnv(raw) {
9708
10117
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
9709
10118
  return void 0;
@@ -9712,27 +10121,33 @@ function parseEnv(raw) {
9712
10121
  Object.entries(raw).filter(([, value]) => typeof value === "string").map(([key, value]) => [key, value])
9713
10122
  );
9714
10123
  }
9715
- function createSandboxWorkspace(sandbox) {
9716
- return {
9717
- sandboxId: sandbox.sandboxId,
9718
- readFileToBuffer(input) {
9719
- return sandbox.readFileToBuffer(input);
9720
- },
9721
- runCommand(input) {
9722
- return sandbox.runCommand(input);
9723
- }
9724
- };
9725
- }
9726
10124
  function createSandboxExecutor(options) {
9727
10125
  let availableSkills = [];
9728
10126
  let referenceFiles = [];
9729
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;
9730
10139
  const sessionManager = createSandboxSessionManager({
9731
10140
  sandboxId: options?.sandboxId,
9732
10141
  sandboxDependencyProfileHash: options?.sandboxDependencyProfileHash,
9733
10142
  timeoutMs: options?.timeoutMs,
9734
10143
  traceContext,
9735
- onSandboxAcquired: options?.onSandboxAcquired
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
+ }
9736
10151
  });
9737
10152
  const withSandboxSpan = (name, op, attributes, callback) => withSpan(name, op, traceContext, callback, attributes);
9738
10153
  const logSandboxBootRequest = (trigger, details = {}) => {
@@ -9750,7 +10165,6 @@ function createSandboxExecutor(options) {
9750
10165
  );
9751
10166
  };
9752
10167
  const executeBashTool = async (rawInput, command) => {
9753
- const headerTransforms = parseHeaderTransforms(rawInput.headerTransforms);
9754
10168
  const env = parseEnv(rawInput.env);
9755
10169
  const timeoutMs = positiveInteger(rawInput.timeoutMs);
9756
10170
  logSandboxBootRequest("tool.bash", {
@@ -9767,7 +10181,6 @@ function createSandboxExecutor(options) {
9767
10181
  try {
9768
10182
  const response = await executeBash({
9769
10183
  command,
9770
- ...headerTransforms ? { headerTransforms } : {},
9771
10184
  ...env ? { env } : {},
9772
10185
  ...timeoutMs ? { timeoutMs } : {}
9773
10186
  });
@@ -10071,7 +10484,7 @@ function createSandboxExecutor(options) {
10071
10484
  return SANDBOX_TOOL_NAMES.has(toolName);
10072
10485
  },
10073
10486
  async createSandbox() {
10074
- return createSandboxWorkspace(await sessionManager.createSandbox());
10487
+ return await sessionManager.createSandbox();
10075
10488
  },
10076
10489
  execute,
10077
10490
  async dispose() {
@@ -10172,34 +10585,6 @@ function buildSandboxInput(toolName, params) {
10172
10585
  return params;
10173
10586
  }
10174
10587
 
10175
- // src/chat/tools/execution/inject-credentials.ts
10176
- function resolveCredentialInjection(toolName, command, capabilityRuntime, sandbox) {
10177
- if (toolName !== "bash" || !capabilityRuntime) {
10178
- return {};
10179
- }
10180
- const headerTransforms = capabilityRuntime.getTurnHeaderTransforms();
10181
- const env = capabilityRuntime.getTurnEnv();
10182
- const isCustomCommand = /^jr-rpc(?:\s|$)/.test(command.trim());
10183
- const shouldLog = !isCustomCommand && Boolean(headerTransforms && headerTransforms.length > 0);
10184
- if (shouldLog) {
10185
- const headerDomains = (headerTransforms ?? []).map(
10186
- (transform) => transform.domain
10187
- );
10188
- const skillName = sandbox.getActiveSkill()?.name;
10189
- logInfo(
10190
- "credential_inject_start",
10191
- {},
10192
- {
10193
- "app.skill.name": skillName,
10194
- "app.credential.delivery": "header_transform",
10195
- "app.credential.header_domains": headerDomains
10196
- },
10197
- `Injecting scoped credential headers for sandbox command (${skillName ?? "unknown skill"} \u2192 ${headerDomains.join(", ")})`
10198
- );
10199
- }
10200
- return { headerTransforms, env };
10201
- }
10202
-
10203
10588
  // src/chat/tools/execution/normalize-result.ts
10204
10589
  function isStructuredToolExecutionResult(value) {
10205
10590
  const content = value?.content;
@@ -10289,7 +10674,7 @@ function handleToolExecutionError(error, toolName, toolCallId, shouldTrace, trac
10289
10674
  }
10290
10675
 
10291
10676
  // src/chat/tools/agent-tools.ts
10292
- function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor, capabilityRuntime, pluginAuthOrchestration, onToolCall) {
10677
+ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor, pluginAuthOrchestration, onToolCall) {
10293
10678
  const shouldTrace = shouldEmitDevAgentTrace();
10294
10679
  return Object.entries(tools).map(([toolName, toolDef]) => ({
10295
10680
  name: toolName,
@@ -10329,21 +10714,11 @@ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor
10329
10714
  };
10330
10715
  }
10331
10716
  const bashCommand = toolName === "bash" && typeof parsed.command === "string" ? parsed.command.trim() : "";
10332
- const injection = resolveCredentialInjection(
10333
- toolName,
10334
- bashCommand,
10335
- capabilityRuntime,
10336
- sandbox
10337
- );
10338
10717
  const sandboxInput = buildSandboxInput(toolName, parsed);
10339
10718
  const isSandbox = Boolean(sandboxExecutor?.canExecute(toolName));
10340
10719
  const result = isSandbox ? await sandboxExecutor.execute({
10341
10720
  toolName,
10342
- input: toolName === "bash" && (injection.headerTransforms || injection.env) ? {
10343
- ...sandboxInput,
10344
- ...injection.headerTransforms ? { headerTransforms: injection.headerTransforms } : {},
10345
- ...injection.env ? { env: injection.env } : {}
10346
- } : sandboxInput
10721
+ input: sandboxInput
10347
10722
  }) : await toolDef.execute(parsed, {
10348
10723
  experimental_context: sandbox
10349
10724
  });
@@ -11206,6 +11581,7 @@ ${typeof result.stderr === "string" ? result.stderr : ""}`.toLowerCase();
11206
11581
  return false;
11207
11582
  }
11208
11583
  return [
11584
+ /\bjunior-auth-required\b/,
11209
11585
  /\b401\b/,
11210
11586
  /\bunauthorized\b/,
11211
11587
  /\bbad credentials\b/,
@@ -11218,6 +11594,33 @@ ${typeof result.stderr === "string" ? result.stderr : ""}`.toLowerCase();
11218
11594
  /\breauthoriz/
11219
11595
  ].some((pattern) => pattern.test(text));
11220
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
+ }
11221
11624
  function commandTargetsProvider(provider, command, details) {
11222
11625
  const normalizedCommand = command.trim().toLowerCase();
11223
11626
  if (!normalizedCommand) {
@@ -11232,16 +11635,15 @@ function commandTargetsProvider(provider, command, details) {
11232
11635
  const credentials = manifest?.credentials;
11233
11636
  if (credentials) {
11234
11637
  candidates.add(credentials.authTokenEnv.toLowerCase());
11235
- for (const domain of credentials.apiDomains) {
11638
+ for (const domain of credentials.domains) {
11236
11639
  candidates.add(domain.toLowerCase());
11237
11640
  }
11238
11641
  }
11239
- for (const domain of manifest?.apiDomains ?? []) {
11642
+ for (const domain of manifest?.domains ?? []) {
11240
11643
  candidates.add(domain.toLowerCase());
11241
11644
  }
11242
11645
  const combinedText = `${normalizedCommand}
11243
- ${details.stdout?.toLowerCase() ?? ""}
11244
- ${details.stderr?.toLowerCase() ?? ""}`;
11646
+ ${commandText(details).toLowerCase()}`;
11245
11647
  return [...candidates].some((candidate) => combinedText.includes(candidate));
11246
11648
  }
11247
11649
  function createPluginAuthOrchestration(deps, abortAgent) {
@@ -11299,23 +11701,22 @@ function createPluginAuthOrchestration(deps, abortAgent) {
11299
11701
  abortAgent();
11300
11702
  throw pendingPause;
11301
11703
  };
11302
- const handleCredentialUnavailable = async (input) => {
11303
- if (pendingPause) {
11304
- throw pendingPause;
11305
- }
11306
- if (!deps.requesterId || !getPluginOAuthConfig(input.error.provider)) {
11307
- throw input.error;
11308
- }
11309
- return await startAuthorizationPause(
11310
- input.error.provider,
11311
- input.activeSkill
11312
- );
11313
- };
11314
11704
  return {
11315
- handleCredentialUnavailable,
11316
11705
  handleCommandFailure: async (input) => {
11317
- const provider = input.activeSkill?.pluginProvider;
11318
- if (!provider || !deps.requesterId || !deps.userTokenStore || !getPluginOAuthConfig(provider) || !isCommandAuthFailure(input.details) || !commandTargetsProvider(provider, input.command, input.details)) {
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)) {
11319
11720
  return;
11320
11721
  }
11321
11722
  await startAuthorizationPause(provider, input.activeSkill, {
@@ -11581,14 +11982,15 @@ async function generateAssistantReply(messageText, context = {}) {
11581
11982
  ...context.configuration ?? {},
11582
11983
  ...persistedConfigurationValues
11583
11984
  };
11584
- const capabilityRuntime = createSkillCapabilityRuntime({
11585
- requesterId: context.requester?.userId
11586
- });
11985
+ const requesterId = context.requester?.userId;
11587
11986
  const userTokenStore = createUserTokenStore();
11588
11987
  sandboxExecutor = createSandboxExecutor({
11589
11988
  sandboxId: context.sandbox?.sandboxId,
11590
11989
  sandboxDependencyProfileHash: context.sandbox?.sandboxDependencyProfileHash,
11591
11990
  traceContext: spanContext,
11991
+ credentialEgress: requesterId ? {
11992
+ requesterId
11993
+ } : void 0,
11592
11994
  onSandboxAcquired: async (sandbox2) => {
11593
11995
  lastKnownSandboxId = sandbox2.sandboxId;
11594
11996
  lastKnownSandboxDependencyProfileHash = sandbox2.sandboxDependencyProfileHash;
@@ -11752,25 +12154,6 @@ async function generateAssistantReply(messageText, context = {}) {
11752
12154
  const syncResumeState = () => {
11753
12155
  loadedSkillNamesForResume = activeSkills.map((skill) => skill.name);
11754
12156
  };
11755
- const enableSkillCredentials = async (skill, reason) => {
11756
- if (!skill?.pluginProvider) {
11757
- return;
11758
- }
11759
- try {
11760
- await capabilityRuntime.enableCredentialsForTurn({
11761
- activeSkill: skill,
11762
- reason
11763
- });
11764
- } catch (error) {
11765
- if (error instanceof CredentialUnavailableError && context.requester?.userId) {
11766
- await pluginAuth.handleCredentialUnavailable({
11767
- activeSkill: skill,
11768
- error
11769
- });
11770
- }
11771
- throw error;
11772
- }
11773
- };
11774
12157
  setTags({
11775
12158
  conversationId: spanContext.conversationId,
11776
12159
  slackThreadId: context.correlation?.threadId,
@@ -11812,10 +12195,6 @@ async function generateAssistantReply(messageText, context = {}) {
11812
12195
  if (mcpAuth.getPendingPause()) {
11813
12196
  return void 0;
11814
12197
  }
11815
- await enableSkillCredentials(
11816
- effective,
11817
- `skill:${effective.name}:turn:load`
11818
- );
11819
12198
  if (!effective.pluginProvider) {
11820
12199
  return void 0;
11821
12200
  }
@@ -11868,7 +12247,6 @@ async function generateAssistantReply(messageText, context = {}) {
11868
12247
  timeoutResumeMessages = existingCheckpoint?.piMessages ?? [];
11869
12248
  throw mcpAuth.getPendingPause();
11870
12249
  }
11871
- await enableSkillCredentials(skill, `skill:${skill.name}:turn:resume`);
11872
12250
  }
11873
12251
  syncResumeState();
11874
12252
  const activeMcpCatalogs = toActiveMcpCatalogSummaries(
@@ -11929,7 +12307,6 @@ async function generateAssistantReply(messageText, context = {}) {
11929
12307
  spanContext,
11930
12308
  context.onStatus,
11931
12309
  sandboxExecutor,
11932
- capabilityRuntime,
11933
12310
  pluginAuth,
11934
12311
  onToolCall
11935
12312
  );
@@ -11939,7 +12316,6 @@ async function generateAssistantReply(messageText, context = {}) {
11939
12316
  spanContext,
11940
12317
  context.onStatus,
11941
12318
  sandboxExecutor,
11942
- capabilityRuntime,
11943
12319
  pluginAuth,
11944
12320
  onToolCall
11945
12321
  );
@@ -14290,6 +14666,337 @@ async function GET5(request, provider, waitUntil) {
14290
14666
  });
14291
14667
  }
14292
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
+
14293
15000
  // src/chat/slack/context.ts
14294
15001
  function toTrimmedSlackString(value) {
14295
15002
  const normalized = toOptionalString(value);
@@ -15087,10 +15794,20 @@ function createSlackTurnRuntime(deps) {
15087
15794
  runId
15088
15795
  }),
15089
15796
  async () => {
15090
- const rawUserText = message.text;
15091
- const userText = deps.stripLeadingBotMention(rawUserText, {
15797
+ const legacyAttachmentText = renderSlackLegacyAttachmentText(
15798
+ message.raw
15799
+ );
15800
+ const rawUserText = appendSlackLegacyAttachmentText(
15801
+ message.text,
15802
+ message.raw
15803
+ );
15804
+ const strippedUserText = deps.stripLeadingBotMention(message.text, {
15092
15805
  stripLeadingSlackMentionToken: Boolean(message.isMention)
15093
15806
  });
15807
+ const userText = appendSlackLegacyAttachmentText(
15808
+ strippedUserText,
15809
+ message.raw
15810
+ );
15094
15811
  const context = {
15095
15812
  threadId,
15096
15813
  requesterId: message.author.userId,
@@ -15129,7 +15846,7 @@ function createSlackTurnRuntime(deps) {
15129
15846
  rawText: rawUserText,
15130
15847
  text: userText,
15131
15848
  conversationContext: deps.getPreparedConversationContext(preparedState),
15132
- hasAttachments: message.attachments.length > 0,
15849
+ hasAttachments: message.attachments.length > 0 || legacyAttachmentText !== "",
15133
15850
  isExplicitMention: Boolean(message.isMention),
15134
15851
  context
15135
15852
  });
@@ -16084,9 +16801,13 @@ function createReplyToThread(deps) {
16084
16801
  modelId: botConfig.modelId
16085
16802
  },
16086
16803
  async () => {
16087
- const userText = stripLeadingBotMention(message.text, {
16804
+ const strippedUserText = stripLeadingBotMention(message.text, {
16088
16805
  stripLeadingSlackMentionToken: options.explicitMention || Boolean(message.isMention)
16089
16806
  });
16807
+ const userText = appendSlackLegacyAttachmentText(
16808
+ strippedUserText,
16809
+ message.raw
16810
+ );
16090
16811
  const preparedState = options.preparedState ?? await deps.prepareTurnState({
16091
16812
  thread,
16092
16813
  message,
@@ -16525,7 +17246,8 @@ function hasPendingImageHydration(conversation) {
16525
17246
  );
16526
17247
  }
16527
17248
  function createConversationMessageFromSdkMessage(entry) {
16528
- const rawText = normalizeConversationText(entry.text);
17249
+ const enrichedText = appendSlackLegacyAttachmentText(entry.text, entry.raw);
17250
+ const rawText = normalizeConversationText(enrichedText);
16529
17251
  if (!rawText) {
16530
17252
  return null;
16531
17253
  }
@@ -17651,6 +18373,12 @@ async function createApp(options) {
17651
18373
  app.post("/api/internal/turn-resume", (c) => {
17652
18374
  return POST(c.req.raw, waitUntil);
17653
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
+ });
17654
18382
  app.post("/api/webhooks/:platform", (c) => {
17655
18383
  return POST2(c.req.raw, c.req.param("platform"), waitUntil);
17656
18384
  });