@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 +1148 -629
- package/dist/{chunk-QAMTCT2R.js → chunk-ELM6HJ6S.js} +1 -1
- package/dist/cli/snapshot-warmup.js +1 -1
- package/package.json +1 -1
package/dist/app.js
CHANGED
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
runNonInteractiveCommand,
|
|
32
32
|
sandboxSkillDir,
|
|
33
33
|
sandboxSkillFile
|
|
34
|
-
} from "./chunk-
|
|
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
|
-
|
|
4518
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
4615
|
+
return {
|
|
4616
|
+
files,
|
|
4617
|
+
limitReached,
|
|
4618
|
+
missingPath,
|
|
4619
|
+
missingRoot: missingPath === params.root
|
|
4620
|
+
};
|
|
4550
4621
|
}
|
|
4551
4622
|
|
|
4552
|
-
// src/chat/tools/sandbox/
|
|
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
|
|
4734
|
+
function validateAndApplyTextEdits(content, edits, targetName) {
|
|
4627
4735
|
if (!Array.isArray(edits) || edits.length === 0) {
|
|
4628
|
-
throw new Error(
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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
|
-
|
|
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 } =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5215
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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":
|
|
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
|
-
|
|
6318
|
-
|
|
6319
|
-
|
|
6320
|
-
|
|
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
|
|
6626
|
+
function createSlackCanvasReadTool() {
|
|
6460
6627
|
return tool({
|
|
6461
|
-
description: "
|
|
6462
|
-
|
|
6463
|
-
|
|
6464
|
-
|
|
6465
|
-
|
|
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: "
|
|
6487
|
-
})
|
|
6488
|
-
|
|
6489
|
-
|
|
6490
|
-
|
|
6491
|
-
|
|
6492
|
-
|
|
6493
|
-
|
|
6494
|
-
|
|
6495
|
-
|
|
6496
|
-
|
|
6497
|
-
|
|
6498
|
-
|
|
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
|
-
"
|
|
6682
|
+
"slack_canvas_read_failed",
|
|
6501
6683
|
{},
|
|
6502
6684
|
{
|
|
6503
|
-
"gen_ai.tool.name": "
|
|
6504
|
-
"app.
|
|
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
|
-
|
|
6688
|
+
message
|
|
6508
6689
|
);
|
|
6509
6690
|
return {
|
|
6510
6691
|
ok: false,
|
|
6511
|
-
|
|
6692
|
+
canvas_id: target.canvasId,
|
|
6693
|
+
error: message
|
|
6512
6694
|
};
|
|
6513
6695
|
}
|
|
6514
|
-
|
|
6515
|
-
|
|
6516
|
-
|
|
6517
|
-
|
|
6518
|
-
|
|
6519
|
-
|
|
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
|
-
|
|
6529
|
-
|
|
6530
|
-
|
|
6531
|
-
|
|
6532
|
-
|
|
6533
|
-
|
|
6534
|
-
|
|
6535
|
-
|
|
6536
|
-
|
|
6537
|
-
|
|
6538
|
-
|
|
6539
|
-
|
|
6540
|
-
|
|
6541
|
-
|
|
6542
|
-
|
|
6543
|
-
|
|
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
|
|
6792
|
+
function createSlackCanvasWriteTool(state) {
|
|
6548
6793
|
return tool({
|
|
6549
|
-
description: "
|
|
6550
|
-
|
|
6551
|
-
|
|
6552
|
-
|
|
6553
|
-
|
|
6554
|
-
|
|
6555
|
-
|
|
6556
|
-
|
|
6557
|
-
|
|
6558
|
-
|
|
6559
|
-
|
|
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
|
-
|
|
6562
|
-
|
|
6822
|
+
...cached,
|
|
6823
|
+
deduplicated: true
|
|
6563
6824
|
};
|
|
6564
6825
|
}
|
|
6565
6826
|
try {
|
|
6566
|
-
const
|
|
6567
|
-
|
|
6568
|
-
|
|
6569
|
-
|
|
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:
|
|
6572
|
-
|
|
6573
|
-
|
|
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
|
|
6844
|
+
const message = error instanceof Error ? error.message : "canvas write failed";
|
|
6582
6845
|
logWarn(
|
|
6583
|
-
"
|
|
6846
|
+
"slack_canvas_write_failed",
|
|
6584
6847
|
{},
|
|
6585
6848
|
{
|
|
6586
|
-
"gen_ai.tool.name": "
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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 () =>
|
|
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:
|
|
10177
|
-
afterCommand:
|
|
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
|
-
|
|
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/
|
|
13484
|
-
function
|
|
13485
|
-
if (typeof
|
|
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
|
|
13493
|
-
|
|
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
|
-
|
|
13496
|
-
|
|
13497
|
-
|
|
13498
|
-
|
|
13499
|
-
|
|
13500
|
-
|
|
13501
|
-
|
|
13502
|
-
|
|
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
|
|
13506
|
-
const
|
|
13507
|
-
|
|
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
|
-
|
|
13532
|
-
|
|
13533
|
-
|
|
13534
|
-
|
|
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
|
-
|
|
13538
|
-
|
|
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
|
|
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
|
-
|
|
13553
|
-
|
|
13554
|
-
|
|
13555
|
-
|
|
13556
|
-
|
|
13557
|
-
|
|
13558
|
-
|
|
13559
|
-
|
|
13560
|
-
|
|
13561
|
-
|
|
13562
|
-
|
|
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
|
-
|
|
13827
|
+
if (rawChannel) {
|
|
13828
|
+
return rawChannel;
|
|
13829
|
+
}
|
|
13570
13830
|
}
|
|
13831
|
+
const threadId = toTrimmedSlackString(
|
|
13832
|
+
message.threadId
|
|
13833
|
+
);
|
|
13834
|
+
return resolveSlackChannelIdFromThreadId(threadId);
|
|
13571
13835
|
}
|
|
13572
|
-
|
|
13573
|
-
|
|
13574
|
-
|
|
13575
|
-
|
|
13576
|
-
|
|
13577
|
-
|
|
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
|
|
15149
|
+
const latestUserMessage = [...conversation.messages].reverse().find((message) => message.role === "user");
|
|
14621
15150
|
const conversationContext = buildConversationContext(conversation, {
|
|
14622
|
-
excludeMessageId:
|
|
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
|
|
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
|
|
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
|
|
14955
|
-
|
|
14956
|
-
|
|
14957
|
-
|
|
14958
|
-
|
|
14959
|
-
|
|
14960
|
-
|
|
14961
|
-
|
|
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
|
-
|
|
15542
|
+
const url = new URL(request.url);
|
|
15543
|
+
return { ok: true, path: `${url.pathname}${url.search}` };
|
|
14964
15544
|
}
|
|
14965
|
-
function buildUpstreamUrl(request
|
|
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
|
|
14988
|
-
if (!path11) {
|
|
14989
|
-
return { ok: false, error:
|
|
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(
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
"
|
|
15782
|
+
"sandbox_egress_transform_missing",
|
|
15129
15783
|
{},
|
|
15130
15784
|
{
|
|
15131
|
-
|
|
15132
|
-
|
|
15133
|
-
|
|
15134
|
-
|
|
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
|
|
15798
|
+
"Sandbox egress credential lease does not cover forwarded host"
|
|
15137
15799
|
);
|
|
15138
|
-
|
|
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
|
|
15184
|
-
|
|
15185
|
-
|
|
15186
|
-
|
|
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
|
-
|
|
15193
|
-
|
|
15194
|
-
|
|
15195
|
-
|
|
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
|
|
16163
|
+
function escapeRegExp3(value) {
|
|
15491
16164
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
15492
16165
|
}
|
|
15493
16166
|
function containsAssistantInvocation(text, botUserName) {
|
|
15494
|
-
const escapedUserName =
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
});
|