@sentry/junior 0.45.0 → 0.47.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
@@ -31,7 +31,7 @@ import {
31
31
  runNonInteractiveCommand,
32
32
  sandboxSkillDir,
33
33
  sandboxSkillFile
34
- } from "./chunk-QAMTCT2R.js";
34
+ } from "./chunk-ELM6HJ6S.js";
35
35
  import {
36
36
  CredentialUnavailableError,
37
37
  buildOAuthTokenRequest,
@@ -2119,6 +2119,10 @@ function markTurnFailed(args) {
2119
2119
  }
2120
2120
 
2121
2121
  // src/chat/runtime/turn-user-message.ts
2122
+ function normalizeSlackMessageTs(value) {
2123
+ const trimmed = value?.trim();
2124
+ return trimmed && /^\d+(?:\.\d+)?$/.test(trimmed) ? trimmed : void 0;
2125
+ }
2122
2126
  function getTurnUserMessage(conversation, sessionId) {
2123
2127
  for (let index = conversation.messages.length - 1; index >= 0; index -= 1) {
2124
2128
  const message = conversation.messages[index];
@@ -2134,6 +2138,9 @@ function getTurnUserMessage(conversation, sessionId) {
2134
2138
  function getTurnUserMessageId(conversation, sessionId) {
2135
2139
  return getTurnUserMessage(conversation, sessionId)?.id;
2136
2140
  }
2141
+ function getTurnUserSlackMessageTs(message) {
2142
+ return normalizeSlackMessageTs(message?.meta?.slackTs) ?? normalizeSlackMessageTs(message?.id);
2143
+ }
2137
2144
  function getTurnUserReplyAttachmentContext(message) {
2138
2145
  const inboundAttachmentCount = message?.meta?.attachmentCount ?? 0;
2139
2146
  const imageAttachmentCount = message?.meta?.imageAttachmentCount ?? 0;
@@ -4459,6 +4466,29 @@ function truncateText(value, maxChars = MAX_TEXT_CHARS) {
4459
4466
  truncated: true
4460
4467
  };
4461
4468
  }
4469
+ function isMissingPathError(error) {
4470
+ if (!error || typeof error !== "object") {
4471
+ return false;
4472
+ }
4473
+ const candidate = error;
4474
+ if (candidate.code === "ENOENT") {
4475
+ return true;
4476
+ }
4477
+ return typeof candidate.message === "string" && candidate.message.startsWith("File not found:");
4478
+ }
4479
+ function missingPathSearchResult(params) {
4480
+ const textPath = params.displayPath ?? params.missingPath ?? params.path;
4481
+ return {
4482
+ content: [{ type: "text", text: `Path not found: ${textPath}` }],
4483
+ details: {
4484
+ ok: false,
4485
+ error: "not_found",
4486
+ path: params.path,
4487
+ ...params.missingPath ? { missing_path: params.missingPath } : {},
4488
+ truncated: false
4489
+ }
4490
+ };
4491
+ }
4462
4492
  function escapeRegExp(value) {
4463
4493
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4464
4494
  }
@@ -4513,13 +4543,35 @@ function resolveWorkspacePath(input, fallback = ".") {
4513
4543
  async function collectFiles(params) {
4514
4544
  const files = [];
4515
4545
  let limitReached = false;
4546
+ let missingPath;
4516
4547
  const visit = async (dirPath) => {
4517
- const entries = (await params.fs.readdir(dirPath)).sort(
4518
- (a, b) => a.toLowerCase().localeCompare(b.toLowerCase())
4519
- );
4548
+ if (missingPath) {
4549
+ return;
4550
+ }
4551
+ let entries;
4552
+ try {
4553
+ entries = (await params.fs.readdir(dirPath)).sort(
4554
+ (a, b) => a.toLowerCase().localeCompare(b.toLowerCase())
4555
+ );
4556
+ } catch (error) {
4557
+ if (isMissingPathError(error)) {
4558
+ missingPath = dirPath;
4559
+ return;
4560
+ }
4561
+ throw error;
4562
+ }
4520
4563
  for (const entry of entries) {
4521
4564
  const fullPath = path4.posix.join(dirPath, entry);
4522
- const stat2 = await params.fs.stat(fullPath);
4565
+ let stat2;
4566
+ try {
4567
+ stat2 = await params.fs.stat(fullPath);
4568
+ } catch (error) {
4569
+ if (isMissingPathError(error)) {
4570
+ missingPath = fullPath;
4571
+ return;
4572
+ }
4573
+ throw error;
4574
+ }
4523
4575
  if (stat2.isDirectory()) {
4524
4576
  if (!SKIPPED_DIRECTORIES.has(entry)) {
4525
4577
  await visit(fullPath);
@@ -4537,20 +4589,38 @@ async function collectFiles(params) {
4537
4589
  }
4538
4590
  }
4539
4591
  };
4540
- const stat = await params.fs.stat(params.root);
4592
+ let stat;
4593
+ try {
4594
+ stat = await params.fs.stat(params.root);
4595
+ } catch (error) {
4596
+ if (isMissingPathError(error)) {
4597
+ return {
4598
+ files,
4599
+ limitReached: false,
4600
+ missingPath: params.root,
4601
+ missingRoot: true
4602
+ };
4603
+ }
4604
+ throw error;
4605
+ }
4541
4606
  if (!stat.isDirectory()) {
4542
4607
  const relativePath = path4.posix.basename(params.root);
4543
4608
  return {
4544
4609
  files: !params.pattern || matchesGlob(relativePath, params.pattern) ? [params.root] : [],
4545
- limitReached: false
4610
+ limitReached: false,
4611
+ missingRoot: false
4546
4612
  };
4547
4613
  }
4548
4614
  await visit(params.root);
4549
- return { files, limitReached };
4615
+ return {
4616
+ files,
4617
+ limitReached,
4618
+ missingPath,
4619
+ missingRoot: missingPath === params.root
4620
+ };
4550
4621
  }
4551
4622
 
4552
- // src/chat/tools/sandbox/edit-file.ts
4553
- import { Type as Type2 } from "@sinclair/typebox";
4623
+ // src/chat/tools/sandbox/text-edits.ts
4554
4624
  function detectLineEnding(value) {
4555
4625
  return value.includes("\r\n") ? "\r\n" : "\n";
4556
4626
  }
@@ -4582,6 +4652,44 @@ function firstChangedLine(oldContent, newContent) {
4582
4652
  }
4583
4653
  return void 0;
4584
4654
  }
4655
+ function prepareTextReplacementArguments(input) {
4656
+ if (!input || typeof input !== "object") {
4657
+ return input;
4658
+ }
4659
+ const raw = { ...input };
4660
+ if (typeof raw.edits === "string") {
4661
+ try {
4662
+ raw.edits = JSON.parse(raw.edits);
4663
+ } catch {
4664
+ return raw;
4665
+ }
4666
+ }
4667
+ const edits = Array.isArray(raw.edits) ? [...raw.edits] : [];
4668
+ const oldText = raw.oldText ?? raw.old_text;
4669
+ const newText = raw.newText ?? raw.new_text;
4670
+ if (typeof oldText === "string" && typeof newText === "string") {
4671
+ edits.push({ oldText, newText });
4672
+ }
4673
+ if (edits.length > 0) {
4674
+ raw.edits = edits.map((edit) => {
4675
+ if (!edit || typeof edit !== "object") {
4676
+ return edit;
4677
+ }
4678
+ const record = edit;
4679
+ const { old_text, new_text, ...rest } = record;
4680
+ return {
4681
+ ...rest,
4682
+ oldText: record.oldText ?? old_text,
4683
+ newText: record.newText ?? new_text
4684
+ };
4685
+ });
4686
+ }
4687
+ delete raw.oldText;
4688
+ delete raw.old_text;
4689
+ delete raw.newText;
4690
+ delete raw.new_text;
4691
+ return raw;
4692
+ }
4585
4693
  function buildCompactDiff(oldContent, newContent) {
4586
4694
  const oldLines = oldContent.split("\n");
4587
4695
  const newLines = newContent.split("\n");
@@ -4623,19 +4731,19 @@ function buildCompactDiff(oldContent, newContent) {
4623
4731
  firstChangedLine: firstChangedLine(oldContent, newContent)
4624
4732
  };
4625
4733
  }
4626
- function validateAndApplyEdits(content, edits, filePath) {
4734
+ function validateAndApplyTextEdits(content, edits, targetName) {
4627
4735
  if (!Array.isArray(edits) || edits.length === 0) {
4628
- throw new Error("editFile requires at least one edit.");
4736
+ throw new Error(`${targetName} requires at least one edit.`);
4629
4737
  }
4630
4738
  const normalizedEdits = edits.map((edit, index) => {
4631
4739
  if (typeof edit.oldText !== "string" || edit.oldText.length === 0) {
4632
4740
  throw new Error(
4633
- `edits[${index}].oldText must not be empty in ${filePath}.`
4741
+ `edits[${index}].oldText must not be empty in ${targetName}.`
4634
4742
  );
4635
4743
  }
4636
4744
  if (typeof edit.newText !== "string") {
4637
4745
  throw new Error(
4638
- `edits[${index}].newText must be a string in ${filePath}.`
4746
+ `edits[${index}].newText must be a string in ${targetName}.`
4639
4747
  );
4640
4748
  }
4641
4749
  return {
@@ -4649,13 +4757,13 @@ function validateAndApplyEdits(content, edits, filePath) {
4649
4757
  const matchIndex = content.indexOf(edit.oldText);
4650
4758
  if (matchIndex === -1) {
4651
4759
  throw new Error(
4652
- `Could not find edits[${index}] in ${filePath}. oldText must match exactly including whitespace and newlines.`
4760
+ `Could not find edits[${index}] in ${targetName}. oldText must match exactly including whitespace and newlines.`
4653
4761
  );
4654
4762
  }
4655
4763
  const occurrences = countOccurrences(content, edit.oldText);
4656
4764
  if (occurrences > 1) {
4657
4765
  throw new Error(
4658
- `Found ${occurrences} occurrences of edits[${index}] in ${filePath}. Each oldText must be unique.`
4766
+ `Found ${occurrences} occurrences of edits[${index}] in ${targetName}. Each oldText must be unique.`
4659
4767
  );
4660
4768
  }
4661
4769
  matchedEdits.push({
@@ -4671,7 +4779,7 @@ function validateAndApplyEdits(content, edits, filePath) {
4671
4779
  const current = matchedEdits[index];
4672
4780
  if (previous.matchIndex + previous.matchLength > current.matchIndex) {
4673
4781
  throw new Error(
4674
- `edits[${previous.editIndex}] and edits[${current.editIndex}] overlap in ${filePath}. Merge overlapping replacements into one edit.`
4782
+ `edits[${previous.editIndex}] and edits[${current.editIndex}] overlap in ${targetName}. Merge overlapping replacements into one edit.`
4675
4783
  );
4676
4784
  }
4677
4785
  }
@@ -4681,47 +4789,15 @@ function validateAndApplyEdits(content, edits, filePath) {
4681
4789
  newContent = newContent.slice(0, edit.matchIndex) + edit.newText + newContent.slice(edit.matchIndex + edit.matchLength);
4682
4790
  }
4683
4791
  if (newContent === content) {
4684
- throw new Error(`No changes made to ${filePath}.`);
4792
+ throw new Error(`No changes made to ${targetName}.`);
4685
4793
  }
4686
4794
  return { baseContent: content, newContent };
4687
4795
  }
4796
+
4797
+ // src/chat/tools/sandbox/edit-file.ts
4798
+ import { Type as Type2 } from "@sinclair/typebox";
4688
4799
  function prepareEditFileArguments(input) {
4689
- if (!input || typeof input !== "object") {
4690
- return input;
4691
- }
4692
- const raw = { ...input };
4693
- if (typeof raw.edits === "string") {
4694
- try {
4695
- raw.edits = JSON.parse(raw.edits);
4696
- } catch {
4697
- return raw;
4698
- }
4699
- }
4700
- const edits = Array.isArray(raw.edits) ? [...raw.edits] : [];
4701
- const oldText = raw.oldText ?? raw.old_text;
4702
- const newText = raw.newText ?? raw.new_text;
4703
- if (typeof oldText === "string" && typeof newText === "string") {
4704
- edits.push({ oldText, newText });
4705
- }
4706
- if (edits.length > 0) {
4707
- raw.edits = edits.map((edit) => {
4708
- if (!edit || typeof edit !== "object") {
4709
- return edit;
4710
- }
4711
- const record = edit;
4712
- const { old_text, new_text, ...rest } = record;
4713
- return {
4714
- ...rest,
4715
- oldText: record.oldText ?? old_text,
4716
- newText: record.newText ?? new_text
4717
- };
4718
- });
4719
- }
4720
- delete raw.oldText;
4721
- delete raw.old_text;
4722
- delete raw.newText;
4723
- delete raw.new_text;
4724
- return raw;
4800
+ return prepareTextReplacementArguments(input);
4725
4801
  }
4726
4802
  async function editFile(params) {
4727
4803
  const filePath = resolveWorkspacePath(params.path);
@@ -4729,7 +4805,7 @@ async function editFile(params) {
4729
4805
  const { bom, text } = stripBom(rawContent);
4730
4806
  const lineEnding = detectLineEnding(text);
4731
4807
  const normalizedContent = normalizeToLf(text);
4732
- const { baseContent, newContent } = validateAndApplyEdits(
4808
+ const { baseContent, newContent } = validateAndApplyTextEdits(
4733
4809
  normalizedContent,
4734
4810
  params.edits,
4735
4811
  params.path
@@ -4810,12 +4886,18 @@ async function findFiles(params) {
4810
4886
  }
4811
4887
  const root = resolveWorkspacePath(params.path);
4812
4888
  const limit = positiveInteger(params.limit) ?? DEFAULT_FIND_LIMIT;
4813
- const { files, limitReached } = await collectFiles({
4889
+ const { files, limitReached, missingPath, missingRoot } = await collectFiles({
4814
4890
  fs: params.fs,
4815
4891
  root,
4816
4892
  pattern: params.pattern,
4817
4893
  limit
4818
4894
  });
4895
+ if (missingPath) {
4896
+ return missingPathSearchResult({
4897
+ path: params.path ?? ".",
4898
+ ...missingRoot ? { displayPath: params.path ?? "." } : { missingPath }
4899
+ });
4900
+ }
4819
4901
  const relativePaths = files.map(
4820
4902
  (filePath) => path5.posix.relative(root, filePath)
4821
4903
  );
@@ -4912,11 +4994,17 @@ async function grepFiles(params) {
4912
4994
  const limit = positiveInteger(params.limit) ?? DEFAULT_GREP_LIMIT;
4913
4995
  const context = positiveInteger(params.context) ?? 0;
4914
4996
  const regex = params.literal ? void 0 : new RegExp(params.pattern, params.ignoreCase ? "i" : "");
4915
- const { files } = await collectFiles({
4997
+ const { files, missingPath, missingRoot } = await collectFiles({
4916
4998
  fs: params.fs,
4917
4999
  root,
4918
5000
  pattern: params.glob
4919
5001
  });
5002
+ if (missingPath) {
5003
+ return missingPathSearchResult({
5004
+ path: params.path ?? ".",
5005
+ ...missingRoot ? { displayPath: params.path ?? "." } : { missingPath }
5006
+ });
5007
+ }
4920
5008
  const output = [];
4921
5009
  let matchCount = 0;
4922
5010
  let matchLimitReached = false;
@@ -4926,8 +5014,14 @@ async function grepFiles(params) {
4926
5014
  let content;
4927
5015
  try {
4928
5016
  content = await params.fs.readFile(filePath, { encoding: "utf8" });
4929
- } catch {
4930
- continue;
5017
+ } catch (error) {
5018
+ if (isMissingPathError(error)) {
5019
+ return missingPathSearchResult({
5020
+ path: params.path ?? ".",
5021
+ missingPath: filePath
5022
+ });
5023
+ }
5024
+ throw error;
4931
5025
  }
4932
5026
  if (content.includes("\0")) {
4933
5027
  continue;
@@ -5207,13 +5301,29 @@ var DEFAULT_LIST_LIMIT = 500;
5207
5301
  async function listDir(params) {
5208
5302
  const dirPath = resolveWorkspacePath(params.path);
5209
5303
  const limit = positiveInteger(params.limit) ?? DEFAULT_LIST_LIMIT;
5210
- const stat = await params.fs.stat(dirPath);
5304
+ let stat;
5305
+ try {
5306
+ stat = await params.fs.stat(dirPath);
5307
+ } catch (error) {
5308
+ if (isMissingPathError(error)) {
5309
+ return missingPathSearchResult({ path: params.path ?? "." });
5310
+ }
5311
+ throw error;
5312
+ }
5211
5313
  if (!stat.isDirectory()) {
5212
5314
  throw new Error(`Not a directory: ${params.path ?? "."}`);
5213
5315
  }
5214
- const entries = (await params.fs.readdir(dirPath)).sort(
5215
- (a, b) => a.toLowerCase().localeCompare(b.toLowerCase())
5216
- );
5316
+ let entries;
5317
+ try {
5318
+ entries = (await params.fs.readdir(dirPath)).sort(
5319
+ (a, b) => a.toLowerCase().localeCompare(b.toLowerCase())
5320
+ );
5321
+ } catch (error) {
5322
+ if (isMissingPathError(error)) {
5323
+ return missingPathSearchResult({ path: params.path ?? "." });
5324
+ }
5325
+ throw error;
5326
+ }
5217
5327
  const output = [];
5218
5328
  let entryLimitReached = false;
5219
5329
  for (const entry of entries) {
@@ -5225,8 +5335,14 @@ async function listDir(params) {
5225
5335
  try {
5226
5336
  const entryStat = await params.fs.stat(entryPath);
5227
5337
  output.push(`${entry}${entryStat.isDirectory() ? "/" : ""}`);
5228
- } catch {
5229
- continue;
5338
+ } catch (error) {
5339
+ if (isMissingPathError(error)) {
5340
+ return missingPathSearchResult({
5341
+ path: params.path ?? ".",
5342
+ missingPath: entryPath
5343
+ });
5344
+ }
5345
+ throw error;
5230
5346
  }
5231
5347
  }
5232
5348
  const bounded = truncateText(
@@ -5817,6 +5933,14 @@ function sliceFileContent(params) {
5817
5933
  } : {}
5818
5934
  };
5819
5935
  }
5936
+ function missingFileResult(path11) {
5937
+ return {
5938
+ content: "",
5939
+ error: "not_found",
5940
+ path: path11,
5941
+ success: false
5942
+ };
5943
+ }
5820
5944
  function createReadFileTool() {
5821
5945
  return tool({
5822
5946
  description: "Read a bounded line range from a file in the sandbox workspace. Use when you need exact file contents to verify facts or make edits safely. Prefer grep/findFiles/listDir for broad discovery.",
@@ -6173,6 +6297,37 @@ function createSlackMessageAddReactionTool(context, state) {
6173
6297
  // src/chat/tools/slack/canvas-tools.ts
6174
6298
  import { Type as Type16 } from "@sinclair/typebox";
6175
6299
 
6300
+ // src/chat/slack/canvas-references.ts
6301
+ var SLACK_FILE_ID_PATTERN = /^F[A-Z0-9]{4,}$/i;
6302
+ var SLACK_CANVAS_PATH_PATTERN = /^\/(?:docs|canvas|files)\/(?:T[A-Z0-9]+\/)?(?:U[A-Z0-9]+\/)?(F[A-Z0-9]{4,})(?:\/|$)/i;
6303
+ function isSlackHost(hostname) {
6304
+ return hostname === "slack.com" || hostname.endsWith(".slack.com");
6305
+ }
6306
+ function normalizeReferenceInput(input) {
6307
+ let value = input.trim();
6308
+ if (value.startsWith("<") && value.endsWith(">")) {
6309
+ value = value.slice(1, -1);
6310
+ }
6311
+ return value.split("|", 1)[0]?.trim() ?? "";
6312
+ }
6313
+ function extractCanvasId(input) {
6314
+ const trimmed = normalizeReferenceInput(input);
6315
+ if (!trimmed) return void 0;
6316
+ if (SLACK_FILE_ID_PATTERN.test(trimmed)) {
6317
+ return trimmed.toUpperCase();
6318
+ }
6319
+ try {
6320
+ const url = new URL(trimmed);
6321
+ if (!isSlackHost(url.hostname)) {
6322
+ return void 0;
6323
+ }
6324
+ const urlMatch = url.pathname.match(SLACK_CANVAS_PATH_PATTERN);
6325
+ return urlMatch?.[1]?.toUpperCase();
6326
+ } catch {
6327
+ return void 0;
6328
+ }
6329
+ }
6330
+
6176
6331
  // src/chat/tools/slack/canvases.ts
6177
6332
  function normalizeCanvasMarkdown(markdown) {
6178
6333
  let normalizedHeadingCount = 0;
@@ -6264,27 +6419,7 @@ async function grantChannelCanvasAccess(canvasId, channelId) {
6264
6419
  );
6265
6420
  }
6266
6421
  }
6267
- async function lookupCanvasSection(canvasId, containsText) {
6268
- const client2 = getSlackClient();
6269
- const response = await withSlackRetries(
6270
- () => client2.canvases.sections.lookup({
6271
- canvas_id: canvasId,
6272
- criteria: {
6273
- contains_text: containsText
6274
- }
6275
- }),
6276
- 3,
6277
- {
6278
- action: "canvases.sections.lookup",
6279
- attributes: {
6280
- "app.slack.canvas.canvas_id_prefix": canvasId.slice(0, 1),
6281
- "app.slack.canvas.contains_text_length": containsText.length
6282
- }
6283
- }
6284
- );
6285
- return response.sections?.[0]?.id;
6286
- }
6287
- async function updateCanvas(input) {
6422
+ async function writeCanvasMarkdown(input) {
6288
6423
  const client2 = getSlackClient();
6289
6424
  const normalizedContent = normalizeCanvasMarkdown(input.markdown);
6290
6425
  await withSlackRetries(
@@ -6292,8 +6427,7 @@ async function updateCanvas(input) {
6292
6427
  canvas_id: input.canvasId,
6293
6428
  changes: [
6294
6429
  {
6295
- operation: input.operation,
6296
- section_id: input.sectionId,
6430
+ operation: "replace",
6297
6431
  document_content: {
6298
6432
  type: "markdown",
6299
6433
  markdown: normalizedContent.markdown
@@ -6306,27 +6440,19 @@ async function updateCanvas(input) {
6306
6440
  action: "canvases.edit",
6307
6441
  attributes: {
6308
6442
  "app.slack.canvas.canvas_id_prefix": input.canvasId.slice(0, 1),
6309
- "app.slack.canvas.operation": input.operation,
6443
+ "app.slack.canvas.operation": "replace",
6310
6444
  "app.slack.canvas.markdown_length": normalizedContent.markdown.length,
6311
6445
  "app.slack.canvas.markdown_normalized": normalizedContent.normalizedHeadingCount > 0,
6312
6446
  "app.slack.canvas.normalized_heading_count": normalizedContent.normalizedHeadingCount
6313
6447
  }
6314
6448
  }
6315
6449
  );
6450
+ return normalizedContent;
6316
6451
  }
6317
- var CANVAS_ID_PATTERN = /^F[A-Z0-9]+$/i;
6318
- var CANVAS_URL_FILE_ID_PATTERN = /\/(?:docs|canvas|files)\/(?:T[A-Z0-9]+\/)?(?:U[A-Z0-9]+\/)?(F[A-Z0-9]+)/i;
6319
- function extractCanvasId(input) {
6320
- const trimmed = input.trim();
6321
- if (!trimmed) return void 0;
6322
- if (CANVAS_ID_PATTERN.test(trimmed)) {
6323
- return trimmed.toUpperCase();
6324
- }
6325
- const urlMatch = trimmed.match(CANVAS_URL_FILE_ID_PATTERN);
6326
- if (urlMatch?.[1]) {
6327
- return urlMatch[1].toUpperCase();
6328
- }
6329
- return void 0;
6452
+ function isCanvasFile(file) {
6453
+ const filetype = file.filetype?.toLowerCase() ?? "";
6454
+ const mimetype = file.mimetype?.toLowerCase() ?? "";
6455
+ return filetype === "quip" || filetype === "canvas" || mimetype.includes("quip") || mimetype.includes("canvas");
6330
6456
  }
6331
6457
  async function readCanvas(canvasIdOrUrl) {
6332
6458
  const canvasId = extractCanvasId(canvasIdOrUrl);
@@ -6352,6 +6478,9 @@ async function readCanvas(canvasIdOrUrl) {
6352
6478
  if (!file) {
6353
6479
  throw new Error("Slack returned no file metadata for canvas.");
6354
6480
  }
6481
+ if (!isCanvasFile(file)) {
6482
+ throw new Error("Slack file metadata did not describe a Canvas document.");
6483
+ }
6355
6484
  const downloadUrl = file.url_private_download ?? file.url_private;
6356
6485
  if (!downloadUrl) {
6357
6486
  throw new Error(
@@ -6371,7 +6500,6 @@ async function readCanvas(canvasIdOrUrl) {
6371
6500
  }
6372
6501
 
6373
6502
  // src/chat/tools/slack/canvas-tools.ts
6374
- var MAX_CANVAS_READ_CHARS = 4e4;
6375
6503
  var MAX_RECENT_CANVASES = 5;
6376
6504
  function mergeRecentCanvases(existing, created) {
6377
6505
  const nextEntry = {
@@ -6384,6 +6512,46 @@ function mergeRecentCanvases(existing, created) {
6384
6512
  const deduped = prior.filter((entry) => entry.id !== created.id);
6385
6513
  return [nextEntry, ...deduped].slice(0, MAX_RECENT_CANVASES);
6386
6514
  }
6515
+ function prepareCanvasEditArguments(input) {
6516
+ return prepareTextReplacementArguments(input);
6517
+ }
6518
+ function storedCanvasUrl(state, canvasId) {
6519
+ const lastCanvasUrl = state.artifactState.lastCanvasUrl;
6520
+ if (lastCanvasUrl && extractCanvasId(lastCanvasUrl) === canvasId) {
6521
+ return lastCanvasUrl;
6522
+ }
6523
+ for (const canvas of state.artifactState.recentCanvases ?? []) {
6524
+ if (extractCanvasId(canvas.id) === canvasId) {
6525
+ return canvas.url;
6526
+ }
6527
+ if (canvas.url && extractCanvasId(canvas.url) === canvasId) {
6528
+ return canvas.url;
6529
+ }
6530
+ }
6531
+ return void 0;
6532
+ }
6533
+ function resolveCanvasTarget(canvas) {
6534
+ const canvasId = extractCanvasId(canvas);
6535
+ if (!canvasId) {
6536
+ return {
6537
+ ok: false,
6538
+ error: "Could not parse a Slack canvas/file ID from input. Provide an F-prefixed ID or a Slack canvas/docs URL."
6539
+ };
6540
+ }
6541
+ return { ok: true, canvasId };
6542
+ }
6543
+ var editReplacementSchema2 = Type16.Object(
6544
+ {
6545
+ oldText: Type16.String({
6546
+ minLength: 1,
6547
+ description: "Exact Canvas markdown to replace. It must be unique in the current Canvas body and must not overlap another edit."
6548
+ }),
6549
+ newText: Type16.String({
6550
+ description: "Replacement Canvas markdown for this edit."
6551
+ })
6552
+ },
6553
+ { additionalProperties: false }
6554
+ );
6387
6555
  function createSlackCanvasCreateTool(context, state) {
6388
6556
  return tool({
6389
6557
  description: "Create a Slack canvas for long-form output in the active assistant context channel. Use when the answer is better as a reusable document than a thread reply: long-form research, timelines, bios/profiles, structured notes, plans, comparisons, or anything likely to exceed one compact Slack reply. After creating it, reply with one or two short sentences plus the canvas link; do not recap the canvas contents. Do not use for short answers that fit cleanly in one normal thread reply.",
@@ -6432,7 +6600,6 @@ function createSlackCanvasCreateTool(context, state) {
6432
6600
  markdown,
6433
6601
  channelId: targetChannelId
6434
6602
  });
6435
- state.setTurnCreatedCanvasId(created.canvasId);
6436
6603
  await state.patchArtifactState({
6437
6604
  lastCanvasId: created.canvasId,
6438
6605
  lastCanvasUrl: created.permalink,
@@ -6456,67 +6623,111 @@ function createSlackCanvasCreateTool(context, state) {
6456
6623
  }
6457
6624
  });
6458
6625
  }
6459
- function createSlackCanvasUpdateTool(state, _context) {
6626
+ function createSlackCanvasReadTool() {
6460
6627
  return tool({
6461
- description: "Update the active Slack canvas tracked in artifact context. Use when continuing or correcting a document already tracked in this thread. Do not use to create a brand-new long-form artifact.",
6462
- inputSchema: Type16.Object({
6463
- markdown: Type16.String({
6464
- minLength: 1,
6465
- description: "Markdown content to insert or use as replacement text."
6466
- }),
6467
- operation: Type16.Optional(
6468
- Type16.Union(
6469
- [
6470
- Type16.Literal("insert_at_end"),
6471
- Type16.Literal("insert_at_start"),
6472
- Type16.Literal("replace")
6473
- ],
6474
- { description: "Canvas update mode." }
6475
- )
6476
- ),
6477
- section_id: Type16.Optional(
6478
- Type16.String({
6479
- minLength: 1,
6480
- description: "Optional section ID required for targeted replace operations."
6481
- })
6482
- ),
6483
- section_contains_text: Type16.Optional(
6484
- Type16.String({
6628
+ description: "Read a bounded line range from a Slack canvas as markdown. Use when you need exact Canvas contents to verify facts or make edits safely. Do not use for generic web pages \u2014 use webFetch for those.",
6629
+ annotations: { readOnlyHint: true, destructiveHint: false },
6630
+ inputSchema: Type16.Object(
6631
+ {
6632
+ canvas: Type16.String({
6485
6633
  minLength: 1,
6486
- description: "Optional helper text used to find the target section when section_id is not provided."
6487
- })
6488
- )
6489
- }),
6490
- execute: async ({
6491
- markdown,
6492
- operation,
6493
- section_id,
6494
- section_contains_text
6495
- }) => {
6496
- const targetCanvasId = state.getTurnCreatedCanvasId() ?? state.getCurrentCanvasId();
6497
- const resolvedOperation = operation ?? "insert_at_end";
6498
- if (!targetCanvasId) {
6634
+ description: "Canvas/file ID (e.g. `F0ABCDEF`) or Slack canvas/docs URL (e.g. `https://team.slack.com/docs/T.../F...`)."
6635
+ }),
6636
+ offset: Type16.Optional(
6637
+ Type16.Integer({
6638
+ minimum: 1,
6639
+ description: "1-indexed line number to start reading from."
6640
+ })
6641
+ ),
6642
+ limit: Type16.Optional(
6643
+ Type16.Integer({
6644
+ minimum: 1,
6645
+ description: "Maximum number of lines to read. Defaults to 1000."
6646
+ })
6647
+ )
6648
+ },
6649
+ { additionalProperties: false }
6650
+ ),
6651
+ execute: async ({ canvas, offset, limit }) => {
6652
+ const target = resolveCanvasTarget(canvas);
6653
+ if (!target.ok) {
6654
+ return target;
6655
+ }
6656
+ try {
6657
+ const result = await readCanvas(target.canvasId);
6658
+ const range = sliceFileContent({
6659
+ content: normalizeToLf(result.content),
6660
+ limit,
6661
+ offset,
6662
+ path: result.canvasId
6663
+ });
6664
+ return {
6665
+ ok: true,
6666
+ canvas_id: result.canvasId,
6667
+ title: result.title,
6668
+ permalink: result.permalink,
6669
+ mimetype: result.mimetype,
6670
+ filetype: result.filetype,
6671
+ original_byte_length: result.byteLength,
6672
+ content: range.content,
6673
+ start_line: range.start_line,
6674
+ end_line: range.end_line,
6675
+ total_lines: range.total_lines,
6676
+ truncated: range.truncated,
6677
+ continuation: range.continuation
6678
+ };
6679
+ } catch (error) {
6680
+ const message = error instanceof Error ? error.message : "canvas read failed";
6499
6681
  logWarn(
6500
- "slack_canvas_update_missing_target",
6682
+ "slack_canvas_read_failed",
6501
6683
  {},
6502
6684
  {
6503
- "gen_ai.tool.name": "slackCanvasUpdate",
6504
- "app.artifacts.last_canvas_id": state.artifactState.lastCanvasId ?? "none",
6505
- "app.artifacts.turn_created_canvas_id": state.getTurnCreatedCanvasId() ?? "none"
6685
+ "gen_ai.tool.name": "slackCanvasRead",
6686
+ "app.slack.canvas.canvas_id_prefix": target.canvasId.slice(0, 1)
6506
6687
  },
6507
- "Canvas update rejected because no explicit target canvas was provided"
6688
+ message
6508
6689
  );
6509
6690
  return {
6510
6691
  ok: false,
6511
- error: "No active canvas found in artifact context"
6692
+ canvas_id: target.canvasId,
6693
+ error: message
6512
6694
  };
6513
6695
  }
6514
- const operationKey = createOperationKey("slackCanvasUpdate", {
6515
- canvas_id: targetCanvasId,
6516
- markdown,
6517
- operation: resolvedOperation,
6518
- section_id: section_id ?? null,
6519
- section_contains_text: section_contains_text ?? null
6696
+ }
6697
+ });
6698
+ }
6699
+ function createSlackCanvasEditTool(state) {
6700
+ return tool({
6701
+ description: "Edit one Slack canvas with exact markdown replacements. Use for precise changes to existing Canvas content. Each oldText must match exactly, be unique, and not overlap another edit. Returns a diff.",
6702
+ promptSnippet: "existing-canvas exact edits; returns diff",
6703
+ promptGuidelines: [
6704
+ "prefer over slackCanvasWrite for targeted changes",
6705
+ "oldText exact, unique, non-overlapping",
6706
+ "multiple same-canvas changes: one edits[] call"
6707
+ ],
6708
+ prepareArguments: prepareCanvasEditArguments,
6709
+ executionMode: "sequential",
6710
+ inputSchema: Type16.Object(
6711
+ {
6712
+ canvas: Type16.String({
6713
+ minLength: 1,
6714
+ description: "Canvas/file ID (e.g. `F0ABCDEF`) or Slack canvas/docs URL."
6715
+ }),
6716
+ edits: Type16.Array(editReplacementSchema2, {
6717
+ minItems: 1,
6718
+ description: "Exact replacements matched against the current Canvas body, not incrementally."
6719
+ })
6720
+ },
6721
+ { additionalProperties: false }
6722
+ ),
6723
+ execute: async ({ canvas, edits }) => {
6724
+ const target = resolveCanvasTarget(canvas);
6725
+ if (!target.ok) {
6726
+ return target;
6727
+ }
6728
+ const operationKey = createOperationKey("slackCanvasEdit", {
6729
+ canvas_id: target.canvasId,
6730
+ edits
6520
6731
  });
6521
6732
  const cached = state.getOperationResult(operationKey);
6522
6733
  if (cached) {
@@ -6525,72 +6736,124 @@ function createSlackCanvasUpdateTool(state, _context) {
6525
6736
  deduplicated: true
6526
6737
  };
6527
6738
  }
6528
- const sectionId = section_id ?? (section_contains_text ? await lookupCanvasSection(targetCanvasId, section_contains_text) : void 0);
6529
- await updateCanvas({
6530
- canvasId: targetCanvasId,
6531
- markdown,
6532
- operation: resolvedOperation,
6533
- sectionId
6534
- });
6535
- await state.patchArtifactState({ lastCanvasId: targetCanvasId });
6536
- const response = {
6537
- ok: true,
6538
- canvas_id: targetCanvasId,
6539
- operation: resolvedOperation,
6540
- section_id: sectionId
6541
- };
6542
- state.setOperationResult(operationKey, response);
6543
- return response;
6739
+ try {
6740
+ const current = await readCanvas(target.canvasId);
6741
+ const normalizedContent = normalizeToLf(current.content);
6742
+ const { baseContent, newContent } = validateAndApplyTextEdits(
6743
+ normalizedContent,
6744
+ edits,
6745
+ target.canvasId
6746
+ );
6747
+ const written = await writeCanvasMarkdown({
6748
+ canvasId: target.canvasId,
6749
+ markdown: newContent
6750
+ });
6751
+ await state.patchArtifactState({
6752
+ lastCanvasId: target.canvasId,
6753
+ lastCanvasUrl: current.permalink ?? state.artifactState.lastCanvasUrl
6754
+ });
6755
+ const diff = buildCompactDiff(
6756
+ normalizeCanvasMarkdown(baseContent).markdown,
6757
+ written.markdown
6758
+ );
6759
+ const response = {
6760
+ ok: true,
6761
+ canvas_id: target.canvasId,
6762
+ title: current.title,
6763
+ permalink: current.permalink,
6764
+ diff: diff.diff,
6765
+ first_changed_line: diff.firstChangedLine,
6766
+ replacements: edits.length,
6767
+ normalized_heading_count: written.normalizedHeadingCount,
6768
+ summary: `Edited canvas ${target.canvasId}`
6769
+ };
6770
+ state.setOperationResult(operationKey, response);
6771
+ return response;
6772
+ } catch (error) {
6773
+ const message = error instanceof Error ? error.message : "canvas edit failed";
6774
+ logWarn(
6775
+ "slack_canvas_edit_failed",
6776
+ {},
6777
+ {
6778
+ "gen_ai.tool.name": "slackCanvasEdit",
6779
+ "app.slack.canvas.canvas_id_prefix": target.canvasId.slice(0, 1)
6780
+ },
6781
+ message
6782
+ );
6783
+ return {
6784
+ ok: false,
6785
+ canvas_id: target.canvasId,
6786
+ error: message
6787
+ };
6788
+ }
6544
6789
  }
6545
6790
  });
6546
6791
  }
6547
- function createSlackCanvasReadTool() {
6792
+ function createSlackCanvasWriteTool(state) {
6548
6793
  return tool({
6549
- description: "Read a Slack canvas the bot has access to (including canvases the bot created) by canvas ID or Slack canvas/docs URL. Use when the user shares a Slack canvas link (https://*.slack.com/docs/... or /canvas/...) or references a canvas ID and you need its contents. Do not use for generic web pages \u2014 use webFetch for those.",
6550
- annotations: { readOnlyHint: true, destructiveHint: false },
6551
- inputSchema: Type16.Object({
6552
- canvas: Type16.String({
6553
- minLength: 1,
6554
- description: "Canvas/file ID (e.g. `F0ABCDEF`) or Slack canvas/docs URL (e.g. `https://team.slack.com/docs/T.../F...`)."
6555
- })
6556
- }),
6557
- execute: async ({ canvas }) => {
6558
- const canvasId = extractCanvasId(canvas);
6559
- if (!canvasId) {
6794
+ description: "Write UTF-8 markdown content to a Slack canvas. Use for deliberate full-Canvas replacement after validation. Do not use for targeted edits.",
6795
+ promptSnippet: "deliberate full-canvas replacement",
6796
+ promptGuidelines: ["targeted existing-canvas changes: slackCanvasEdit"],
6797
+ executionMode: "sequential",
6798
+ inputSchema: Type16.Object(
6799
+ {
6800
+ canvas: Type16.String({
6801
+ minLength: 1,
6802
+ description: "Canvas/file ID (e.g. `F0ABCDEF`) or Slack canvas/docs URL."
6803
+ }),
6804
+ content: Type16.String({
6805
+ description: "UTF-8 markdown content to write."
6806
+ })
6807
+ },
6808
+ { additionalProperties: false }
6809
+ ),
6810
+ execute: async ({ canvas, content }) => {
6811
+ const target = resolveCanvasTarget(canvas);
6812
+ if (!target.ok) {
6813
+ return target;
6814
+ }
6815
+ const operationKey = createOperationKey("slackCanvasWrite", {
6816
+ canvas_id: target.canvasId,
6817
+ content
6818
+ });
6819
+ const cached = state.getOperationResult(operationKey);
6820
+ if (cached) {
6560
6821
  return {
6561
- ok: false,
6562
- error: "Could not parse a Slack canvas/file ID from input. Provide an F-prefixed ID or a Slack canvas/docs URL."
6822
+ ...cached,
6823
+ deduplicated: true
6563
6824
  };
6564
6825
  }
6565
6826
  try {
6566
- const result = await readCanvas(canvas);
6567
- const truncated = result.content.length > MAX_CANVAS_READ_CHARS;
6568
- const content = truncated ? result.content.slice(0, MAX_CANVAS_READ_CHARS) : result.content;
6569
- return {
6827
+ const written = await writeCanvasMarkdown({
6828
+ canvasId: target.canvasId,
6829
+ markdown: content
6830
+ });
6831
+ await state.patchArtifactState({
6832
+ lastCanvasId: target.canvasId,
6833
+ lastCanvasUrl: storedCanvasUrl(state, target.canvasId)
6834
+ });
6835
+ const response = {
6570
6836
  ok: true,
6571
- canvas_id: result.canvasId,
6572
- title: result.title,
6573
- permalink: result.permalink,
6574
- mimetype: result.mimetype,
6575
- filetype: result.filetype,
6576
- original_byte_length: result.byteLength,
6577
- truncated,
6578
- content
6837
+ canvas_id: target.canvasId,
6838
+ normalized_heading_count: written.normalizedHeadingCount,
6839
+ summary: `Wrote canvas ${target.canvasId}`
6579
6840
  };
6841
+ state.setOperationResult(operationKey, response);
6842
+ return response;
6580
6843
  } catch (error) {
6581
- const message = error instanceof Error ? error.message : "canvas read failed";
6844
+ const message = error instanceof Error ? error.message : "canvas write failed";
6582
6845
  logWarn(
6583
- "slack_canvas_read_failed",
6846
+ "slack_canvas_write_failed",
6584
6847
  {},
6585
6848
  {
6586
- "gen_ai.tool.name": "slackCanvasRead",
6587
- "app.slack.canvas.canvas_id_prefix": canvasId.slice(0, 1)
6849
+ "gen_ai.tool.name": "slackCanvasWrite",
6850
+ "app.slack.canvas.canvas_id_prefix": target.canvasId.slice(0, 1)
6588
6851
  },
6589
6852
  message
6590
6853
  );
6591
6854
  return {
6592
6855
  ok: false,
6593
- canvas_id: canvasId,
6856
+ canvas_id: target.canvasId,
6594
6857
  error: message
6595
6858
  };
6596
6859
  }
@@ -8522,7 +8785,6 @@ function createWriteFileTool() {
8522
8785
  // src/chat/tools/index.ts
8523
8786
  function createToolState(hooks, context) {
8524
8787
  const operationResultCache = /* @__PURE__ */ new Map();
8525
- let turnCreatedCanvasId;
8526
8788
  const artifactState = {
8527
8789
  ...context.artifactState ?? {},
8528
8790
  listColumnMap: {
@@ -8542,11 +8804,6 @@ function createToolState(hooks, context) {
8542
8804
  return {
8543
8805
  artifactState,
8544
8806
  patchArtifactState,
8545
- getCurrentCanvasId: () => artifactState.lastCanvasId,
8546
- getTurnCreatedCanvasId: () => turnCreatedCanvasId,
8547
- setTurnCreatedCanvasId: (canvasId) => {
8548
- turnCreatedCanvasId = canvasId;
8549
- },
8550
8807
  getCurrentListId: () => artifactState.lastListId,
8551
8808
  getOperationResult: (operationKey) => operationResultCache.get(operationKey),
8552
8809
  setOperationResult: (operationKey, result) => {
@@ -8577,7 +8834,8 @@ function createTools(availableSkills, hooks = {}, context) {
8577
8834
  hooks.toolOverrides?.imageGenerate
8578
8835
  ),
8579
8836
  slackCanvasRead: createSlackCanvasReadTool(),
8580
- slackCanvasUpdate: createSlackCanvasUpdateTool(state, context),
8837
+ slackCanvasEdit: createSlackCanvasEditTool(state),
8838
+ slackCanvasWrite: createSlackCanvasWriteTool(state),
8581
8839
  slackThreadRead: createSlackThreadReadTool(context),
8582
8840
  slackUserLookup: createSlackUserLookupTool(),
8583
8841
  slackListCreate: createSlackListCreateTool(state),
@@ -8631,7 +8889,6 @@ function resolveChannelCapabilities(channelId) {
8631
8889
  import fs4 from "fs/promises";
8632
8890
 
8633
8891
  // src/chat/sandbox/egress-policy.ts
8634
- var SANDBOX_EGRESS_PROXY_PATH = "/api/internal/sandbox-egress";
8635
8892
  function matchesSandboxEgressDomain(host, domain) {
8636
8893
  return host.toLowerCase() === domain.toLowerCase();
8637
8894
  }
@@ -8653,31 +8910,24 @@ function resolveSandboxEgressProviderForHost(host) {
8653
8910
  (entry) => entry.domains.some((domain) => matchesSandboxEgressDomain(host, domain))
8654
8911
  )?.provider;
8655
8912
  }
8656
- function proxyUrl(egressId) {
8913
+ function sandboxProxyUrl() {
8657
8914
  const baseUrl = resolveBaseUrl();
8658
8915
  if (!baseUrl) {
8659
- return void 0;
8660
- }
8661
- const url = new URL(
8662
- `${SANDBOX_EGRESS_PROXY_PATH}/${encodeURIComponent(egressId)}`,
8663
- baseUrl
8664
- );
8665
- return url.toString();
8666
- }
8667
- function buildSandboxEgressNetworkPolicy(egressId) {
8668
- const entries = providerEntries();
8669
- if (entries.length === 0) {
8670
- return void 0;
8671
- }
8672
- const forwardURL = proxyUrl(egressId);
8673
- if (!forwardURL) {
8674
8916
  throw new Error(
8675
8917
  "Cannot determine base URL for sandbox credential egress (set JUNIOR_BASE_URL or deploy to Vercel)"
8676
8918
  );
8677
8919
  }
8920
+ return new URL("/", baseUrl).toString();
8921
+ }
8922
+ function buildSandboxEgressNetworkPolicy() {
8678
8923
  const allow = {
8679
8924
  "*": []
8680
8925
  };
8926
+ const entries = providerEntries();
8927
+ if (entries.length === 0) {
8928
+ return { allow };
8929
+ }
8930
+ const forwardURL = sandboxProxyUrl();
8681
8931
  for (const entry of entries) {
8682
8932
  for (const domain of entry.domains) {
8683
8933
  allow[domain] = [{ forwardURL }];
@@ -8685,11 +8935,14 @@ function buildSandboxEgressNetworkPolicy(egressId) {
8685
8935
  }
8686
8936
  return { allow };
8687
8937
  }
8688
- async function resolveSandboxCommandEnvironment() {
8938
+ async function resolveSandboxCommandEnvironment(provider) {
8689
8939
  const env = {};
8690
8940
  for (const plugin of getPluginProviders().sort(
8691
8941
  (left, right) => left.manifest.name.localeCompare(right.manifest.name)
8692
8942
  )) {
8943
+ if (provider && plugin.manifest.name !== provider) {
8944
+ continue;
8945
+ }
8693
8946
  Object.assign(env, resolvePluginCommandEnv(plugin.manifest));
8694
8947
  const credentials = plugin.manifest.credentials;
8695
8948
  if (credentials) {
@@ -10006,7 +10259,6 @@ function createSandboxSessionManager(options) {
10006
10259
  return {
10007
10260
  bash: async (input) => {
10008
10261
  const commandEgressId = sandboxInstance.sandboxEgressId;
10009
- await options?.beforeCommand?.(commandEgressId);
10010
10262
  let timedOut = false;
10011
10263
  let timeoutId;
10012
10264
  let commandFinished = false;
@@ -10016,6 +10268,7 @@ function createSandboxSessionManager(options) {
10016
10268
  }
10017
10269
  commandFinished = true;
10018
10270
  await options?.afterCommand?.(commandEgressId);
10271
+ await refreshNetworkPolicy(sandboxInstance);
10019
10272
  };
10020
10273
  const finishCommandBestEffort = async () => {
10021
10274
  try {
@@ -10033,6 +10286,8 @@ function createSandboxSessionManager(options) {
10033
10286
  }
10034
10287
  };
10035
10288
  try {
10289
+ await options?.beforeCommand?.(commandEgressId);
10290
+ await refreshNetworkPolicy(sandboxInstance);
10036
10291
  const sandboxCommandEnv = await resolveCommandEnv();
10037
10292
  const script = buildNonInteractiveShellScript(input.command, {
10038
10293
  env: { ...sandboxCommandEnv, ...input.env ?? {} },
@@ -10156,14 +10411,14 @@ function createSandboxExecutor(options) {
10156
10411
  let referenceFiles = [];
10157
10412
  const traceContext = options?.traceContext ?? {};
10158
10413
  const credentialEgress = options?.credentialEgress;
10159
- const syncSandboxEgressSession = credentialEgress ? async (egressId) => {
10414
+ const authorizeSandboxEgressForCommand = credentialEgress ? async (egressId) => {
10160
10415
  await upsertSandboxEgressSession({
10161
10416
  egressId,
10162
10417
  requesterId: credentialEgress.requesterId,
10163
10418
  ttlMs: options?.timeoutMs
10164
10419
  });
10165
10420
  } : void 0;
10166
- const clearSandboxEgressSessionForCommand = credentialEgress ? async (egressId) => {
10421
+ const clearSandboxEgressForCommand = credentialEgress ? async (egressId) => {
10167
10422
  await clearSandboxEgressSession(egressId);
10168
10423
  } : void 0;
10169
10424
  const sessionManager = createSandboxSessionManager({
@@ -10171,10 +10426,13 @@ function createSandboxExecutor(options) {
10171
10426
  sandboxDependencyProfileHash: options?.sandboxDependencyProfileHash,
10172
10427
  timeoutMs: options?.timeoutMs,
10173
10428
  traceContext,
10174
- commandEnv: credentialEgress ? async () => await resolveSandboxCommandEnvironment() : void 0,
10429
+ commandEnv: credentialEgress ? async () => {
10430
+ const provider = credentialEgress.activeProvider?.();
10431
+ return provider ? await resolveSandboxCommandEnvironment(provider) : {};
10432
+ } : void 0,
10175
10433
  createNetworkPolicy: credentialEgress ? buildSandboxEgressNetworkPolicy : void 0,
10176
- beforeCommand: syncSandboxEgressSession,
10177
- afterCommand: clearSandboxEgressSessionForCommand,
10434
+ beforeCommand: authorizeSandboxEgressForCommand,
10435
+ afterCommand: clearSandboxEgressForCommand,
10178
10436
  onSandboxAcquired: async (sandbox) => {
10179
10437
  await options?.onSandboxAcquired?.(sandbox);
10180
10438
  }
@@ -10297,7 +10555,16 @@ function createSandboxExecutor(options) {
10297
10555
  "app.sandbox.path.length": filePath.length
10298
10556
  },
10299
10557
  async () => {
10300
- const response = await executeReadFile({ path: filePath });
10558
+ let response;
10559
+ try {
10560
+ response = await executeReadFile({ path: filePath });
10561
+ } catch (error) {
10562
+ if (isMissingPathError(error)) {
10563
+ setSpanStatus("ok");
10564
+ return missingFileResult(filePath);
10565
+ }
10566
+ throw error;
10567
+ }
10301
10568
  const content = String(response.content ?? "");
10302
10569
  setSpanAttributes({
10303
10570
  "app.sandbox.read.bytes": Buffer.byteLength(content, "utf8"),
@@ -12098,7 +12365,8 @@ async function generateAssistantReply(messageText, context = {}) {
12098
12365
  sandboxDependencyProfileHash: context.sandbox?.sandboxDependencyProfileHash,
12099
12366
  traceContext: spanContext,
12100
12367
  credentialEgress: requesterId ? {
12101
- requesterId
12368
+ requesterId,
12369
+ activeProvider: () => skillSandbox.getActiveSkill()?.pluginProvider
12102
12370
  } : void 0,
12103
12371
  onSandboxAcquired: async (sandbox2) => {
12104
12372
  lastKnownSandboxId = sandbox2.sandboxId;
@@ -13480,102 +13748,343 @@ async function postSlackApiReplyPosts(args) {
13480
13748
  return lastPostedMessageTs;
13481
13749
  }
13482
13750
 
13483
- // src/chat/runtime/slack-resume.ts
13484
- function resolveReplyTimeoutMs(explicitTimeoutMs) {
13485
- if (typeof explicitTimeoutMs === "number" && explicitTimeoutMs > 0) {
13486
- return explicitTimeoutMs;
13487
- }
13488
- const raw = process.env.EVAL_AGENT_REPLY_TIMEOUT_MS?.trim();
13489
- if (!raw) {
13751
+ // src/chat/slack/errors.ts
13752
+ function getSlackApiErrorCode(error) {
13753
+ if (!error || typeof error !== "object") {
13490
13754
  return void 0;
13491
13755
  }
13492
- const parsed = Number.parseInt(raw, 10);
13493
- return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
13756
+ const candidate = error;
13757
+ if (typeof candidate.data?.error === "string" && candidate.data.error.trim().length > 0) {
13758
+ return candidate.data.error;
13759
+ }
13760
+ if (typeof candidate.code === "string" && candidate.code.trim().length > 0) {
13761
+ return candidate.code;
13762
+ }
13763
+ return void 0;
13494
13764
  }
13495
- async function postSlackMessageBestEffort(channelId, threadTs, text) {
13496
- try {
13497
- await postSlackMessage({
13498
- channelId,
13499
- threadTs,
13500
- text
13501
- });
13502
- } catch {
13765
+ function getSlackErrorObservabilityAttributes(error) {
13766
+ if (!error || typeof error !== "object") {
13767
+ return {};
13768
+ }
13769
+ const candidate = error;
13770
+ const attributes = {};
13771
+ if (typeof candidate.code === "string" && candidate.code.trim().length > 0) {
13772
+ attributes["app.slack.error_code"] = candidate.code;
13773
+ }
13774
+ if (typeof candidate.data?.error === "string" && candidate.data.error.trim().length > 0) {
13775
+ attributes["app.slack.api_error"] = candidate.data.error;
13776
+ }
13777
+ const requestId = getHeaderString(candidate.headers, "x-slack-req-id");
13778
+ if (requestId) {
13779
+ attributes["app.slack.request_id"] = requestId;
13780
+ }
13781
+ if (typeof candidate.statusCode === "number" && Number.isFinite(candidate.statusCode)) {
13782
+ attributes["http.response.status_code"] = candidate.statusCode;
13503
13783
  }
13784
+ return attributes;
13504
13785
  }
13505
- function createReadOnlyConfigService(values) {
13506
- const entries = Object.entries(values).map(([key, value]) => ({
13507
- key,
13508
- value,
13509
- scope: "conversation",
13510
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
13511
- }));
13512
- return {
13513
- get: async (key) => entries.find((entry) => entry.key === key),
13514
- set: async () => {
13515
- throw new Error("Read-only configuration in resumed context");
13516
- },
13517
- unset: async () => false,
13518
- list: async ({ prefix } = {}) => entries.filter((entry) => !prefix || entry.key.startsWith(prefix)),
13519
- resolve: async (key) => values[key],
13520
- resolveValues: async ({ keys, prefix } = {}) => {
13521
- const filtered = {};
13522
- for (const [key, value] of Object.entries(values)) {
13523
- if (prefix && !key.startsWith(prefix)) continue;
13524
- if (keys && !keys.includes(key)) continue;
13525
- filtered[key] = value;
13526
- }
13527
- return filtered;
13528
- }
13529
- };
13786
+ function isSlackTitlePermissionError(error) {
13787
+ const code = getSlackApiErrorCode(error);
13788
+ return code === "no_permission" || code === "missing_scope" || code === "not_allowed_token_type";
13530
13789
  }
13531
- var ResumeTurnBusyError = class extends Error {
13532
- constructor(lockKey) {
13533
- super(`A turn already owns resume lock "${lockKey}"`);
13534
- this.name = "ResumeTurnBusyError";
13790
+
13791
+ // src/chat/slack/context.ts
13792
+ function toTrimmedSlackString(value) {
13793
+ const normalized = toOptionalString(value);
13794
+ return normalized?.trim() || void 0;
13795
+ }
13796
+ function parseSlackThreadId(threadId) {
13797
+ const normalizedThreadId = toTrimmedSlackString(threadId);
13798
+ if (!normalizedThreadId) {
13799
+ return void 0;
13535
13800
  }
13536
- };
13537
- function getDefaultLockKey(channelId, threadTs) {
13538
- return `slack:${channelId}:${threadTs}`;
13801
+ const parts = normalizedThreadId.split(":");
13802
+ if (parts.length !== 3 || parts[0] !== "slack") {
13803
+ return void 0;
13804
+ }
13805
+ const channelId = toTrimmedSlackString(parts[1]);
13806
+ const threadTs = toTrimmedSlackString(parts[2]);
13807
+ if (!channelId || !threadTs) {
13808
+ return void 0;
13809
+ }
13810
+ return { channelId, threadTs };
13539
13811
  }
13540
- function getResumeLogContext(args, lockKey) {
13541
- return {
13542
- conversationId: args.replyContext?.correlation?.conversationId ?? lockKey,
13543
- slackThreadId: args.replyContext?.correlation?.threadId ?? lockKey,
13544
- slackUserId: args.replyContext?.requester?.userId ?? args.replyContext?.correlation?.requesterId,
13545
- slackUserName: args.replyContext?.requester?.userName,
13546
- slackChannelId: args.channelId,
13547
- runId: args.replyContext?.correlation?.runId,
13548
- assistantUserName: botConfig.userName,
13549
- modelId: botConfig.modelId
13550
- };
13812
+ function resolveSlackChannelIdFromThreadId(threadId) {
13813
+ return parseSlackThreadId(threadId)?.channelId;
13551
13814
  }
13552
- async function postResumeFailureReply(args) {
13553
- try {
13554
- await postSlackMessage({
13555
- channelId: args.channelId,
13556
- threadTs: args.threadTs,
13557
- text: buildTurnFailureResponse(args.eventId)
13558
- });
13559
- } catch (error) {
13560
- logException(
13561
- error,
13562
- "slack_resume_failure_reply_post_failed",
13563
- args.logContext,
13564
- {
13565
- "app.error.original_event_id": args.eventId
13566
- },
13567
- "Failed to post resumed turn failure reply"
13815
+ function resolveSlackChannelIdFromMessage(message) {
13816
+ const messageChannelId = toTrimmedSlackString(
13817
+ message.channelId
13818
+ );
13819
+ if (messageChannelId) {
13820
+ return messageChannelId;
13821
+ }
13822
+ const raw = message.raw;
13823
+ if (raw && typeof raw === "object") {
13824
+ const rawChannel = toTrimmedSlackString(
13825
+ raw.channel
13568
13826
  );
13569
- throw error;
13827
+ if (rawChannel) {
13828
+ return rawChannel;
13829
+ }
13570
13830
  }
13831
+ const threadId = toTrimmedSlackString(
13832
+ message.threadId
13833
+ );
13834
+ return resolveSlackChannelIdFromThreadId(threadId);
13571
13835
  }
13572
- async function postTurnContinuationNoticeBestEffort(args) {
13573
- try {
13574
- await postSlackMessage({
13575
- channelId: args.resumeArgs.channelId,
13576
- threadTs: args.resumeArgs.threadTs,
13577
- text: buildTurnContinuationResponse()
13578
- });
13836
+
13837
+ // src/chat/runtime/thread-context.ts
13838
+ function escapeRegExp2(value) {
13839
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
13840
+ }
13841
+ function stripLeadingBotMention(text, options = {}) {
13842
+ if (!text.trim()) return text;
13843
+ let next = text;
13844
+ if (options.stripLeadingSlackMentionToken) {
13845
+ next = next.replace(/^\s*<@[^>]+>[\s,:-]*/, "").trim();
13846
+ }
13847
+ const mentionByNameRe = new RegExp(
13848
+ `^\\s*@${escapeRegExp2(botConfig.userName)}\\b[\\s,:-]*`,
13849
+ "i"
13850
+ );
13851
+ next = next.replace(mentionByNameRe, "").trim();
13852
+ const mentionByLabeledEntityRe = new RegExp(
13853
+ `^\\s*<@[^>|]+\\|${escapeRegExp2(botConfig.userName)}>[\\s,:-]*`,
13854
+ "i"
13855
+ );
13856
+ next = next.replace(mentionByLabeledEntityRe, "").trim();
13857
+ return next;
13858
+ }
13859
+ function getThreadId(thread, _message) {
13860
+ return toOptionalString(thread.id);
13861
+ }
13862
+ function getRunId(thread, message) {
13863
+ return toOptionalString(thread.runId) ?? toOptionalString(message.runId);
13864
+ }
13865
+ function getChannelId(thread, message) {
13866
+ return resolveSlackChannelIdFromThreadId(toOptionalString(thread.id)) ?? normalizeSlackConversationId(toOptionalString(thread.channelId)) ?? resolveSlackChannelIdFromMessage(message);
13867
+ }
13868
+ function getThreadTs(threadId) {
13869
+ return parseSlackThreadId(threadId)?.threadTs;
13870
+ }
13871
+ function getAssistantThreadContext(message) {
13872
+ const raw = message.raw;
13873
+ const rawRecord = raw && typeof raw === "object" ? raw : void 0;
13874
+ const channelId = toOptionalString(rawRecord?.channel);
13875
+ if (channelId) {
13876
+ const rawThreadTs = toOptionalString(rawRecord?.thread_ts);
13877
+ const threadTs = isDmChannel(channelId) ? rawThreadTs : rawThreadTs ?? toOptionalString(rawRecord?.ts);
13878
+ if (threadTs) {
13879
+ return { channelId, threadTs };
13880
+ }
13881
+ }
13882
+ const parsedThreadId = parseSlackThreadId(
13883
+ toOptionalString(message.threadId)
13884
+ );
13885
+ if (!parsedThreadId || isDmChannel(parsedThreadId.channelId)) {
13886
+ return void 0;
13887
+ }
13888
+ return parsedThreadId;
13889
+ }
13890
+ function getMessageTs(message) {
13891
+ const directTs = toOptionalString(
13892
+ message.ts
13893
+ );
13894
+ if (directTs) {
13895
+ return directTs;
13896
+ }
13897
+ const raw = message.raw;
13898
+ if (!raw || typeof raw !== "object") {
13899
+ return void 0;
13900
+ }
13901
+ const rawRecord = raw;
13902
+ return toOptionalString(rawRecord.ts) ?? toOptionalString(rawRecord.event_ts) ?? toOptionalString(rawRecord.message?.ts);
13903
+ }
13904
+
13905
+ // src/chat/runtime/processing-reaction.ts
13906
+ var PROCESSING_REACTION_EMOJI = "eyes";
13907
+ var noProcessingReaction = {
13908
+ keep: () => void 0,
13909
+ stop: async () => void 0
13910
+ };
13911
+ function isProcessingReactionEmoji(value) {
13912
+ return typeof value === "string" && normalizeSlackEmojiName(value) === PROCESSING_REACTION_EMOJI;
13913
+ }
13914
+ function shouldKeepProcessingReactionForToolInvocation(input) {
13915
+ return input.toolName === "slackMessageAddReaction" && isProcessingReactionEmoji(input.params.emoji);
13916
+ }
13917
+ async function startSlackProcessingReaction(args) {
13918
+ if (args.message.author.isMe) {
13919
+ return noProcessingReaction;
13920
+ }
13921
+ const channelId = getChannelId(args.thread, args.message);
13922
+ const messageTs = getMessageTs(args.message);
13923
+ if (!channelId || !messageTs) {
13924
+ return noProcessingReaction;
13925
+ }
13926
+ return startSlackProcessingReactionForMessage({
13927
+ channelId,
13928
+ timestamp: messageTs,
13929
+ logException: args.logException,
13930
+ logContext: args.logContext
13931
+ });
13932
+ }
13933
+ async function startSlackProcessingReactionForMessage(args) {
13934
+ try {
13935
+ await addReactionToMessage({
13936
+ channelId: args.channelId,
13937
+ timestamp: args.timestamp,
13938
+ emoji: PROCESSING_REACTION_EMOJI
13939
+ });
13940
+ } catch (error) {
13941
+ args.logException(
13942
+ error,
13943
+ "slack_processing_reaction_add_failed",
13944
+ args.logContext,
13945
+ {
13946
+ "app.slack.action": "reactions.add",
13947
+ "messaging.message.id": args.timestamp,
13948
+ ...getSlackErrorObservabilityAttributes(error)
13949
+ },
13950
+ "Failed to add Slack processing reaction"
13951
+ );
13952
+ return noProcessingReaction;
13953
+ }
13954
+ let shouldRemove = true;
13955
+ return {
13956
+ keep: () => {
13957
+ shouldRemove = false;
13958
+ },
13959
+ stop: async () => {
13960
+ if (!shouldRemove) {
13961
+ return;
13962
+ }
13963
+ try {
13964
+ await removeReactionFromMessage({
13965
+ channelId: args.channelId,
13966
+ timestamp: args.timestamp,
13967
+ emoji: PROCESSING_REACTION_EMOJI
13968
+ });
13969
+ } catch (error) {
13970
+ args.logException(
13971
+ error,
13972
+ "slack_processing_reaction_remove_failed",
13973
+ args.logContext,
13974
+ {
13975
+ "app.slack.action": "reactions.remove",
13976
+ "messaging.message.id": args.timestamp,
13977
+ ...getSlackErrorObservabilityAttributes(error)
13978
+ },
13979
+ "Failed to remove Slack processing reaction"
13980
+ );
13981
+ }
13982
+ }
13983
+ };
13984
+ }
13985
+
13986
+ // src/chat/services/auth-pause-response.ts
13987
+ var AUTH_PAUSE_RESPONSE = "I need authorization to continue. Check your private link to connect.";
13988
+ function buildAuthPauseResponse() {
13989
+ return AUTH_PAUSE_RESPONSE;
13990
+ }
13991
+
13992
+ // src/chat/runtime/slack-resume.ts
13993
+ function resolveReplyTimeoutMs(explicitTimeoutMs) {
13994
+ if (typeof explicitTimeoutMs === "number" && explicitTimeoutMs > 0) {
13995
+ return explicitTimeoutMs;
13996
+ }
13997
+ const raw = process.env.EVAL_AGENT_REPLY_TIMEOUT_MS?.trim();
13998
+ if (!raw) {
13999
+ return void 0;
14000
+ }
14001
+ const parsed = Number.parseInt(raw, 10);
14002
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
14003
+ }
14004
+ async function postSlackMessageBestEffort(channelId, threadTs, text) {
14005
+ try {
14006
+ await postSlackMessage({
14007
+ channelId,
14008
+ threadTs,
14009
+ text
14010
+ });
14011
+ } catch {
14012
+ }
14013
+ }
14014
+ function createReadOnlyConfigService(values) {
14015
+ const entries = Object.entries(values).map(([key, value]) => ({
14016
+ key,
14017
+ value,
14018
+ scope: "conversation",
14019
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
14020
+ }));
14021
+ return {
14022
+ get: async (key) => entries.find((entry) => entry.key === key),
14023
+ set: async () => {
14024
+ throw new Error("Read-only configuration in resumed context");
14025
+ },
14026
+ unset: async () => false,
14027
+ list: async ({ prefix } = {}) => entries.filter((entry) => !prefix || entry.key.startsWith(prefix)),
14028
+ resolve: async (key) => values[key],
14029
+ resolveValues: async ({ keys, prefix } = {}) => {
14030
+ const filtered = {};
14031
+ for (const [key, value] of Object.entries(values)) {
14032
+ if (prefix && !key.startsWith(prefix)) continue;
14033
+ if (keys && !keys.includes(key)) continue;
14034
+ filtered[key] = value;
14035
+ }
14036
+ return filtered;
14037
+ }
14038
+ };
14039
+ }
14040
+ var ResumeTurnBusyError = class extends Error {
14041
+ constructor(lockKey) {
14042
+ super(`A turn already owns resume lock "${lockKey}"`);
14043
+ this.name = "ResumeTurnBusyError";
14044
+ }
14045
+ };
14046
+ function getDefaultLockKey(channelId, threadTs) {
14047
+ return `slack:${channelId}:${threadTs}`;
14048
+ }
14049
+ function getResumeLogContext(args, lockKey) {
14050
+ return {
14051
+ conversationId: args.replyContext?.correlation?.conversationId ?? lockKey,
14052
+ slackThreadId: args.replyContext?.correlation?.threadId ?? lockKey,
14053
+ slackUserId: args.replyContext?.requester?.userId ?? args.replyContext?.correlation?.requesterId,
14054
+ slackUserName: args.replyContext?.requester?.userName,
14055
+ slackChannelId: args.channelId,
14056
+ runId: args.replyContext?.correlation?.runId,
14057
+ assistantUserName: botConfig.userName,
14058
+ modelId: botConfig.modelId
14059
+ };
14060
+ }
14061
+ async function postResumeFailureReply(args) {
14062
+ try {
14063
+ await postSlackMessage({
14064
+ channelId: args.channelId,
14065
+ threadTs: args.threadTs,
14066
+ text: buildTurnFailureResponse(args.eventId)
14067
+ });
14068
+ } catch (error) {
14069
+ logException(
14070
+ error,
14071
+ "slack_resume_failure_reply_post_failed",
14072
+ args.logContext,
14073
+ {
14074
+ "app.error.original_event_id": args.eventId
14075
+ },
14076
+ "Failed to post resumed turn failure reply"
14077
+ );
14078
+ throw error;
14079
+ }
14080
+ }
14081
+ async function postTurnContinuationNoticeBestEffort(args) {
14082
+ try {
14083
+ await postSlackMessage({
14084
+ channelId: args.resumeArgs.channelId,
14085
+ threadTs: args.resumeArgs.threadTs,
14086
+ text: buildTurnContinuationResponse()
14087
+ });
13579
14088
  } catch (error) {
13580
14089
  logException(
13581
14090
  error,
@@ -13655,10 +14164,19 @@ async function resumeSlackTurn(args) {
13655
14164
  channelId: args.channelId,
13656
14165
  threadTs: args.threadTs
13657
14166
  });
14167
+ let processingReaction;
13658
14168
  let deferredPauseKind;
13659
14169
  let deferredPauseHandler;
13660
14170
  let deferredFailureHandler;
13661
14171
  try {
14172
+ if (args.messageTs) {
14173
+ processingReaction = await startSlackProcessingReactionForMessage({
14174
+ channelId: args.channelId,
14175
+ timestamp: args.messageTs,
14176
+ logException,
14177
+ logContext: { ...getResumeLogContext(args, lockKey) }
14178
+ });
14179
+ }
13662
14180
  if (args.initialText) {
13663
14181
  await postSlackMessageBestEffort(
13664
14182
  args.channelId,
@@ -13730,11 +14248,19 @@ async function resumeSlackTurn(args) {
13730
14248
  };
13731
14249
  }
13732
14250
  } finally {
14251
+ await processingReaction?.stop();
13733
14252
  await stateAdapter.releaseLock(lock);
13734
14253
  }
13735
14254
  if (deferredPauseHandler) {
13736
14255
  try {
13737
14256
  await deferredPauseHandler();
14257
+ if (deferredPauseKind === "auth") {
14258
+ await postSlackMessageBestEffort(
14259
+ args.channelId,
14260
+ args.threadTs,
14261
+ buildAuthPauseResponse()
14262
+ );
14263
+ }
13738
14264
  if (deferredPauseKind === "timeout") {
13739
14265
  await postTurnContinuationNoticeBestEffort({
13740
14266
  lockKey,
@@ -13762,6 +14288,7 @@ async function resumeAuthorizedRequest(args) {
13762
14288
  messageText: args.messageText,
13763
14289
  channelId: args.channelId,
13764
14290
  threadTs: args.threadTs,
14291
+ messageTs: args.messageTs,
13765
14292
  replyContext: args.replyContext,
13766
14293
  lockKey: args.lockKey,
13767
14294
  initialText: args.connectedText,
@@ -14084,6 +14611,7 @@ async function resumeAuthorizedMcpTurn(args) {
14084
14611
  messageText: userMessage.text,
14085
14612
  channelId: authSession.channelId,
14086
14613
  threadTs: authSession.threadTs,
14614
+ messageTs: getTurnUserSlackMessageTs(userMessage),
14087
14615
  lockKey: authSession.conversationId,
14088
14616
  connectedText: "",
14089
14617
  replyContext: {
@@ -14525,6 +15053,7 @@ async function resumeCheckpointedOAuthTurn(stored) {
14525
15053
  messageText: stored.pendingMessage ?? userMessage.text,
14526
15054
  channelId: stored.channelId,
14527
15055
  threadTs: stored.threadTs,
15056
+ messageTs: getTurnUserSlackMessageTs(userMessage),
14528
15057
  lockKey: stored.resumeConversationId,
14529
15058
  initialText: "",
14530
15059
  replyContext: {
@@ -14617,14 +15146,15 @@ async function resumePendingOAuthMessage(stored) {
14617
15146
  const conversation = coerceThreadConversationState(
14618
15147
  await getPersistedThreadState(threadId)
14619
15148
  );
14620
- const latestUserMessageId = [...conversation.messages].reverse().find((message) => message.role === "user")?.id;
15149
+ const latestUserMessage = [...conversation.messages].reverse().find((message) => message.role === "user");
14621
15150
  const conversationContext = buildConversationContext(conversation, {
14622
- excludeMessageId: latestUserMessageId
15151
+ excludeMessageId: latestUserMessage?.id
14623
15152
  });
14624
15153
  await resumeAuthorizedRequest({
14625
15154
  messageText: stored.pendingMessage,
14626
15155
  channelId: stored.channelId,
14627
15156
  threadTs: stored.threadTs,
15157
+ messageTs: getTurnUserSlackMessageTs(latestUserMessage),
14628
15158
  connectedText: "",
14629
15159
  replyContext: {
14630
15160
  requester: { userId: stored.userId },
@@ -14881,12 +15411,7 @@ async function getJwks(issuer) {
14881
15411
  });
14882
15412
  return jwks;
14883
15413
  }
14884
- function validateSandboxClaim(payload, egressId) {
14885
- if (payload.sandbox_id !== egressId) {
14886
- throw new Error("Vercel OIDC token belongs to a different sandbox");
14887
- }
14888
- }
14889
- async function verifyVercelSandboxOidcToken(token, egressId) {
15414
+ async function verifyVercelSandboxOidcToken(token) {
14890
15415
  const unverified = decodeJwt(token);
14891
15416
  if (typeof unverified.iss !== "string") {
14892
15417
  throw new Error("Vercel OIDC token did not include an issuer");
@@ -14895,7 +15420,6 @@ async function verifyVercelSandboxOidcToken(token, egressId) {
14895
15420
  const verified = await jwtVerify(token, jwks, {
14896
15421
  issuer: unverified.iss
14897
15422
  });
14898
- validateSandboxClaim(verified.payload, egressId);
14899
15423
  return verified.payload;
14900
15424
  }
14901
15425
 
@@ -14904,7 +15428,7 @@ var OIDC_TOKEN_HEADER = "vercel-sandbox-oidc-token";
14904
15428
  var FORWARDED_HOST_HEADER = "vercel-forwarded-host";
14905
15429
  var FORWARDED_SCHEME_HEADER = "vercel-forwarded-scheme";
14906
15430
  var FORWARDED_PORT_HEADER = "vercel-forwarded-port";
14907
- var ROUTE_PREFIX = "/api/internal/sandbox-egress";
15431
+ var FORWARDED_PATH_HEADER = "vercel-forwarded-path";
14908
15432
  var HOP_BY_HOP_HEADERS = /* @__PURE__ */ new Set([
14909
15433
  "connection",
14910
15434
  "host",
@@ -14920,7 +15444,8 @@ var PROXY_ONLY_HEADERS = /* @__PURE__ */ new Set([
14920
15444
  OIDC_TOKEN_HEADER,
14921
15445
  FORWARDED_HOST_HEADER,
14922
15446
  FORWARDED_SCHEME_HEADER,
14923
- FORWARDED_PORT_HEADER
15447
+ FORWARDED_PORT_HEADER,
15448
+ FORWARDED_PATH_HEADER
14924
15449
  ]);
14925
15450
  var DECODED_RESPONSE_HEADERS = /* @__PURE__ */ new Set([
14926
15451
  "content-encoding",
@@ -14930,6 +15455,52 @@ var AUTH_REJECTION_STATUS = /* @__PURE__ */ new Set([401, 403]);
14930
15455
  function jsonError(message, status) {
14931
15456
  return Response.json({ error: message }, { status });
14932
15457
  }
15458
+ function shouldLogSandboxEgressInfo() {
15459
+ const environment = (process.env.SENTRY_ENVIRONMENT ?? process.env.VERCEL_ENV ?? process.env.NODE_ENV ?? "").trim().toLowerCase();
15460
+ return environment !== "production";
15461
+ }
15462
+ function egressAttributes(input) {
15463
+ return {
15464
+ ...input.egressId ? { "app.sandbox.egress_id": input.egressId } : {},
15465
+ ...input.provider ? { "app.credential.provider": input.provider } : {},
15466
+ ...input.host ? { "server.address": input.host } : {},
15467
+ ...input.method ? { "http.request.method": input.method } : {},
15468
+ ...input.path ? { "url.path": input.path } : {},
15469
+ ...input.status ? { "http.response.status_code": input.status } : {}
15470
+ };
15471
+ }
15472
+ function routingAttributes(request, upstreamUrl) {
15473
+ const proxyUrl = new URL(request.url);
15474
+ const attributes = {
15475
+ "app.sandbox.egress.proxy_path": proxyUrl.pathname
15476
+ };
15477
+ if (upstreamUrl) {
15478
+ attributes["app.sandbox.egress.upstream_path"] = upstreamUrl.pathname;
15479
+ }
15480
+ return attributes;
15481
+ }
15482
+ function logSandboxEgressUpstreamRequest(input) {
15483
+ if (!shouldLogSandboxEgressInfo()) {
15484
+ return;
15485
+ }
15486
+ logInfo(
15487
+ "sandbox_egress_upstream_request",
15488
+ {},
15489
+ {
15490
+ ...egressAttributes({
15491
+ egressId: input.egressId,
15492
+ host: input.upstreamUrl.hostname,
15493
+ method: input.request.method,
15494
+ path: input.upstreamUrl.pathname,
15495
+ provider: input.provider,
15496
+ status: input.upstream.status
15497
+ }),
15498
+ ...routingAttributes(input.request, input.upstreamUrl),
15499
+ "app.sandbox.egress.upstream_ok": input.upstream.ok
15500
+ },
15501
+ `Sandbox egress ${input.request.method} ${input.upstreamUrl.hostname}${input.upstreamUrl.pathname} -> ${input.upstream.status}`
15502
+ );
15503
+ }
14933
15504
  function normalizeHost(value) {
14934
15505
  const trimmed = value.trim().toLowerCase();
14935
15506
  if (!trimmed || trimmed.includes("/") || trimmed.includes("\\") || trimmed.includes(":")) {
@@ -14951,18 +15522,27 @@ function normalizePort(value) {
14951
15522
  const port = Number.parseInt(trimmed, 10);
14952
15523
  return port >= 1 && port <= 65535 ? trimmed : void 0;
14953
15524
  }
14954
- function upstreamPath(request, egressId) {
14955
- const url = new URL(request.url);
14956
- const prefix = `${ROUTE_PREFIX}/${encodeURIComponent(egressId)}`;
14957
- if (url.pathname === prefix) {
14958
- return `/${url.search}`;
14959
- }
14960
- if (url.pathname.startsWith(`${prefix}/`)) {
14961
- return `${url.pathname.slice(prefix.length)}${url.search}`;
15525
+ function sandboxIdFromPayload(payload) {
15526
+ return typeof payload.sandbox_id === "string" ? payload.sandbox_id : void 0;
15527
+ }
15528
+ function upstreamPath(request) {
15529
+ const forwardedPath = request.headers.get(FORWARDED_PATH_HEADER);
15530
+ if (forwardedPath?.trim()) {
15531
+ const path11 = forwardedPath.trim();
15532
+ if (!path11.startsWith("/") || path11.startsWith("//") || path11.includes("#") || /[\r\n]/.test(path11)) {
15533
+ return { ok: false, error: "Invalid forwarded path" };
15534
+ }
15535
+ try {
15536
+ const url2 = new URL(path11, "https://sandbox-forwarded.local");
15537
+ return { ok: true, path: `${url2.pathname}${url2.search}` };
15538
+ } catch {
15539
+ return { ok: false, error: "Invalid forwarded path" };
15540
+ }
14962
15541
  }
14963
- return void 0;
15542
+ const url = new URL(request.url);
15543
+ return { ok: true, path: `${url.pathname}${url.search}` };
14964
15544
  }
14965
- function buildUpstreamUrl(request, egressId) {
15545
+ function buildUpstreamUrl(request) {
14966
15546
  const forwardedHost = request.headers.get(FORWARDED_HOST_HEADER);
14967
15547
  if (!forwardedHost?.trim()) {
14968
15548
  return { ok: false, error: "Missing forwarded host" };
@@ -14984,12 +15564,14 @@ function buildUpstreamUrl(request, egressId) {
14984
15564
  if (forwardedPort && !port) {
14985
15565
  return { ok: false, error: "Invalid forwarded port" };
14986
15566
  }
14987
- const path11 = upstreamPath(request, egressId);
14988
- if (!path11) {
14989
- return { ok: false, error: "Invalid egress route" };
15567
+ const path11 = upstreamPath(request);
15568
+ if (!path11.ok) {
15569
+ return { ok: false, error: path11.error };
14990
15570
  }
14991
15571
  try {
14992
- const url = new URL(`${scheme}://${host}${port ? `:${port}` : ""}${path11}`);
15572
+ const url = new URL(
15573
+ `${scheme}://${host}${port ? `:${port}` : ""}${path11.path}`
15574
+ );
14993
15575
  return { ok: true, url };
14994
15576
  } catch {
14995
15577
  return { ok: false, error: "Invalid forwarded URL" };
@@ -15063,15 +15645,20 @@ function hasTransformForHost(lease, host) {
15063
15645
  (transform) => matchesSandboxEgressDomain(host, transform.domain)
15064
15646
  );
15065
15647
  }
15066
- async function proxySandboxEgressRequest(request, egressId, deps = {}) {
15648
+ function isSandboxEgressForwardedRequest(request) {
15649
+ return Boolean(
15650
+ request.headers.get(OIDC_TOKEN_HEADER)?.trim() && request.headers.get(FORWARDED_HOST_HEADER)?.trim() && request.headers.get(FORWARDED_SCHEME_HEADER)?.trim()
15651
+ );
15652
+ }
15653
+ async function proxySandboxEgressRequest(request, deps = {}) {
15067
15654
  const oidcToken = request.headers.get(OIDC_TOKEN_HEADER)?.trim();
15068
15655
  if (!oidcToken) {
15069
15656
  return jsonError("Missing Vercel Sandbox OIDC token", 401);
15070
15657
  }
15658
+ let oidcPayload;
15071
15659
  try {
15072
- await (deps.verifyOidc ?? verifyVercelSandboxOidcToken)(
15073
- oidcToken,
15074
- egressId
15660
+ oidcPayload = await (deps.verifyOidc ?? verifyVercelSandboxOidcToken)(
15661
+ oidcToken
15075
15662
  );
15076
15663
  } catch (error) {
15077
15664
  logWarn(
@@ -15084,24 +15671,101 @@ async function proxySandboxEgressRequest(request, egressId, deps = {}) {
15084
15671
  );
15085
15672
  return jsonError("Invalid Vercel Sandbox OIDC token", 401);
15086
15673
  }
15087
- const upstreamResult = buildUpstreamUrl(request, egressId);
15674
+ const activeEgressId = sandboxIdFromPayload(oidcPayload);
15675
+ if (!activeEgressId) {
15676
+ logWarn(
15677
+ "sandbox_egress_oidc_session_missing",
15678
+ {},
15679
+ {
15680
+ "http.request.method": request.method,
15681
+ "url.path": new URL(request.url).pathname
15682
+ },
15683
+ "Sandbox egress OIDC payload did not include a VM session id"
15684
+ );
15685
+ return jsonError(
15686
+ "Vercel Sandbox OIDC token did not include sandbox_id",
15687
+ 401
15688
+ );
15689
+ }
15690
+ const upstreamResult = buildUpstreamUrl(request);
15088
15691
  if (!upstreamResult.ok) {
15692
+ logWarn(
15693
+ "sandbox_egress_upstream_url_invalid",
15694
+ {},
15695
+ {
15696
+ ...egressAttributes({
15697
+ egressId: activeEgressId,
15698
+ method: request.method,
15699
+ path: new URL(request.url).pathname,
15700
+ status: 400
15701
+ }),
15702
+ ...routingAttributes(request)
15703
+ },
15704
+ "Sandbox egress forwarded request had invalid upstream routing headers"
15705
+ );
15089
15706
  return jsonError(upstreamResult.error, 400);
15090
15707
  }
15091
15708
  const upstreamUrl = upstreamResult.url;
15092
15709
  const provider = resolveSandboxEgressProviderForHost(upstreamUrl.hostname);
15093
15710
  if (!provider) {
15711
+ logWarn(
15712
+ "sandbox_egress_provider_unresolved",
15713
+ {},
15714
+ {
15715
+ ...egressAttributes({
15716
+ egressId: activeEgressId,
15717
+ host: upstreamUrl.hostname,
15718
+ method: request.method,
15719
+ path: upstreamUrl.pathname,
15720
+ status: 403
15721
+ }),
15722
+ ...routingAttributes(request, upstreamUrl)
15723
+ },
15724
+ "Sandbox egress forwarded host is not owned by any credential provider"
15725
+ );
15094
15726
  return jsonError("No provider owns forwarded host", 403);
15095
15727
  }
15096
- const session = await getSandboxEgressSession(egressId);
15728
+ const session = await getSandboxEgressSession(activeEgressId);
15097
15729
  if (!session) {
15730
+ logWarn(
15731
+ "sandbox_egress_session_unauthorized",
15732
+ {},
15733
+ {
15734
+ ...egressAttributes({
15735
+ egressId: activeEgressId,
15736
+ host: upstreamUrl.hostname,
15737
+ method: request.method,
15738
+ path: upstreamUrl.pathname,
15739
+ provider,
15740
+ status: 403
15741
+ }),
15742
+ ...routingAttributes(request, upstreamUrl)
15743
+ },
15744
+ "Sandbox egress VM session is not authorized for requester credentials"
15745
+ );
15098
15746
  return jsonError("Sandbox egress session is not authorized", 403);
15099
15747
  }
15100
15748
  let lease;
15101
15749
  try {
15102
- lease = await credentialLease(egressId, provider, session);
15750
+ lease = await credentialLease(activeEgressId, provider, session);
15103
15751
  } catch (error) {
15104
15752
  if (error instanceof CredentialUnavailableError) {
15753
+ logWarn(
15754
+ "sandbox_egress_credential_unavailable",
15755
+ {},
15756
+ {
15757
+ ...egressAttributes({
15758
+ egressId: activeEgressId,
15759
+ host: upstreamUrl.hostname,
15760
+ method: request.method,
15761
+ path: upstreamUrl.pathname,
15762
+ provider,
15763
+ status: 401
15764
+ }),
15765
+ ...routingAttributes(request, upstreamUrl)
15766
+ },
15767
+ "Sandbox egress provider credential is unavailable"
15768
+ );
15105
15769
  return new Response(
15106
15770
  `junior-auth-required provider=${error.provider} 401 unauthorized
15107
15771
  ${error.message}`,
@@ -15114,85 +15778,94 @@ ${error.message}`,
15114
15778
  throw error;
15115
15779
  }
15116
15780
  if (!hasTransformForHost(lease, upstreamUrl.hostname)) {
15117
- return jsonError("Credential lease does not cover forwarded host", 403);
15118
- }
15119
- const body = await requestBodyBytes(request);
15120
- const upstream = await (deps.fetch ?? fetch)(upstreamUrl, {
15121
- method: request.method,
15122
- headers: requestHeaders(request, lease, upstreamUrl.hostname),
15123
- ...body ? { body } : {},
15124
- redirect: "manual"
15125
- });
15126
- if (AUTH_REJECTION_STATUS.has(upstream.status)) {
15127
15781
  logWarn(
15128
- "sandbox_egress_upstream_auth_rejected",
15782
+ "sandbox_egress_transform_missing",
15129
15783
  {},
15130
15784
  {
15131
- "app.credential.provider": provider,
15132
- "http.request.method": request.method,
15133
- "http.response.status_code": upstream.status,
15134
- "server.address": upstreamUrl.hostname
15785
+ ...egressAttributes({
15786
+ egressId: activeEgressId,
15787
+ host: upstreamUrl.hostname,
15788
+ method: request.method,
15789
+ path: upstreamUrl.pathname,
15790
+ provider,
15791
+ status: 403
15792
+ }),
15793
+ "app.sandbox.egress.transform_domains": lease.headerTransforms.map(
15794
+ (transform) => transform.domain
15795
+ ),
15796
+ ...routingAttributes(request, upstreamUrl)
15135
15797
  },
15136
- "Sandbox egress upstream auth rejected"
15798
+ "Sandbox egress credential lease does not cover forwarded host"
15137
15799
  );
15138
- await clearSandboxEgressCredentialLease(egressId, provider, session);
15139
- }
15140
- return new Response(upstream.body, {
15141
- status: upstream.status,
15142
- statusText: upstream.statusText,
15143
- headers: responseHeaders(upstream)
15144
- });
15145
- }
15146
-
15147
- // src/handlers/sandbox-egress-proxy.ts
15148
- async function ALL(request, egressId) {
15149
- return await proxySandboxEgressRequest(request, egressId);
15150
- }
15151
-
15152
- // src/chat/slack/context.ts
15153
- function toTrimmedSlackString(value) {
15154
- const normalized = toOptionalString(value);
15155
- return normalized?.trim() || void 0;
15156
- }
15157
- function parseSlackThreadId(threadId) {
15158
- const normalizedThreadId = toTrimmedSlackString(threadId);
15159
- if (!normalizedThreadId) {
15160
- return void 0;
15161
- }
15162
- const parts = normalizedThreadId.split(":");
15163
- if (parts.length !== 3 || parts[0] !== "slack") {
15164
- return void 0;
15165
- }
15166
- const channelId = toTrimmedSlackString(parts[1]);
15167
- const threadTs = toTrimmedSlackString(parts[2]);
15168
- if (!channelId || !threadTs) {
15169
- return void 0;
15170
- }
15171
- return { channelId, threadTs };
15172
- }
15173
- function resolveSlackChannelIdFromThreadId(threadId) {
15174
- return parseSlackThreadId(threadId)?.channelId;
15175
- }
15176
- function resolveSlackChannelIdFromMessage(message) {
15177
- const messageChannelId = toTrimmedSlackString(
15178
- message.channelId
15179
- );
15180
- if (messageChannelId) {
15181
- return messageChannelId;
15800
+ return jsonError("Credential lease does not cover forwarded host", 403);
15182
15801
  }
15183
- const raw = message.raw;
15184
- if (raw && typeof raw === "object") {
15185
- const rawChannel = toTrimmedSlackString(
15186
- raw.channel
15802
+ const body = await requestBodyBytes(request);
15803
+ const fetchImpl = deps.fetch ?? fetch;
15804
+ const headers = requestHeaders(request, lease, upstreamUrl.hostname);
15805
+ const upstream = await fetchImpl(upstreamUrl, {
15806
+ method: request.method,
15807
+ headers,
15808
+ ...body !== void 0 ? { body } : {},
15809
+ redirect: "manual"
15810
+ });
15811
+ logSandboxEgressUpstreamRequest({
15812
+ egressId: activeEgressId,
15813
+ provider,
15814
+ request,
15815
+ upstream,
15816
+ upstreamUrl
15817
+ });
15818
+ if (upstream.status >= 400) {
15819
+ logWarn(
15820
+ "sandbox_egress_upstream_error_response",
15821
+ {},
15822
+ {
15823
+ ...egressAttributes({
15824
+ egressId: activeEgressId,
15825
+ host: upstreamUrl.hostname,
15826
+ method: request.method,
15827
+ path: upstreamUrl.pathname,
15828
+ provider,
15829
+ status: upstream.status
15830
+ }),
15831
+ ...routingAttributes(request, upstreamUrl),
15832
+ "error.type": `http_${upstream.status}`
15833
+ },
15834
+ `Sandbox egress upstream returned HTTP ${upstream.status}`
15187
15835
  );
15188
- if (rawChannel) {
15189
- return rawChannel;
15190
- }
15191
15836
  }
15192
- const threadId = toTrimmedSlackString(
15193
- message.threadId
15194
- );
15195
- return resolveSlackChannelIdFromThreadId(threadId);
15837
+ if (AUTH_REJECTION_STATUS.has(upstream.status)) {
15838
+ logWarn(
15839
+ "sandbox_egress_upstream_auth_rejected",
15840
+ {},
15841
+ {
15842
+ ...egressAttributes({
15843
+ egressId: activeEgressId,
15844
+ host: upstreamUrl.hostname,
15845
+ method: request.method,
15846
+ path: upstreamUrl.pathname,
15847
+ provider,
15848
+ status: upstream.status
15849
+ }),
15850
+ ...routingAttributes(request, upstreamUrl)
15851
+ },
15852
+ "Sandbox egress upstream auth rejected"
15853
+ );
15854
+ await clearSandboxEgressCredentialLease(activeEgressId, provider, session);
15855
+ }
15856
+ return new Response(upstream.body, {
15857
+ status: upstream.status,
15858
+ statusText: upstream.statusText,
15859
+ headers: responseHeaders(upstream)
15860
+ });
15861
+ }
15862
+
15863
+ // src/handlers/sandbox-egress-proxy.ts
15864
+ async function ALL(request) {
15865
+ return await proxySandboxEgressRequest(request);
15866
+ }
15867
+ function isSandboxEgressRequest(request) {
15868
+ return isSandboxEgressForwardedRequest(request);
15196
15869
  }
15197
15870
 
15198
15871
  // src/handlers/turn-resume.ts
@@ -15487,11 +16160,11 @@ var DIRECTED_FOLLOW_UP_CUE_RE = /\b(?:you said|you just said|your last response|
15487
16160
  var TERSE_CLARIFICATION_RE = /^(?:which one|which ones|why|how so|what do you mean|what did you mean|say more|explain that|clarify that|expand on that|elaborate on that)\??$/i;
15488
16161
  var GENERIC_IMMEDIATE_SIDE_CONVERSATION_RE = /^(?:is that (?:the )?right (?:approach|call|move)|(?:can|could|would) you check on this)\??$/i;
15489
16162
  var RECENT_THREAD_WINDOW = 6;
15490
- function escapeRegExp2(value) {
16163
+ function escapeRegExp3(value) {
15491
16164
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
15492
16165
  }
15493
16166
  function containsAssistantInvocation(text, botUserName) {
15494
- const escapedUserName = escapeRegExp2(botUserName);
16167
+ const escapedUserName = escapeRegExp3(botUserName);
15495
16168
  const plainNameMentionRe = new RegExp(`(^|\\s)@${escapedUserName}\\b`, "i");
15496
16169
  const labeledEntityMentionRe = new RegExp(
15497
16170
  `<@[^>|]+\\|${escapedUserName}>`,
@@ -15786,187 +16459,6 @@ async function decideSubscribedThreadReply(args) {
15786
16459
  }
15787
16460
  }
15788
16461
 
15789
- // src/chat/slack/errors.ts
15790
- function getSlackApiErrorCode(error) {
15791
- if (!error || typeof error !== "object") {
15792
- return void 0;
15793
- }
15794
- const candidate = error;
15795
- if (typeof candidate.data?.error === "string" && candidate.data.error.trim().length > 0) {
15796
- return candidate.data.error;
15797
- }
15798
- if (typeof candidate.code === "string" && candidate.code.trim().length > 0) {
15799
- return candidate.code;
15800
- }
15801
- return void 0;
15802
- }
15803
- function getSlackErrorObservabilityAttributes(error) {
15804
- if (!error || typeof error !== "object") {
15805
- return {};
15806
- }
15807
- const candidate = error;
15808
- const attributes = {};
15809
- if (typeof candidate.code === "string" && candidate.code.trim().length > 0) {
15810
- attributes["app.slack.error_code"] = candidate.code;
15811
- }
15812
- if (typeof candidate.data?.error === "string" && candidate.data.error.trim().length > 0) {
15813
- attributes["app.slack.api_error"] = candidate.data.error;
15814
- }
15815
- const requestId = getHeaderString(candidate.headers, "x-slack-req-id");
15816
- if (requestId) {
15817
- attributes["app.slack.request_id"] = requestId;
15818
- }
15819
- if (typeof candidate.statusCode === "number" && Number.isFinite(candidate.statusCode)) {
15820
- attributes["http.response.status_code"] = candidate.statusCode;
15821
- }
15822
- return attributes;
15823
- }
15824
- function isSlackTitlePermissionError(error) {
15825
- const code = getSlackApiErrorCode(error);
15826
- return code === "no_permission" || code === "missing_scope" || code === "not_allowed_token_type";
15827
- }
15828
-
15829
- // src/chat/runtime/thread-context.ts
15830
- function escapeRegExp3(value) {
15831
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
15832
- }
15833
- function stripLeadingBotMention(text, options = {}) {
15834
- if (!text.trim()) return text;
15835
- let next = text;
15836
- if (options.stripLeadingSlackMentionToken) {
15837
- next = next.replace(/^\s*<@[^>]+>[\s,:-]*/, "").trim();
15838
- }
15839
- const mentionByNameRe = new RegExp(
15840
- `^\\s*@${escapeRegExp3(botConfig.userName)}\\b[\\s,:-]*`,
15841
- "i"
15842
- );
15843
- next = next.replace(mentionByNameRe, "").trim();
15844
- const mentionByLabeledEntityRe = new RegExp(
15845
- `^\\s*<@[^>|]+\\|${escapeRegExp3(botConfig.userName)}>[\\s,:-]*`,
15846
- "i"
15847
- );
15848
- next = next.replace(mentionByLabeledEntityRe, "").trim();
15849
- return next;
15850
- }
15851
- function getThreadId(thread, _message) {
15852
- return toOptionalString(thread.id);
15853
- }
15854
- function getRunId(thread, message) {
15855
- return toOptionalString(thread.runId) ?? toOptionalString(message.runId);
15856
- }
15857
- function getChannelId(thread, message) {
15858
- return resolveSlackChannelIdFromThreadId(toOptionalString(thread.id)) ?? normalizeSlackConversationId(toOptionalString(thread.channelId)) ?? resolveSlackChannelIdFromMessage(message);
15859
- }
15860
- function getThreadTs(threadId) {
15861
- return parseSlackThreadId(threadId)?.threadTs;
15862
- }
15863
- function getAssistantThreadContext(message) {
15864
- const raw = message.raw;
15865
- const rawRecord = raw && typeof raw === "object" ? raw : void 0;
15866
- const channelId = toOptionalString(rawRecord?.channel);
15867
- if (channelId) {
15868
- const rawThreadTs = toOptionalString(rawRecord?.thread_ts);
15869
- const threadTs = isDmChannel(channelId) ? rawThreadTs : rawThreadTs ?? toOptionalString(rawRecord?.ts);
15870
- if (threadTs) {
15871
- return { channelId, threadTs };
15872
- }
15873
- }
15874
- const parsedThreadId = parseSlackThreadId(
15875
- toOptionalString(message.threadId)
15876
- );
15877
- if (!parsedThreadId || isDmChannel(parsedThreadId.channelId)) {
15878
- return void 0;
15879
- }
15880
- return parsedThreadId;
15881
- }
15882
- function getMessageTs(message) {
15883
- const directTs = toOptionalString(
15884
- message.ts
15885
- );
15886
- if (directTs) {
15887
- return directTs;
15888
- }
15889
- const raw = message.raw;
15890
- if (!raw || typeof raw !== "object") {
15891
- return void 0;
15892
- }
15893
- const rawRecord = raw;
15894
- return toOptionalString(rawRecord.ts) ?? toOptionalString(rawRecord.event_ts) ?? toOptionalString(rawRecord.message?.ts);
15895
- }
15896
-
15897
- // src/chat/runtime/processing-reaction.ts
15898
- var PROCESSING_REACTION_EMOJI = "eyes";
15899
- var noProcessingReaction = {
15900
- keep: () => void 0,
15901
- stop: async () => void 0
15902
- };
15903
- function isProcessingReactionEmoji(value) {
15904
- return typeof value === "string" && normalizeSlackEmojiName(value) === PROCESSING_REACTION_EMOJI;
15905
- }
15906
- function shouldKeepProcessingReactionForToolInvocation(input) {
15907
- return input.toolName === "slackMessageAddReaction" && isProcessingReactionEmoji(input.params.emoji);
15908
- }
15909
- async function startSlackProcessingReaction(args) {
15910
- if (args.message.author.isMe) {
15911
- return noProcessingReaction;
15912
- }
15913
- const channelId = getChannelId(args.thread, args.message);
15914
- const messageTs = getMessageTs(args.message);
15915
- if (!channelId || !messageTs) {
15916
- return noProcessingReaction;
15917
- }
15918
- try {
15919
- await addReactionToMessage({
15920
- channelId,
15921
- timestamp: messageTs,
15922
- emoji: PROCESSING_REACTION_EMOJI
15923
- });
15924
- } catch (error) {
15925
- args.logException(
15926
- error,
15927
- "slack_processing_reaction_add_failed",
15928
- args.logContext,
15929
- {
15930
- "app.slack.action": "reactions.add",
15931
- "messaging.message.id": messageTs,
15932
- ...getSlackErrorObservabilityAttributes(error)
15933
- },
15934
- "Failed to add Slack processing reaction"
15935
- );
15936
- return noProcessingReaction;
15937
- }
15938
- let shouldRemove = true;
15939
- return {
15940
- keep: () => {
15941
- shouldRemove = false;
15942
- },
15943
- stop: async () => {
15944
- if (!shouldRemove) {
15945
- return;
15946
- }
15947
- try {
15948
- await removeReactionFromMessage({
15949
- channelId,
15950
- timestamp: messageTs,
15951
- emoji: PROCESSING_REACTION_EMOJI
15952
- });
15953
- } catch (error) {
15954
- args.logException(
15955
- error,
15956
- "slack_processing_reaction_remove_failed",
15957
- args.logContext,
15958
- {
15959
- "app.slack.action": "reactions.remove",
15960
- "messaging.message.id": messageTs,
15961
- ...getSlackErrorObservabilityAttributes(error)
15962
- },
15963
- "Failed to remove Slack processing reaction"
15964
- );
15965
- }
15966
- }
15967
- };
15968
- }
15969
-
15970
16462
  // src/chat/runtime/slack-runtime.ts
15971
16463
  var THREAD_OPTOUT_ACK = "Understood. I'll stay out of this thread unless someone @mentions me again.";
15972
16464
  async function maybeHandleThreadOptOutDecision(args) {
@@ -16131,24 +16623,14 @@ function createSlackTurnRuntime(deps) {
16131
16623
  const threadId = deps.getThreadId(thread, message);
16132
16624
  const channelId = deps.getChannelId(thread, message);
16133
16625
  const runId = deps.getRunId(thread, message);
16134
- const context = logContext({
16626
+ const turnContext = logContext({
16135
16627
  threadId,
16136
16628
  requesterId: message.author.userId,
16137
16629
  requesterUserName: message.author.userName,
16138
16630
  channelId,
16139
16631
  runId
16140
16632
  });
16141
- processingReaction = await startSlackProcessingReaction({
16142
- thread,
16143
- message,
16144
- logException: deps.logException,
16145
- logContext: context
16146
- });
16147
- const toolInvocationHook = createToolInvocationHook(
16148
- processingReaction,
16149
- hooks
16150
- );
16151
- await deps.withSpan("chat.turn", "chat.turn", context, async () => {
16633
+ await deps.withSpan("chat.turn", "chat.turn", turnContext, async () => {
16152
16634
  const legacyAttachmentText = renderSlackLegacyAttachmentText(
16153
16635
  message.raw
16154
16636
  );
@@ -16163,7 +16645,7 @@ function createSlackTurnRuntime(deps) {
16163
16645
  strippedUserText,
16164
16646
  message.raw
16165
16647
  );
16166
- const context2 = {
16648
+ const threadContext = {
16167
16649
  threadId,
16168
16650
  requesterId: message.author.userId,
16169
16651
  channelId,
@@ -16181,7 +16663,7 @@ function createSlackTurnRuntime(deps) {
16181
16663
  thread,
16182
16664
  message,
16183
16665
  decision: { shouldReply: false, reason },
16184
- context: context2,
16666
+ context: threadContext,
16185
16667
  userText
16186
16668
  });
16187
16669
  return;
@@ -16191,7 +16673,7 @@ function createSlackTurnRuntime(deps) {
16191
16673
  message,
16192
16674
  userText,
16193
16675
  explicitMention: Boolean(message.isMention),
16194
- context: context2
16676
+ context: threadContext
16195
16677
  });
16196
16678
  await deps.persistPreparedState({
16197
16679
  thread,
@@ -16203,7 +16685,7 @@ function createSlackTurnRuntime(deps) {
16203
16685
  conversationContext: deps.getPreparedConversationContext(preparedState),
16204
16686
  hasAttachments: message.attachments.length > 0 || legacyAttachmentText !== "",
16205
16687
  isExplicitMention: Boolean(message.isMention),
16206
- context: context2
16688
+ context: threadContext
16207
16689
  });
16208
16690
  if (await maybeHandleThreadOptOutDecision({
16209
16691
  thread,
@@ -16214,7 +16696,7 @@ function createSlackTurnRuntime(deps) {
16214
16696
  thread,
16215
16697
  message,
16216
16698
  decision,
16217
- context: context2,
16699
+ context: threadContext,
16218
16700
  preparedState,
16219
16701
  userText
16220
16702
  });
@@ -16225,12 +16707,22 @@ function createSlackTurnRuntime(deps) {
16225
16707
  thread,
16226
16708
  message,
16227
16709
  decision,
16228
- context: context2,
16710
+ context: threadContext,
16229
16711
  preparedState,
16230
16712
  userText
16231
16713
  });
16232
16714
  return;
16233
16715
  }
16716
+ processingReaction = await startSlackProcessingReaction({
16717
+ thread,
16718
+ message,
16719
+ logException: deps.logException,
16720
+ logContext: turnContext
16721
+ });
16722
+ const toolInvocationHook = createToolInvocationHook(
16723
+ processingReaction,
16724
+ hooks
16725
+ );
16234
16726
  await deps.replyToThread(thread, message, {
16235
16727
  explicitMention: Boolean(message.isMention),
16236
16728
  preparedState,
@@ -16988,8 +17480,14 @@ function createJuniorRuntimeServices(overrides = {}) {
16988
17480
  }
16989
17481
 
16990
17482
  // src/chat/slack/message.ts
17483
+ function isSlackMessageTs(value) {
17484
+ return /^\d+(?:\.\d+)?$/.test(value.trim());
17485
+ }
16991
17486
  function getSlackMessageTs(message) {
16992
- if (message.id.endsWith(":message_changed_mention") && message.raw && typeof message.raw === "object") {
17487
+ if (isSlackMessageTs(message.id)) {
17488
+ return message.id;
17489
+ }
17490
+ if (message.raw && typeof message.raw === "object") {
16993
17491
  const ts = message.raw.ts;
16994
17492
  if (typeof ts === "string" && ts.length > 0) {
16995
17493
  return ts;
@@ -17152,6 +17650,26 @@ function createReplyToThread(deps) {
17152
17650
  throw error;
17153
17651
  }
17154
17652
  };
17653
+ const postAuthPauseNotice = async () => {
17654
+ try {
17655
+ await beforeFirstResponsePost();
17656
+ await thread.post(
17657
+ buildSlackOutputMessage(buildAuthPauseResponse())
17658
+ );
17659
+ } catch (error) {
17660
+ logException(
17661
+ error,
17662
+ "slack_auth_pause_notice_post_failed",
17663
+ turnTraceContext,
17664
+ {
17665
+ "app.slack.reply_stage": "thread_reply_auth_pause_notice",
17666
+ ...messageTs ? { "messaging.message.id": messageTs } : {},
17667
+ ...getSlackErrorObservabilityAttributes(error)
17668
+ },
17669
+ "Failed to post auth pause notice"
17670
+ );
17671
+ }
17672
+ };
17155
17673
  const activeTurnId = preparedState.conversation.processing.activeTurnId;
17156
17674
  if (conversationId && activeTurnId) {
17157
17675
  const resumeRequest = await deps.services.getAwaitingTurnContinuationRequest({
@@ -17466,6 +17984,7 @@ function createReplyToThread(deps) {
17466
17984
  }
17467
17985
  } catch (error) {
17468
17986
  if (isRetryableTurnError(error, "mcp_auth_resume") || isRetryableTurnError(error, "plugin_auth_resume")) {
17987
+ await postAuthPauseNotice();
17469
17988
  completeAuthPauseTurn({
17470
17989
  conversation: preparedState.conversation,
17471
17990
  sessionId: error.metadata?.sessionId ?? turnId
@@ -18692,6 +19211,12 @@ async function createApp(options) {
18692
19211
  logException(err, "unhandled_route_error");
18693
19212
  return c.text("Internal Server Error", 500);
18694
19213
  });
19214
+ app.use("*", async (c, next) => {
19215
+ if (isSandboxEgressRequest(c.req.raw)) {
19216
+ return await ALL(c.req.raw);
19217
+ }
19218
+ await next();
19219
+ });
18695
19220
  app.get("/", () => GET3());
18696
19221
  app.get("/health", () => GET2());
18697
19222
  app.get("/api/info", () => GET());
@@ -18704,12 +19229,6 @@ async function createApp(options) {
18704
19229
  app.post("/api/internal/turn-resume", (c) => {
18705
19230
  return POST(c.req.raw, waitUntil);
18706
19231
  });
18707
- app.all("/api/internal/sandbox-egress/:egressId", (c) => {
18708
- return ALL(c.req.raw, c.req.param("egressId"));
18709
- });
18710
- app.all("/api/internal/sandbox-egress/:egressId/*", (c) => {
18711
- return ALL(c.req.raw, c.req.param("egressId"));
18712
- });
18713
19232
  app.post("/api/webhooks/:platform", (c) => {
18714
19233
  return POST2(c.req.raw, c.req.param("platform"), waitUntil);
18715
19234
  });