@sentry/junior 0.19.0 → 0.21.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 +1712 -911
- package/dist/{chunk-DTOS5CG4.js → chunk-N4ICA2BC.js} +59 -8
- package/dist/{chunk-4XWTSMRF.js → chunk-NRSP2MLC.js} +2 -3
- package/dist/{chunk-RBB2MZAN.js → chunk-XPXD3FCE.js} +23 -5
- package/dist/{chunk-XYOKYK6U.js → chunk-Z43DS7XN.js} +11 -2
- package/dist/cli/check.js +60 -3
- package/dist/cli/init.js +9 -3
- package/dist/cli/snapshot-warmup.js +3 -3
- package/dist/nitro.js +1 -1
- package/package.json +1 -1
package/dist/app.js
CHANGED
|
@@ -5,10 +5,10 @@ import {
|
|
|
5
5
|
listCapabilityProviders,
|
|
6
6
|
loadSkillsByName,
|
|
7
7
|
logCapabilityCatalogLoadedOnce,
|
|
8
|
-
parseSkillInvocation
|
|
9
|
-
|
|
10
|
-
} from "./chunk-4XWTSMRF.js";
|
|
8
|
+
parseSkillInvocation
|
|
9
|
+
} from "./chunk-NRSP2MLC.js";
|
|
11
10
|
import {
|
|
11
|
+
SANDBOX_DATA_ROOT,
|
|
12
12
|
SANDBOX_SKILLS_ROOT,
|
|
13
13
|
SANDBOX_WORKSPACE_ROOT,
|
|
14
14
|
botConfig,
|
|
@@ -25,8 +25,9 @@ import {
|
|
|
25
25
|
resolveRuntimeDependencySnapshot,
|
|
26
26
|
runNonInteractiveCommand,
|
|
27
27
|
sandboxSkillDir,
|
|
28
|
+
sandboxSkillFile,
|
|
28
29
|
toOptionalTrimmed
|
|
29
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-Z43DS7XN.js";
|
|
30
31
|
import {
|
|
31
32
|
CredentialUnavailableError,
|
|
32
33
|
buildOAuthTokenRequest,
|
|
@@ -38,6 +39,7 @@ import {
|
|
|
38
39
|
getPluginMcpProviders,
|
|
39
40
|
getPluginOAuthConfig,
|
|
40
41
|
getPluginProviders,
|
|
42
|
+
hasRequiredOAuthScope,
|
|
41
43
|
isPluginProvider,
|
|
42
44
|
isRecord,
|
|
43
45
|
logError,
|
|
@@ -55,15 +57,16 @@ import {
|
|
|
55
57
|
toOptionalString,
|
|
56
58
|
withContext,
|
|
57
59
|
withSpan
|
|
58
|
-
} from "./chunk-
|
|
60
|
+
} from "./chunk-N4ICA2BC.js";
|
|
59
61
|
import "./chunk-Z3YD6NHK.js";
|
|
60
62
|
import {
|
|
61
|
-
aboutPathCandidates,
|
|
62
63
|
discoverInstalledPluginPackageContent,
|
|
63
64
|
homeDir,
|
|
65
|
+
listReferenceFiles,
|
|
64
66
|
setPluginPackages,
|
|
65
|
-
soulPathCandidates
|
|
66
|
-
|
|
67
|
+
soulPathCandidates,
|
|
68
|
+
worldPathCandidates
|
|
69
|
+
} from "./chunk-XPXD3FCE.js";
|
|
67
70
|
import "./chunk-2KG3PWR4.js";
|
|
68
71
|
|
|
69
72
|
// src/app.ts
|
|
@@ -72,9 +75,12 @@ import { Hono } from "hono";
|
|
|
72
75
|
// src/handlers/diagnostics.ts
|
|
73
76
|
import { readFileSync } from "fs";
|
|
74
77
|
import path from "path";
|
|
75
|
-
function
|
|
78
|
+
function readDescriptionText() {
|
|
76
79
|
try {
|
|
77
|
-
const raw = readFileSync(
|
|
80
|
+
const raw = readFileSync(
|
|
81
|
+
path.join(homeDir(), "DESCRIPTION.md"),
|
|
82
|
+
"utf8"
|
|
83
|
+
).trim();
|
|
78
84
|
return raw || void 0;
|
|
79
85
|
} catch {
|
|
80
86
|
return void 0;
|
|
@@ -86,7 +92,7 @@ async function GET() {
|
|
|
86
92
|
return Response.json({
|
|
87
93
|
cwd: process.cwd(),
|
|
88
94
|
homeDir: homeDir(),
|
|
89
|
-
|
|
95
|
+
descriptionText: readDescriptionText(),
|
|
90
96
|
providers: getPluginProviders().map((plugin) => plugin.manifest.name),
|
|
91
97
|
skills: skills.map((skill) => ({
|
|
92
98
|
name: skill.name,
|
|
@@ -198,9 +204,9 @@ async function GET3() {
|
|
|
198
204
|
</head>
|
|
199
205
|
<body>
|
|
200
206
|
<h1>> junior</h1>`;
|
|
201
|
-
if (d?.
|
|
207
|
+
if (d?.descriptionText) {
|
|
202
208
|
html += `
|
|
203
|
-
<div class="subtitle">${escapeXml(String(d.
|
|
209
|
+
<div class="subtitle">${escapeXml(String(d.descriptionText))}</div>`;
|
|
204
210
|
}
|
|
205
211
|
html += `
|
|
206
212
|
<div class="section">
|
|
@@ -2298,6 +2304,17 @@ async function completeObject(params) {
|
|
|
2298
2304
|
};
|
|
2299
2305
|
}
|
|
2300
2306
|
|
|
2307
|
+
// src/chat/slack/message.ts
|
|
2308
|
+
function getSlackMessageTs(message) {
|
|
2309
|
+
if (message.id.endsWith(":message_changed_mention") && message.raw && typeof message.raw === "object") {
|
|
2310
|
+
const ts = message.raw.ts;
|
|
2311
|
+
if (typeof ts === "string" && ts.length > 0) {
|
|
2312
|
+
return ts;
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
return message.id;
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2301
2318
|
// src/chat/services/conversation-memory.ts
|
|
2302
2319
|
var CONTEXT_COMPACTION_TRIGGER_TOKENS = 9e3;
|
|
2303
2320
|
var CONTEXT_COMPACTION_TARGET_TOKENS = 7e3;
|
|
@@ -2572,7 +2589,7 @@ function createConversationMessageFromSdkMessage(entry) {
|
|
|
2572
2589
|
isBot: typeof entry.author.isBot === "boolean" ? entry.author.isBot : void 0
|
|
2573
2590
|
},
|
|
2574
2591
|
meta: {
|
|
2575
|
-
slackTs: entry
|
|
2592
|
+
slackTs: getSlackMessageTs(entry)
|
|
2576
2593
|
}
|
|
2577
2594
|
};
|
|
2578
2595
|
}
|
|
@@ -2651,6 +2668,7 @@ import { Agent } from "@mariozechner/pi-agent-core";
|
|
|
2651
2668
|
|
|
2652
2669
|
// src/chat/prompt.ts
|
|
2653
2670
|
import fs from "fs";
|
|
2671
|
+
import path2 from "path";
|
|
2654
2672
|
var DEFAULT_SOUL = "You are Junior, a practical and concise assistant.";
|
|
2655
2673
|
function getLoggedMarkdownFiles() {
|
|
2656
2674
|
const globalState = globalThis;
|
|
@@ -2698,8 +2716,8 @@ function loadSoul() {
|
|
|
2698
2716
|
);
|
|
2699
2717
|
return DEFAULT_SOUL;
|
|
2700
2718
|
}
|
|
2701
|
-
function
|
|
2702
|
-
return loadOptionalMarkdownFile(
|
|
2719
|
+
function loadWorld() {
|
|
2720
|
+
return loadOptionalMarkdownFile(worldPathCandidates(), "WORLD.md");
|
|
2703
2721
|
}
|
|
2704
2722
|
var JUNIOR_PERSONALITY = (() => {
|
|
2705
2723
|
try {
|
|
@@ -2716,17 +2734,17 @@ var JUNIOR_PERSONALITY = (() => {
|
|
|
2716
2734
|
return DEFAULT_SOUL;
|
|
2717
2735
|
}
|
|
2718
2736
|
})();
|
|
2719
|
-
var
|
|
2737
|
+
var JUNIOR_WORLD = (() => {
|
|
2720
2738
|
try {
|
|
2721
|
-
return
|
|
2739
|
+
return loadWorld();
|
|
2722
2740
|
} catch (error) {
|
|
2723
2741
|
logWarn(
|
|
2724
|
-
"
|
|
2742
|
+
"world_load_failed",
|
|
2725
2743
|
{},
|
|
2726
2744
|
{
|
|
2727
2745
|
"error.message": error instanceof Error ? error.message : String(error)
|
|
2728
2746
|
},
|
|
2729
|
-
"Failed to load
|
|
2747
|
+
"Failed to load WORLD.md; omitting world prompt context"
|
|
2730
2748
|
);
|
|
2731
2749
|
return null;
|
|
2732
2750
|
}
|
|
@@ -2846,6 +2864,25 @@ function baseSystemPrompt() {
|
|
|
2846
2864
|
"- When active skills are present, follow their instructions before default behavior."
|
|
2847
2865
|
].join("\n");
|
|
2848
2866
|
}
|
|
2867
|
+
function formatReferenceFilesSection() {
|
|
2868
|
+
const files = listReferenceFiles();
|
|
2869
|
+
if (files.length === 0) {
|
|
2870
|
+
return [];
|
|
2871
|
+
}
|
|
2872
|
+
const fileNames = files.map((filePath) => {
|
|
2873
|
+
const name = path2.basename(filePath);
|
|
2874
|
+
return `- ${escapeXml(name)} (${escapeXml(`${SANDBOX_DATA_ROOT}/${name}`)})`;
|
|
2875
|
+
});
|
|
2876
|
+
return [
|
|
2877
|
+
renderTag(
|
|
2878
|
+
"reference-files",
|
|
2879
|
+
[
|
|
2880
|
+
"Additional reference documents available in the sandbox. Read them with `readFile` when relevant.",
|
|
2881
|
+
...fileNames
|
|
2882
|
+
].join("\n")
|
|
2883
|
+
)
|
|
2884
|
+
];
|
|
2885
|
+
}
|
|
2849
2886
|
function buildSystemPrompt(params) {
|
|
2850
2887
|
const {
|
|
2851
2888
|
availableSkills,
|
|
@@ -2857,7 +2894,8 @@ function buildSystemPrompt(params) {
|
|
|
2857
2894
|
artifactState,
|
|
2858
2895
|
configuration,
|
|
2859
2896
|
relevantConfigurationKeys,
|
|
2860
|
-
runtimeMetadata
|
|
2897
|
+
runtimeMetadata,
|
|
2898
|
+
threadParticipants
|
|
2861
2899
|
} = params;
|
|
2862
2900
|
const assistantSection = renderIdentityBlock("assistant", {
|
|
2863
2901
|
user_name: assistant?.userName ?? botConfig.userName,
|
|
@@ -2913,22 +2951,43 @@ function buildSystemPrompt(params) {
|
|
|
2913
2951
|
JUNIOR_PERSONALITY.trim()
|
|
2914
2952
|
].join("\n")
|
|
2915
2953
|
),
|
|
2916
|
-
...
|
|
2954
|
+
...JUNIOR_WORLD ? [
|
|
2917
2955
|
renderTag(
|
|
2918
|
-
"
|
|
2956
|
+
"world",
|
|
2919
2957
|
[
|
|
2920
|
-
"Use this as the assistant's
|
|
2958
|
+
"Use this as the assistant's operational/domain context.",
|
|
2921
2959
|
"",
|
|
2922
|
-
|
|
2960
|
+
JUNIOR_WORLD.trim()
|
|
2923
2961
|
].join("\n")
|
|
2924
2962
|
)
|
|
2925
2963
|
] : [],
|
|
2964
|
+
...formatReferenceFilesSection(),
|
|
2926
2965
|
renderTag(
|
|
2927
2966
|
"identity-context",
|
|
2928
2967
|
[
|
|
2929
2968
|
"Use these blocks as authoritative metadata for identity questions.",
|
|
2930
2969
|
assistantSection,
|
|
2931
|
-
requesterSection
|
|
2970
|
+
requesterSection,
|
|
2971
|
+
...threadParticipants && threadParticipants.length > 0 ? [
|
|
2972
|
+
renderTag(
|
|
2973
|
+
"thread-participants",
|
|
2974
|
+
[
|
|
2975
|
+
"Known participants in this thread. When you mention one of these people, use the provided Slack mention token exactly as `<@USERID>` and do not write a bare `@name` form.",
|
|
2976
|
+
...threadParticipants.map((p) => {
|
|
2977
|
+
const parts = [];
|
|
2978
|
+
if (p.userId) {
|
|
2979
|
+
parts.push(`user_id: ${escapeXml(p.userId)}`);
|
|
2980
|
+
parts.push(`slack_mention: <@${p.userId}>`);
|
|
2981
|
+
}
|
|
2982
|
+
if (p.userName)
|
|
2983
|
+
parts.push(`user_name: ${escapeXml(p.userName)}`);
|
|
2984
|
+
if (p.fullName)
|
|
2985
|
+
parts.push(`full_name: ${escapeXml(p.fullName)}`);
|
|
2986
|
+
return `- ${parts.join(", ")}`;
|
|
2987
|
+
})
|
|
2988
|
+
].join("\n")
|
|
2989
|
+
)
|
|
2990
|
+
] : []
|
|
2932
2991
|
].join("\n")
|
|
2933
2992
|
),
|
|
2934
2993
|
renderTag(
|
|
@@ -3827,7 +3886,8 @@ async function handleOAuthStartCommand(args, deps) {
|
|
|
3827
3886
|
}
|
|
3828
3887
|
if (deps.requesterId && deps.userTokenStore) {
|
|
3829
3888
|
const stored = await deps.userTokenStore.get(deps.requesterId, provider);
|
|
3830
|
-
|
|
3889
|
+
const providerConfig = getPluginOAuthConfig(provider);
|
|
3890
|
+
if (stored && (stored.expiresAt === void 0 || stored.expiresAt > Date.now()) && hasRequiredOAuthScope(stored.scope, providerConfig?.scope)) {
|
|
3831
3891
|
const providerLabel = formatProviderLabel(provider);
|
|
3832
3892
|
return commandResult({
|
|
3833
3893
|
stdout: {
|
|
@@ -3982,12 +4042,12 @@ async function maybeExecuteJrRpcCustomCommand(command, deps) {
|
|
|
3982
4042
|
|
|
3983
4043
|
// src/chat/sandbox/skill-sandbox.ts
|
|
3984
4044
|
import fs2 from "fs/promises";
|
|
3985
|
-
import
|
|
4045
|
+
import path3 from "path";
|
|
3986
4046
|
var MAX_SKILL_FILE_BYTES = 256 * 1024;
|
|
3987
4047
|
var DEFAULT_MAX_SKILL_FILE_CHARS = 2e4;
|
|
3988
4048
|
var DEFAULT_MAX_SKILL_LIST_ENTRIES = 200;
|
|
3989
4049
|
function normalizePathForOutput(value) {
|
|
3990
|
-
return value.split(
|
|
4050
|
+
return value.split(path3.sep).join("/");
|
|
3991
4051
|
}
|
|
3992
4052
|
function normalizeSkillName(value) {
|
|
3993
4053
|
return value.trim().toLowerCase();
|
|
@@ -3996,12 +4056,12 @@ function resolvePathWithinRoot(root, relativePath) {
|
|
|
3996
4056
|
if (!relativePath.trim()) {
|
|
3997
4057
|
throw new Error("Path must not be empty.");
|
|
3998
4058
|
}
|
|
3999
|
-
if (
|
|
4059
|
+
if (path3.isAbsolute(relativePath)) {
|
|
4000
4060
|
throw new Error("Absolute paths are not allowed.");
|
|
4001
4061
|
}
|
|
4002
|
-
const resolvedRoot =
|
|
4003
|
-
const resolvedPath =
|
|
4004
|
-
if (resolvedPath !== resolvedRoot && !resolvedPath.startsWith(`${resolvedRoot}${
|
|
4062
|
+
const resolvedRoot = path3.resolve(root);
|
|
4063
|
+
const resolvedPath = path3.resolve(resolvedRoot, relativePath);
|
|
4064
|
+
if (resolvedPath !== resolvedRoot && !resolvedPath.startsWith(`${resolvedRoot}${path3.sep}`)) {
|
|
4005
4065
|
throw new Error("Path escapes the skill directory.");
|
|
4006
4066
|
}
|
|
4007
4067
|
return resolvedPath;
|
|
@@ -4081,7 +4141,7 @@ var SkillSandbox = class {
|
|
|
4081
4141
|
1,
|
|
4082
4142
|
Math.min(params.maxEntries ?? DEFAULT_MAX_SKILL_LIST_ENTRIES, 1e3)
|
|
4083
4143
|
);
|
|
4084
|
-
const root =
|
|
4144
|
+
const root = path3.resolve(skill.skillPath);
|
|
4085
4145
|
const targetDirectory = resolvePathWithinRoot(root, directory);
|
|
4086
4146
|
const targetStats = await fs2.stat(targetDirectory);
|
|
4087
4147
|
if (!targetStats.isDirectory()) {
|
|
@@ -4097,9 +4157,9 @@ var SkillSandbox = class {
|
|
|
4097
4157
|
});
|
|
4098
4158
|
children.sort((a, b) => a.name.localeCompare(b.name));
|
|
4099
4159
|
for (const child of children) {
|
|
4100
|
-
const absolutePath =
|
|
4160
|
+
const absolutePath = path3.join(currentDirectory, child.name);
|
|
4101
4161
|
const relativePath = normalizePathForOutput(
|
|
4102
|
-
|
|
4162
|
+
path3.relative(root, absolutePath)
|
|
4103
4163
|
);
|
|
4104
4164
|
if (!relativePath || relativePath.startsWith("..")) {
|
|
4105
4165
|
continue;
|
|
@@ -4122,7 +4182,7 @@ var SkillSandbox = class {
|
|
|
4122
4182
|
}
|
|
4123
4183
|
}
|
|
4124
4184
|
const relativeDirectory = normalizePathForOutput(
|
|
4125
|
-
|
|
4185
|
+
path3.relative(root, targetDirectory) || "."
|
|
4126
4186
|
);
|
|
4127
4187
|
return {
|
|
4128
4188
|
skillName: skill.name,
|
|
@@ -4137,7 +4197,7 @@ var SkillSandbox = class {
|
|
|
4137
4197
|
1,
|
|
4138
4198
|
Math.min(params.maxChars ?? DEFAULT_MAX_SKILL_FILE_CHARS, 1e5)
|
|
4139
4199
|
);
|
|
4140
|
-
const root =
|
|
4200
|
+
const root = path3.resolve(skill.skillPath);
|
|
4141
4201
|
const targetPath = resolvePathWithinRoot(root, params.filePath);
|
|
4142
4202
|
const stats = await fs2.stat(targetPath);
|
|
4143
4203
|
if (!stats.isFile()) {
|
|
@@ -4152,7 +4212,7 @@ var SkillSandbox = class {
|
|
|
4152
4212
|
const truncated = raw.length > maxChars;
|
|
4153
4213
|
return {
|
|
4154
4214
|
skillName: skill.name,
|
|
4155
|
-
path: normalizePathForOutput(
|
|
4215
|
+
path: normalizePathForOutput(path3.relative(root, targetPath)),
|
|
4156
4216
|
content: truncated ? raw.slice(0, maxChars) : raw,
|
|
4157
4217
|
truncated
|
|
4158
4218
|
};
|
|
@@ -4726,7 +4786,7 @@ function createBashTool() {
|
|
|
4726
4786
|
}
|
|
4727
4787
|
|
|
4728
4788
|
// src/chat/tools/sandbox/attach-file.ts
|
|
4729
|
-
import
|
|
4789
|
+
import path4 from "path";
|
|
4730
4790
|
import { Type as Type2 } from "@sinclair/typebox";
|
|
4731
4791
|
var MAX_ATTACH_FILE_BYTES = 10 * 1024 * 1024;
|
|
4732
4792
|
var MIME_BY_EXTENSION = {
|
|
@@ -4748,20 +4808,20 @@ function normalizeSandboxPath(inputPath) {
|
|
|
4748
4808
|
if (!trimmed) {
|
|
4749
4809
|
throw new Error("path is required");
|
|
4750
4810
|
}
|
|
4751
|
-
if (
|
|
4811
|
+
if (path4.posix.isAbsolute(trimmed)) {
|
|
4752
4812
|
return trimmed;
|
|
4753
4813
|
}
|
|
4754
|
-
return
|
|
4814
|
+
return path4.posix.join(SANDBOX_WORKSPACE_ROOT, trimmed);
|
|
4755
4815
|
}
|
|
4756
4816
|
function sanitizeFilename(value, fallbackPath) {
|
|
4757
4817
|
const candidate = (value ?? "").trim();
|
|
4758
4818
|
if (candidate) {
|
|
4759
|
-
const base =
|
|
4819
|
+
const base = path4.posix.basename(candidate);
|
|
4760
4820
|
if (base && base !== "." && base !== "..") {
|
|
4761
4821
|
return base;
|
|
4762
4822
|
}
|
|
4763
4823
|
}
|
|
4764
|
-
const derived =
|
|
4824
|
+
const derived = path4.posix.basename(fallbackPath);
|
|
4765
4825
|
if (derived && derived !== "." && derived !== "..") {
|
|
4766
4826
|
return derived;
|
|
4767
4827
|
}
|
|
@@ -4772,7 +4832,7 @@ function inferMimeType(filename, explicitMimeType) {
|
|
|
4772
4832
|
if (explicit) {
|
|
4773
4833
|
return explicit;
|
|
4774
4834
|
}
|
|
4775
|
-
const ext =
|
|
4835
|
+
const ext = path4.extname(filename).toLowerCase();
|
|
4776
4836
|
return MIME_BY_EXTENSION[ext] ?? "application/octet-stream";
|
|
4777
4837
|
}
|
|
4778
4838
|
async function detectMimeType(sandbox, targetPath) {
|
|
@@ -4819,7 +4879,7 @@ function createAttachFileTool(sandbox, hooks = {}) {
|
|
|
4819
4879
|
const fileBuffer = await sandbox.readFileToBuffer({ path: targetPath });
|
|
4820
4880
|
if (!fileBuffer) {
|
|
4821
4881
|
const generatedFile = hooks.getGeneratedFile?.(
|
|
4822
|
-
|
|
4882
|
+
path4.posix.basename(targetPath)
|
|
4823
4883
|
);
|
|
4824
4884
|
if (generatedFile) {
|
|
4825
4885
|
hooks.onGeneratedFiles?.([generatedFile]);
|
|
@@ -5021,7 +5081,7 @@ function toLoadedSkill(result, availableSkills) {
|
|
|
5021
5081
|
return {
|
|
5022
5082
|
name: result.skill_name,
|
|
5023
5083
|
description: result.description,
|
|
5024
|
-
skillPath: result.skill_dir,
|
|
5084
|
+
skillPath: metadata?.skillPath ?? result.skill_dir,
|
|
5025
5085
|
...metadata?.pluginProvider ? { pluginProvider: metadata.pluginProvider } : {},
|
|
5026
5086
|
...metadata?.allowedTools ? { allowedTools: metadata.allowedTools } : {},
|
|
5027
5087
|
...metadata?.requiresCapabilities ? { requiresCapabilities: metadata.requiresCapabilities } : {},
|
|
@@ -5029,7 +5089,7 @@ function toLoadedSkill(result, availableSkills) {
|
|
|
5029
5089
|
body: result.instructions
|
|
5030
5090
|
};
|
|
5031
5091
|
}
|
|
5032
|
-
async function
|
|
5092
|
+
async function loadSkillFromHost(availableSkills, skillName) {
|
|
5033
5093
|
const requested = skillName.trim().toLowerCase();
|
|
5034
5094
|
const skill = availableSkills.find(
|
|
5035
5095
|
(entry) => entry.name.toLowerCase() === requested
|
|
@@ -5042,10 +5102,10 @@ async function loadSkillFromSandbox(sandbox, availableSkills, skillName) {
|
|
|
5042
5102
|
};
|
|
5043
5103
|
}
|
|
5044
5104
|
const skillDir = sandboxSkillDir(skill.name);
|
|
5045
|
-
const skillFilePath =
|
|
5046
|
-
const
|
|
5047
|
-
if (!
|
|
5048
|
-
throw new Error(`failed to
|
|
5105
|
+
const skillFilePath = sandboxSkillFile(skill.name);
|
|
5106
|
+
const [loaded] = await loadSkillsByName([skill.name], availableSkills);
|
|
5107
|
+
if (!loaded) {
|
|
5108
|
+
throw new Error(`failed to load ${skill.name}`);
|
|
5049
5109
|
}
|
|
5050
5110
|
return {
|
|
5051
5111
|
ok: true,
|
|
@@ -5054,10 +5114,10 @@ async function loadSkillFromSandbox(sandbox, availableSkills, skillName) {
|
|
|
5054
5114
|
...skill.requiresCapabilities ? { requires_capabilities: skill.requiresCapabilities } : {},
|
|
5055
5115
|
skill_dir: skillDir,
|
|
5056
5116
|
location: skillFilePath,
|
|
5057
|
-
instructions:
|
|
5117
|
+
instructions: loaded.body
|
|
5058
5118
|
};
|
|
5059
5119
|
}
|
|
5060
|
-
function createLoadSkillTool(
|
|
5120
|
+
function createLoadSkillTool(availableSkills, options) {
|
|
5061
5121
|
return tool({
|
|
5062
5122
|
description: "Load a skill by name so its instructions are available for this turn. The result includes `requires_capabilities` when the skill declares authenticated provider access, and `available_tools` when the skill exposes MCP tools for this turn. Use when a request clearly matches a known skill. Do not use when no skill is relevant.",
|
|
5063
5123
|
inputSchema: Type4.Object({
|
|
@@ -5067,11 +5127,7 @@ function createLoadSkillTool(sandbox, availableSkills, options) {
|
|
|
5067
5127
|
})
|
|
5068
5128
|
}),
|
|
5069
5129
|
execute: async ({ skill_name }) => {
|
|
5070
|
-
const result = await
|
|
5071
|
-
sandbox,
|
|
5072
|
-
availableSkills,
|
|
5073
|
-
skill_name
|
|
5074
|
-
);
|
|
5130
|
+
const result = await loadSkillFromHost(availableSkills, skill_name);
|
|
5075
5131
|
const loadedSkill = toLoadedSkill(result, availableSkills);
|
|
5076
5132
|
if (loadedSkill) {
|
|
5077
5133
|
const metadata = await options?.onSkillLoaded?.(loadedSkill);
|
|
@@ -6810,7 +6866,7 @@ function createToolState(hooks, context) {
|
|
|
6810
6866
|
function createTools(availableSkills, hooks = {}, context) {
|
|
6811
6867
|
const state = createToolState(hooks, context);
|
|
6812
6868
|
const tools = {
|
|
6813
|
-
loadSkill: createLoadSkillTool(
|
|
6869
|
+
loadSkill: createLoadSkillTool(availableSkills, {
|
|
6814
6870
|
onSkillLoaded: hooks.onSkillLoaded
|
|
6815
6871
|
}),
|
|
6816
6872
|
systemTime: createSystemTimeTool(),
|
|
@@ -6866,10 +6922,7 @@ function resolveChannelCapabilities(channelId) {
|
|
|
6866
6922
|
}
|
|
6867
6923
|
|
|
6868
6924
|
// src/chat/sandbox/sandbox.ts
|
|
6869
|
-
import
|
|
6870
|
-
import path4 from "path";
|
|
6871
|
-
import { Sandbox } from "@vercel/sandbox";
|
|
6872
|
-
import { createBashTool as createBashTool2 } from "bash-tool";
|
|
6925
|
+
import fs4 from "fs/promises";
|
|
6873
6926
|
|
|
6874
6927
|
// src/chat/sandbox/http-error-details.ts
|
|
6875
6928
|
var DEFAULT_PREVIEW_LIMIT = 512;
|
|
@@ -6973,6 +7026,109 @@ function extractHttpErrorDetails(error, options = {}) {
|
|
|
6973
7026
|
};
|
|
6974
7027
|
}
|
|
6975
7028
|
|
|
7029
|
+
// src/chat/sandbox/errors.ts
|
|
7030
|
+
var SANDBOX_ERROR_FIELDS = [
|
|
7031
|
+
{
|
|
7032
|
+
sourceKey: "sandboxId",
|
|
7033
|
+
attributeKey: "sandbox_id",
|
|
7034
|
+
summaryKey: "sandboxId"
|
|
7035
|
+
}
|
|
7036
|
+
];
|
|
7037
|
+
function getSandboxErrorDetails(error) {
|
|
7038
|
+
return extractHttpErrorDetails(error, {
|
|
7039
|
+
attributePrefix: "app.sandbox.api_error",
|
|
7040
|
+
extraFields: [...SANDBOX_ERROR_FIELDS]
|
|
7041
|
+
});
|
|
7042
|
+
}
|
|
7043
|
+
function findInErrorChain(error, predicate) {
|
|
7044
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7045
|
+
let current = error;
|
|
7046
|
+
while (current && !seen.has(current)) {
|
|
7047
|
+
if (predicate(current)) {
|
|
7048
|
+
return true;
|
|
7049
|
+
}
|
|
7050
|
+
seen.add(current);
|
|
7051
|
+
current = typeof current === "object" ? current.cause : void 0;
|
|
7052
|
+
}
|
|
7053
|
+
return false;
|
|
7054
|
+
}
|
|
7055
|
+
function getFirstErrorMessage(error) {
|
|
7056
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7057
|
+
let current = error;
|
|
7058
|
+
while (current && !seen.has(current)) {
|
|
7059
|
+
if (current instanceof Error) {
|
|
7060
|
+
const message = current.message.trim();
|
|
7061
|
+
if (message) {
|
|
7062
|
+
return message;
|
|
7063
|
+
}
|
|
7064
|
+
}
|
|
7065
|
+
seen.add(current);
|
|
7066
|
+
current = typeof current === "object" ? current.cause : void 0;
|
|
7067
|
+
}
|
|
7068
|
+
return void 0;
|
|
7069
|
+
}
|
|
7070
|
+
function isAlreadyExistsError(error) {
|
|
7071
|
+
const details = getSandboxErrorDetails(error);
|
|
7072
|
+
return details.searchableText.includes("already exists") || details.searchableText.includes("file exists") || details.searchableText.includes("eexist");
|
|
7073
|
+
}
|
|
7074
|
+
function isSandboxUnavailableError(error) {
|
|
7075
|
+
return findInErrorChain(error, (candidate) => {
|
|
7076
|
+
const details = getSandboxErrorDetails(candidate);
|
|
7077
|
+
const searchable = `${details.searchableText} ${details.summary}`.toLowerCase();
|
|
7078
|
+
return searchable.includes("sandbox_stopped") || searchable.includes("status=410") || searchable.includes("status code 410") || searchable.includes("no longer available");
|
|
7079
|
+
});
|
|
7080
|
+
}
|
|
7081
|
+
function isSnapshottingError(error) {
|
|
7082
|
+
return findInErrorChain(error, (candidate) => {
|
|
7083
|
+
const details = getSandboxErrorDetails(candidate);
|
|
7084
|
+
const searchable = `${details.searchableText} ${details.summary}`.toLowerCase();
|
|
7085
|
+
return searchable.includes("sandbox_snapshotting") || searchable.includes("creating a snapshot") || searchable.includes("stopped shortly");
|
|
7086
|
+
});
|
|
7087
|
+
}
|
|
7088
|
+
function wrapSandboxSetupError(error) {
|
|
7089
|
+
try {
|
|
7090
|
+
const details = getSandboxErrorDetails(error);
|
|
7091
|
+
if (details.summary) {
|
|
7092
|
+
return new Error(`sandbox setup failed (${details.summary})`, {
|
|
7093
|
+
cause: error
|
|
7094
|
+
});
|
|
7095
|
+
}
|
|
7096
|
+
} catch {
|
|
7097
|
+
}
|
|
7098
|
+
let causeMessage;
|
|
7099
|
+
try {
|
|
7100
|
+
causeMessage = getFirstErrorMessage(error);
|
|
7101
|
+
} catch (cause) {
|
|
7102
|
+
causeMessage = cause instanceof Error ? cause.message : void 0;
|
|
7103
|
+
}
|
|
7104
|
+
if (causeMessage && causeMessage.trim() && causeMessage !== "sandbox setup failed") {
|
|
7105
|
+
const oneLine = causeMessage.replace(/\s+/g, " ").trim();
|
|
7106
|
+
return new Error(`sandbox setup failed (${oneLine})`, { cause: error });
|
|
7107
|
+
}
|
|
7108
|
+
return new Error("sandbox setup failed", { cause: error });
|
|
7109
|
+
}
|
|
7110
|
+
function throwSandboxOperationError(action, error, includeMissingPath = false) {
|
|
7111
|
+
const details = getSandboxErrorDetails(error);
|
|
7112
|
+
setSpanAttributes({
|
|
7113
|
+
...details.attributes,
|
|
7114
|
+
...includeMissingPath ? {
|
|
7115
|
+
"app.sandbox.api_error.missing_path": details.searchableText.includes("no such file") || details.searchableText.includes("enoent")
|
|
7116
|
+
} : {},
|
|
7117
|
+
"app.sandbox.success": false
|
|
7118
|
+
});
|
|
7119
|
+
setSpanStatus("error");
|
|
7120
|
+
throw new Error(
|
|
7121
|
+
details.summary ? `${action} failed (${details.summary})` : `${action} failed`,
|
|
7122
|
+
{
|
|
7123
|
+
cause: error
|
|
7124
|
+
}
|
|
7125
|
+
);
|
|
7126
|
+
}
|
|
7127
|
+
|
|
7128
|
+
// src/chat/sandbox/session.ts
|
|
7129
|
+
import { Sandbox } from "@vercel/sandbox";
|
|
7130
|
+
import { createBashTool as createBashTool2 } from "bash-tool";
|
|
7131
|
+
|
|
6976
7132
|
// src/chat/runtime/status-format.ts
|
|
6977
7133
|
var SLACK_STATUS_MAX_LENGTH = 50;
|
|
6978
7134
|
function truncateWithEllipsis(text, maxLength) {
|
|
@@ -7234,112 +7390,11 @@ function logAssistantStatusFailure(status, error) {
|
|
|
7234
7390
|
);
|
|
7235
7391
|
}
|
|
7236
7392
|
|
|
7237
|
-
// src/chat/sandbox/
|
|
7238
|
-
|
|
7239
|
-
|
|
7240
|
-
var SANDBOX_RUNTIME = "node22";
|
|
7241
|
-
var SANDBOX_RUNTIME_BIN_DIR = `${SANDBOX_WORKSPACE_ROOT}/.junior/bin`;
|
|
7242
|
-
var EVAL_GH_STUB_PATH = `${SANDBOX_RUNTIME_BIN_DIR}/gh`;
|
|
7243
|
-
var SNAPSHOT_BOOT_RETRY_COUNT = 3;
|
|
7244
|
-
var SNAPSHOT_BOOT_RETRY_DELAY_MS = 1e3;
|
|
7245
|
-
var SANDBOX_ERROR_FIELDS = [
|
|
7246
|
-
{
|
|
7247
|
-
sourceKey: "sandboxId",
|
|
7248
|
-
attributeKey: "sandbox_id",
|
|
7249
|
-
summaryKey: "sandboxId"
|
|
7250
|
-
}
|
|
7251
|
-
];
|
|
7252
|
-
function mergeNetworkPolicyWithHeaderTransforms(networkPolicy, headerTransforms) {
|
|
7253
|
-
const basePolicy = networkPolicy && typeof networkPolicy === "object" && !Array.isArray(networkPolicy) ? { ...networkPolicy } : {};
|
|
7254
|
-
const existingAllowRaw = basePolicy.allow;
|
|
7255
|
-
const existingAllow = existingAllowRaw && typeof existingAllowRaw === "object" && !Array.isArray(existingAllowRaw) ? Object.fromEntries(
|
|
7256
|
-
Object.entries(existingAllowRaw).map(
|
|
7257
|
-
([domain, rules]) => [
|
|
7258
|
-
domain,
|
|
7259
|
-
Array.isArray(rules) ? [...rules] : []
|
|
7260
|
-
]
|
|
7261
|
-
)
|
|
7262
|
-
) : { "*": [] };
|
|
7263
|
-
for (const transform of headerTransforms) {
|
|
7264
|
-
const currentRules = existingAllow[transform.domain] ?? [];
|
|
7265
|
-
existingAllow[transform.domain] = [
|
|
7266
|
-
...currentRules,
|
|
7267
|
-
{ transform: [{ headers: transform.headers }] }
|
|
7268
|
-
];
|
|
7269
|
-
}
|
|
7270
|
-
return {
|
|
7271
|
-
...basePolicy,
|
|
7272
|
-
allow: existingAllow
|
|
7273
|
-
};
|
|
7274
|
-
}
|
|
7275
|
-
function truncateOutput(output, maxLength) {
|
|
7276
|
-
if (output.length <= maxLength) {
|
|
7277
|
-
return { value: output, truncated: false };
|
|
7278
|
-
}
|
|
7279
|
-
const truncatedLength = output.length - maxLength;
|
|
7280
|
-
return {
|
|
7281
|
-
value: `${output.slice(0, maxLength)}
|
|
7393
|
+
// src/chat/sandbox/skill-sync.ts
|
|
7394
|
+
import fs3 from "fs/promises";
|
|
7395
|
+
import path5 from "path";
|
|
7282
7396
|
|
|
7283
|
-
|
|
7284
|
-
truncated: true
|
|
7285
|
-
};
|
|
7286
|
-
}
|
|
7287
|
-
function toPosixRelative(base, absolute) {
|
|
7288
|
-
return path4.relative(base, absolute).split(path4.sep).join("/");
|
|
7289
|
-
}
|
|
7290
|
-
async function listFilesRecursive(root) {
|
|
7291
|
-
const queue = [root];
|
|
7292
|
-
const files = [];
|
|
7293
|
-
while (queue.length > 0) {
|
|
7294
|
-
const dir = queue.shift();
|
|
7295
|
-
const entries = await fs3.readdir(dir, { withFileTypes: true });
|
|
7296
|
-
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
7297
|
-
for (const entry of entries) {
|
|
7298
|
-
const absolute = path4.join(dir, entry.name);
|
|
7299
|
-
if (entry.isDirectory()) {
|
|
7300
|
-
queue.push(absolute);
|
|
7301
|
-
} else if (entry.isFile()) {
|
|
7302
|
-
files.push(absolute);
|
|
7303
|
-
}
|
|
7304
|
-
}
|
|
7305
|
-
}
|
|
7306
|
-
return files;
|
|
7307
|
-
}
|
|
7308
|
-
async function buildSkillSyncFiles(availableSkills) {
|
|
7309
|
-
const filesToWrite = [];
|
|
7310
|
-
const index = {
|
|
7311
|
-
skills: []
|
|
7312
|
-
};
|
|
7313
|
-
for (const skill of availableSkills) {
|
|
7314
|
-
const skillFiles = await listFilesRecursive(skill.skillPath);
|
|
7315
|
-
for (const absoluteFile of skillFiles) {
|
|
7316
|
-
const relative = toPosixRelative(skill.skillPath, absoluteFile);
|
|
7317
|
-
if (!relative || relative.startsWith("..")) {
|
|
7318
|
-
continue;
|
|
7319
|
-
}
|
|
7320
|
-
filesToWrite.push({
|
|
7321
|
-
path: `${sandboxSkillDir(skill.name)}/${relative}`,
|
|
7322
|
-
content: await fs3.readFile(absoluteFile)
|
|
7323
|
-
});
|
|
7324
|
-
}
|
|
7325
|
-
index.skills.push({
|
|
7326
|
-
name: skill.name,
|
|
7327
|
-
description: skill.description,
|
|
7328
|
-
root: sandboxSkillDir(skill.name)
|
|
7329
|
-
});
|
|
7330
|
-
}
|
|
7331
|
-
filesToWrite.push({
|
|
7332
|
-
path: `${SANDBOX_SKILLS_ROOT}/index.json`,
|
|
7333
|
-
content: Buffer.from(JSON.stringify(index), "utf8")
|
|
7334
|
-
});
|
|
7335
|
-
if (process.env.EVAL_ENABLE_TEST_CREDENTIALS === "1") {
|
|
7336
|
-
filesToWrite.push({
|
|
7337
|
-
path: EVAL_GH_STUB_PATH,
|
|
7338
|
-
content: Buffer.from(buildEvalGitHubCliStub(), "utf8")
|
|
7339
|
-
});
|
|
7340
|
-
}
|
|
7341
|
-
return filesToWrite;
|
|
7342
|
-
}
|
|
7397
|
+
// src/chat/sandbox/eval-gh-stub.ts
|
|
7343
7398
|
function buildEvalGitHubCliStub() {
|
|
7344
7399
|
return `#!/usr/bin/env node
|
|
7345
7400
|
const fs = require("node:fs");
|
|
@@ -7517,6 +7572,8 @@ if (args[0] === "api") {
|
|
|
7517
7572
|
outputJson({ items: [] });
|
|
7518
7573
|
process.exit(0);
|
|
7519
7574
|
}
|
|
7575
|
+
outputJson({});
|
|
7576
|
+
process.exit(0);
|
|
7520
7577
|
}
|
|
7521
7578
|
|
|
7522
7579
|
if (args[0] === "issue") {
|
|
@@ -7558,7 +7615,9 @@ if (args[0] === "issue") {
|
|
|
7558
7615
|
|
|
7559
7616
|
const number = Number.parseInt(positionals[2] || "", 10);
|
|
7560
7617
|
const key = repo + "#" + number;
|
|
7561
|
-
const record =
|
|
7618
|
+
const record =
|
|
7619
|
+
state.issues[key] ||
|
|
7620
|
+
defaultIssue(repo, Number.isFinite(number) ? number : 101);
|
|
7562
7621
|
|
|
7563
7622
|
if (subcommand === "view") {
|
|
7564
7623
|
const jsonFields = getFlag("--json");
|
|
@@ -7599,134 +7658,379 @@ if (args[0] === "issue") {
|
|
|
7599
7658
|
fallbackToRealGh();
|
|
7600
7659
|
`;
|
|
7601
7660
|
}
|
|
7602
|
-
|
|
7603
|
-
|
|
7604
|
-
|
|
7605
|
-
|
|
7606
|
-
|
|
7607
|
-
|
|
7608
|
-
|
|
7609
|
-
|
|
7610
|
-
|
|
7661
|
+
|
|
7662
|
+
// src/chat/sandbox/skill-sync.ts
|
|
7663
|
+
function toPosixRelative(base, absolute) {
|
|
7664
|
+
return path5.relative(base, absolute).split(path5.sep).join("/");
|
|
7665
|
+
}
|
|
7666
|
+
async function listFilesRecursive(root) {
|
|
7667
|
+
const queue = [root];
|
|
7668
|
+
const files = [];
|
|
7669
|
+
while (queue.length > 0) {
|
|
7670
|
+
const dir = queue.shift();
|
|
7671
|
+
const entries = await fs3.readdir(dir, { withFileTypes: true });
|
|
7672
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
7673
|
+
for (const entry of entries) {
|
|
7674
|
+
const absolute = path5.join(dir, entry.name);
|
|
7675
|
+
if (entry.isDirectory()) {
|
|
7676
|
+
queue.push(absolute);
|
|
7677
|
+
} else if (entry.isFile()) {
|
|
7678
|
+
files.push(absolute);
|
|
7679
|
+
}
|
|
7611
7680
|
}
|
|
7612
7681
|
}
|
|
7613
|
-
return
|
|
7614
|
-
(directory) => directory === SANDBOX_WORKSPACE_ROOT || directory.startsWith(`${SANDBOX_WORKSPACE_ROOT}/`)
|
|
7615
|
-
).sort((a, b) => a.length - b.length);
|
|
7616
|
-
}
|
|
7617
|
-
function getSandboxErrorDetails(error) {
|
|
7618
|
-
return extractHttpErrorDetails(error, {
|
|
7619
|
-
attributePrefix: "app.sandbox.api_error",
|
|
7620
|
-
extraFields: [...SANDBOX_ERROR_FIELDS]
|
|
7621
|
-
});
|
|
7682
|
+
return files;
|
|
7622
7683
|
}
|
|
7623
|
-
function
|
|
7624
|
-
|
|
7625
|
-
|
|
7684
|
+
async function buildSkillSyncFiles(availableSkills, runtimeBinDir, referenceFiles) {
|
|
7685
|
+
const filesToWrite = [];
|
|
7686
|
+
const index = {
|
|
7687
|
+
skills: []
|
|
7688
|
+
};
|
|
7689
|
+
for (const skill of availableSkills) {
|
|
7690
|
+
const skillFiles = await listFilesRecursive(skill.skillPath);
|
|
7691
|
+
for (const absoluteFile of skillFiles) {
|
|
7692
|
+
const relative = toPosixRelative(skill.skillPath, absoluteFile);
|
|
7693
|
+
if (!relative || relative.startsWith("..")) {
|
|
7694
|
+
continue;
|
|
7695
|
+
}
|
|
7696
|
+
filesToWrite.push({
|
|
7697
|
+
path: `${sandboxSkillDir(skill.name)}/${relative}`,
|
|
7698
|
+
content: await fs3.readFile(absoluteFile)
|
|
7699
|
+
});
|
|
7700
|
+
}
|
|
7701
|
+
index.skills.push({
|
|
7702
|
+
name: skill.name,
|
|
7703
|
+
description: skill.description,
|
|
7704
|
+
root: sandboxSkillDir(skill.name)
|
|
7705
|
+
});
|
|
7706
|
+
}
|
|
7707
|
+
filesToWrite.push({
|
|
7708
|
+
path: `${SANDBOX_SKILLS_ROOT}/index.json`,
|
|
7709
|
+
content: Buffer.from(JSON.stringify(index), "utf8")
|
|
7626
7710
|
});
|
|
7711
|
+
if (referenceFiles && referenceFiles.length > 0) {
|
|
7712
|
+
for (const absoluteFile of referenceFiles) {
|
|
7713
|
+
const fileName = path5.basename(absoluteFile);
|
|
7714
|
+
filesToWrite.push({
|
|
7715
|
+
path: `${SANDBOX_DATA_ROOT}/${fileName}`,
|
|
7716
|
+
content: await fs3.readFile(absoluteFile)
|
|
7717
|
+
});
|
|
7718
|
+
}
|
|
7719
|
+
}
|
|
7720
|
+
if (process.env.EVAL_ENABLE_TEST_CREDENTIALS === "1") {
|
|
7721
|
+
filesToWrite.push({
|
|
7722
|
+
path: `${runtimeBinDir}/gh`,
|
|
7723
|
+
content: Buffer.from(buildEvalGitHubCliStub(), "utf8")
|
|
7724
|
+
});
|
|
7725
|
+
}
|
|
7726
|
+
return filesToWrite;
|
|
7627
7727
|
}
|
|
7628
|
-
function
|
|
7629
|
-
const
|
|
7630
|
-
|
|
7728
|
+
function collectDirectories(filesToWrite, workspaceRoot) {
|
|
7729
|
+
const directoriesToEnsure = /* @__PURE__ */ new Set();
|
|
7730
|
+
for (const file of filesToWrite) {
|
|
7731
|
+
const normalizedPath = path5.posix.normalize(file.path);
|
|
7732
|
+
const parts = normalizedPath.split("/").filter(Boolean);
|
|
7733
|
+
let current = "";
|
|
7734
|
+
for (let index = 0; index < parts.length - 1; index += 1) {
|
|
7735
|
+
current = `${current}/${parts[index]}`;
|
|
7736
|
+
directoriesToEnsure.add(current);
|
|
7737
|
+
}
|
|
7738
|
+
}
|
|
7739
|
+
return Array.from(directoriesToEnsure).filter(
|
|
7740
|
+
(directory) => directory === workspaceRoot || directory.startsWith(`${workspaceRoot}/`)
|
|
7741
|
+
).sort((a, b) => a.length - b.length);
|
|
7631
7742
|
}
|
|
7632
|
-
function
|
|
7633
|
-
const
|
|
7634
|
-
|
|
7635
|
-
|
|
7636
|
-
if (
|
|
7637
|
-
|
|
7743
|
+
function resolveHostSkillPath(availableSkills, sandboxPath) {
|
|
7744
|
+
const normalizedPath = path5.posix.normalize(sandboxPath.trim());
|
|
7745
|
+
for (const skill of availableSkills) {
|
|
7746
|
+
const virtualRoot = sandboxSkillDir(skill.name);
|
|
7747
|
+
if (normalizedPath !== virtualRoot && !normalizedPath.startsWith(`${virtualRoot}/`)) {
|
|
7748
|
+
continue;
|
|
7638
7749
|
}
|
|
7639
|
-
|
|
7640
|
-
if (
|
|
7641
|
-
|
|
7642
|
-
} else {
|
|
7643
|
-
current = void 0;
|
|
7750
|
+
const relativePath = path5.posix.relative(virtualRoot, normalizedPath);
|
|
7751
|
+
if (!relativePath || relativePath.startsWith("../")) {
|
|
7752
|
+
return null;
|
|
7644
7753
|
}
|
|
7754
|
+
const hostRoot = path5.resolve(skill.skillPath);
|
|
7755
|
+
const hostPath = path5.resolve(hostRoot, ...relativePath.split("/"));
|
|
7756
|
+
if (hostPath !== hostRoot && !hostPath.startsWith(`${hostRoot}${path5.sep}`)) {
|
|
7757
|
+
return null;
|
|
7758
|
+
}
|
|
7759
|
+
return hostPath;
|
|
7645
7760
|
}
|
|
7646
|
-
return
|
|
7647
|
-
}
|
|
7648
|
-
function isSandboxUnavailableError(error) {
|
|
7649
|
-
return findInErrorChain(error, (candidate) => {
|
|
7650
|
-
const details = getSandboxErrorDetails(candidate);
|
|
7651
|
-
const searchable = `${details.searchableText} ${details.summary}`.toLowerCase();
|
|
7652
|
-
return searchable.includes("sandbox_stopped") || searchable.includes("status=410") || searchable.includes("status code 410") || searchable.includes("no longer available");
|
|
7653
|
-
});
|
|
7654
|
-
}
|
|
7655
|
-
function isSnapshottingError(error) {
|
|
7656
|
-
return findInErrorChain(error, (candidate) => {
|
|
7657
|
-
const details = getSandboxErrorDetails(candidate);
|
|
7658
|
-
const searchable = `${details.searchableText} ${details.summary}`.toLowerCase();
|
|
7659
|
-
return searchable.includes("sandbox_snapshotting") || searchable.includes("creating a snapshot") || searchable.includes("stopped shortly");
|
|
7660
|
-
});
|
|
7761
|
+
return null;
|
|
7661
7762
|
}
|
|
7662
|
-
function
|
|
7663
|
-
const
|
|
7664
|
-
|
|
7665
|
-
|
|
7666
|
-
|
|
7667
|
-
|
|
7668
|
-
|
|
7669
|
-
|
|
7670
|
-
|
|
7763
|
+
function resolveHostDataPath(referenceFiles, sandboxPath) {
|
|
7764
|
+
const normalizedPath = path5.posix.normalize(sandboxPath.trim());
|
|
7765
|
+
if (normalizedPath !== SANDBOX_DATA_ROOT && !normalizedPath.startsWith(`${SANDBOX_DATA_ROOT}/`)) {
|
|
7766
|
+
return null;
|
|
7767
|
+
}
|
|
7768
|
+
const relativePath = path5.posix.relative(SANDBOX_DATA_ROOT, normalizedPath);
|
|
7769
|
+
if (!relativePath || relativePath.startsWith("../") || relativePath.includes("/")) {
|
|
7770
|
+
return null;
|
|
7771
|
+
}
|
|
7772
|
+
for (const hostFile of referenceFiles) {
|
|
7773
|
+
if (path5.basename(hostFile) === relativePath) {
|
|
7774
|
+
return hostFile;
|
|
7671
7775
|
}
|
|
7672
|
-
seen.add(current);
|
|
7673
|
-
current = typeof current === "object" ? current.cause : void 0;
|
|
7674
7776
|
}
|
|
7675
|
-
return
|
|
7777
|
+
return null;
|
|
7676
7778
|
}
|
|
7677
|
-
function
|
|
7678
|
-
|
|
7679
|
-
|
|
7680
|
-
|
|
7681
|
-
|
|
7682
|
-
|
|
7683
|
-
|
|
7779
|
+
function isHostFileMissingError(error) {
|
|
7780
|
+
return Boolean(
|
|
7781
|
+
error && typeof error === "object" && error.code === "ENOENT"
|
|
7782
|
+
);
|
|
7783
|
+
}
|
|
7784
|
+
async function syncSkillsToSandbox(params) {
|
|
7785
|
+
const workspaceRoot = params.workspaceRoot ?? SANDBOX_WORKSPACE_ROOT;
|
|
7786
|
+
await params.withSpan(
|
|
7787
|
+
"sandbox.sync_skills",
|
|
7788
|
+
"sandbox.sync",
|
|
7789
|
+
{
|
|
7790
|
+
"app.sandbox.skills_count": params.skills.length
|
|
7791
|
+
},
|
|
7792
|
+
async () => {
|
|
7793
|
+
const filesToWrite = await buildSkillSyncFiles(
|
|
7794
|
+
params.skills,
|
|
7795
|
+
params.runtimeBinDir,
|
|
7796
|
+
params.referenceFiles
|
|
7797
|
+
);
|
|
7798
|
+
const bytesWritten = filesToWrite.reduce(
|
|
7799
|
+
(total, file) => total + file.content.length,
|
|
7800
|
+
0
|
|
7801
|
+
);
|
|
7802
|
+
const directories = collectDirectories(filesToWrite, workspaceRoot);
|
|
7803
|
+
await params.withSpan(
|
|
7804
|
+
"sandbox.sync_writeFiles",
|
|
7805
|
+
"sandbox.sync.write",
|
|
7806
|
+
{
|
|
7807
|
+
"app.sandbox.sync.files_written": filesToWrite.length,
|
|
7808
|
+
"app.sandbox.sync.bytes_written": bytesWritten,
|
|
7809
|
+
"app.sandbox.sync.directories_ensured": directories.length
|
|
7810
|
+
},
|
|
7811
|
+
async () => {
|
|
7812
|
+
try {
|
|
7813
|
+
for (const directory of directories) {
|
|
7814
|
+
try {
|
|
7815
|
+
await params.sandbox.mkDir(directory);
|
|
7816
|
+
} catch (error) {
|
|
7817
|
+
if (!isAlreadyExistsError(error)) {
|
|
7818
|
+
throw error;
|
|
7819
|
+
}
|
|
7820
|
+
}
|
|
7821
|
+
}
|
|
7822
|
+
await params.sandbox.writeFiles(filesToWrite);
|
|
7823
|
+
const executableFiles = filesToWrite.map((file) => file.path).filter(
|
|
7824
|
+
(filePath) => filePath.startsWith(`${params.runtimeBinDir}/`)
|
|
7825
|
+
);
|
|
7826
|
+
for (const filePath of executableFiles) {
|
|
7827
|
+
const chmod = await runNonInteractiveCommand(params.sandbox, {
|
|
7828
|
+
cmd: "chmod",
|
|
7829
|
+
args: ["0755", filePath],
|
|
7830
|
+
cwd: workspaceRoot
|
|
7831
|
+
});
|
|
7832
|
+
if (chmod.exitCode !== 0) {
|
|
7833
|
+
throw new Error(
|
|
7834
|
+
`sandbox chmod failed for ${filePath}: ${await chmod.stderr() || await chmod.stdout() || `exit ${chmod.exitCode}`}`
|
|
7835
|
+
);
|
|
7836
|
+
}
|
|
7837
|
+
}
|
|
7838
|
+
} catch (error) {
|
|
7839
|
+
throwSandboxOperationError("sandbox writeFiles", error, true);
|
|
7840
|
+
}
|
|
7841
|
+
}
|
|
7842
|
+
);
|
|
7684
7843
|
}
|
|
7685
|
-
|
|
7686
|
-
|
|
7687
|
-
|
|
7688
|
-
|
|
7689
|
-
|
|
7690
|
-
|
|
7691
|
-
|
|
7844
|
+
);
|
|
7845
|
+
}
|
|
7846
|
+
|
|
7847
|
+
// src/chat/sandbox/session.ts
|
|
7848
|
+
var DEFAULT_MAX_OUTPUT_LENGTH = 3e4;
|
|
7849
|
+
var SANDBOX_RUNTIME = "node22";
|
|
7850
|
+
var SANDBOX_RUNTIME_BIN_DIR = `${SANDBOX_WORKSPACE_ROOT}/.junior/bin`;
|
|
7851
|
+
var SNAPSHOT_BOOT_RETRY_COUNT = 3;
|
|
7852
|
+
var SNAPSHOT_BOOT_RETRY_DELAY_MS = 1e3;
|
|
7853
|
+
var SNAPSHOT_PHASE_STATUS = {
|
|
7854
|
+
resolve_start: { kind: "loading", context: "sandbox snapshot cache" },
|
|
7855
|
+
waiting_for_lock: { kind: "loading", context: "sandbox snapshot build" },
|
|
7856
|
+
building_snapshot: { kind: "creating", context: "sandbox snapshot" },
|
|
7857
|
+
cache_hit: { kind: "loading", context: "sandbox snapshot" }
|
|
7858
|
+
};
|
|
7859
|
+
function mergeNetworkPolicyWithHeaderTransforms(networkPolicy, headerTransforms) {
|
|
7860
|
+
const basePolicy = networkPolicy && typeof networkPolicy === "object" && !Array.isArray(networkPolicy) ? { ...networkPolicy } : {};
|
|
7861
|
+
const existingAllowRaw = basePolicy.allow;
|
|
7862
|
+
const existingAllow = existingAllowRaw && typeof existingAllowRaw === "object" && !Array.isArray(existingAllowRaw) ? Object.fromEntries(
|
|
7863
|
+
Object.entries(existingAllowRaw).map(
|
|
7864
|
+
([domain, rules]) => [
|
|
7865
|
+
domain,
|
|
7866
|
+
Array.isArray(rules) ? [...rules] : []
|
|
7867
|
+
]
|
|
7868
|
+
)
|
|
7869
|
+
) : { "*": [] };
|
|
7870
|
+
for (const transform of headerTransforms) {
|
|
7871
|
+
const currentRules = existingAllow[transform.domain] ?? [];
|
|
7872
|
+
existingAllow[transform.domain] = [
|
|
7873
|
+
...currentRules,
|
|
7874
|
+
{ transform: [{ headers: transform.headers }] }
|
|
7875
|
+
];
|
|
7692
7876
|
}
|
|
7693
|
-
|
|
7694
|
-
|
|
7695
|
-
|
|
7877
|
+
return {
|
|
7878
|
+
...basePolicy,
|
|
7879
|
+
allow: existingAllow
|
|
7880
|
+
};
|
|
7881
|
+
}
|
|
7882
|
+
function truncateOutput(output, maxLength) {
|
|
7883
|
+
if (output.length <= maxLength) {
|
|
7884
|
+
return { value: output, truncated: false };
|
|
7696
7885
|
}
|
|
7697
|
-
|
|
7886
|
+
const truncatedLength = output.length - maxLength;
|
|
7887
|
+
return {
|
|
7888
|
+
value: `${output.slice(0, maxLength)}
|
|
7889
|
+
|
|
7890
|
+
[output truncated: ${truncatedLength} characters removed]`,
|
|
7891
|
+
truncated: true
|
|
7892
|
+
};
|
|
7698
7893
|
}
|
|
7699
|
-
function
|
|
7700
|
-
|
|
7701
|
-
|
|
7702
|
-
...details.attributes,
|
|
7703
|
-
...includeMissingPath ? {
|
|
7704
|
-
"app.sandbox.api_error.missing_path": details.searchableText.includes("no such file") || details.searchableText.includes("enoent")
|
|
7705
|
-
} : {},
|
|
7706
|
-
"app.sandbox.success": false
|
|
7894
|
+
function sleep2(ms) {
|
|
7895
|
+
return new Promise((resolve) => {
|
|
7896
|
+
setTimeout(resolve, ms);
|
|
7707
7897
|
});
|
|
7708
|
-
|
|
7709
|
-
|
|
7710
|
-
|
|
7711
|
-
|
|
7712
|
-
|
|
7898
|
+
}
|
|
7899
|
+
function createStatusEmitter(emitStatus) {
|
|
7900
|
+
let statusCount = 0;
|
|
7901
|
+
const sentStatuses = /* @__PURE__ */ new Set();
|
|
7902
|
+
const emit = async (status) => {
|
|
7903
|
+
const statusKey = `${status.kind}:${status.context ?? ""}`;
|
|
7904
|
+
if (!emitStatus || statusCount >= 4 || sentStatuses.has(statusKey)) {
|
|
7905
|
+
return;
|
|
7713
7906
|
}
|
|
7907
|
+
sentStatuses.add(statusKey);
|
|
7908
|
+
statusCount += 1;
|
|
7909
|
+
await emitStatus(status);
|
|
7910
|
+
};
|
|
7911
|
+
const reportSnapshotPhase = async (phase) => {
|
|
7912
|
+
const status = SNAPSHOT_PHASE_STATUS[phase];
|
|
7913
|
+
if (status) {
|
|
7914
|
+
await emit(makeAssistantStatus(status.kind, status.context));
|
|
7915
|
+
}
|
|
7916
|
+
};
|
|
7917
|
+
return { emit, reportSnapshotPhase };
|
|
7918
|
+
}
|
|
7919
|
+
function parseKeepAliveMs() {
|
|
7920
|
+
const parsed = Number.parseInt(
|
|
7921
|
+
process.env.VERCEL_SANDBOX_KEEPALIVE_MS ?? "0",
|
|
7922
|
+
10
|
|
7714
7923
|
);
|
|
7924
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
|
|
7715
7925
|
}
|
|
7716
|
-
function
|
|
7926
|
+
function createSandboxSessionManager(options) {
|
|
7717
7927
|
let sandbox = null;
|
|
7718
7928
|
let sandboxIdHint = options?.sandboxId;
|
|
7719
7929
|
let availableSkills = [];
|
|
7930
|
+
let availableReferenceFiles = [];
|
|
7720
7931
|
let toolExecutors;
|
|
7721
7932
|
const timeoutMs = options?.timeoutMs ?? 1e3 * 60 * 30;
|
|
7722
7933
|
const traceContext = options?.traceContext ?? {};
|
|
7723
|
-
const emitStatus = options?.onStatus;
|
|
7724
7934
|
const dependencyProfileHash = getRuntimeDependencyProfileHash(SANDBOX_RUNTIME);
|
|
7725
7935
|
const withSandboxSpan = (name, op, attributes, callback) => withSpan(name, op, traceContext, callback, attributes);
|
|
7726
|
-
const
|
|
7936
|
+
const emitSandboxStatus = async (source, statusEmitter, status) => {
|
|
7937
|
+
logInfo(
|
|
7938
|
+
"sandbox_status_emitted",
|
|
7939
|
+
traceContext,
|
|
7940
|
+
{
|
|
7941
|
+
"app.sandbox.status.source": source,
|
|
7942
|
+
"app.sandbox.status.kind": status.kind,
|
|
7943
|
+
...status.context ? { "app.sandbox.status.context": status.context } : {}
|
|
7944
|
+
},
|
|
7945
|
+
"Sandbox status emitted"
|
|
7946
|
+
);
|
|
7947
|
+
if (typeof statusEmitter === "function") {
|
|
7948
|
+
await statusEmitter(status);
|
|
7949
|
+
return;
|
|
7950
|
+
}
|
|
7951
|
+
await statusEmitter.emit(status);
|
|
7952
|
+
};
|
|
7953
|
+
const clearSession = () => {
|
|
7954
|
+
sandbox = null;
|
|
7955
|
+
sandboxIdHint = void 0;
|
|
7956
|
+
toolExecutors = void 0;
|
|
7957
|
+
};
|
|
7958
|
+
const rememberSandbox = (nextSandbox) => {
|
|
7959
|
+
sandbox = nextSandbox;
|
|
7960
|
+
sandboxIdHint = nextSandbox.sandboxId;
|
|
7961
|
+
toolExecutors = void 0;
|
|
7962
|
+
return nextSandbox;
|
|
7963
|
+
};
|
|
7964
|
+
const failSetup = (error) => {
|
|
7965
|
+
throw wrapSandboxSetupError(error);
|
|
7966
|
+
};
|
|
7967
|
+
const syncSkills = async (targetSandbox) => {
|
|
7968
|
+
await syncSkillsToSandbox({
|
|
7969
|
+
sandbox: targetSandbox,
|
|
7970
|
+
skills: availableSkills,
|
|
7971
|
+
referenceFiles: availableReferenceFiles,
|
|
7972
|
+
withSpan: withSandboxSpan,
|
|
7973
|
+
runtimeBinDir: SANDBOX_RUNTIME_BIN_DIR
|
|
7974
|
+
});
|
|
7975
|
+
};
|
|
7976
|
+
const ensureSandboxReachable = async (targetSandbox, source) => {
|
|
7977
|
+
await withSandboxSpan(
|
|
7978
|
+
"sandbox.reuse_probe",
|
|
7979
|
+
"sandbox.acquire.probe",
|
|
7980
|
+
{
|
|
7981
|
+
"app.sandbox.reused": true,
|
|
7982
|
+
"app.sandbox.source": source
|
|
7983
|
+
},
|
|
7984
|
+
async () => {
|
|
7985
|
+
try {
|
|
7986
|
+
await targetSandbox.mkDir(SANDBOX_WORKSPACE_ROOT);
|
|
7987
|
+
} catch (error) {
|
|
7988
|
+
if (!isAlreadyExistsError(error)) {
|
|
7989
|
+
throw error;
|
|
7990
|
+
}
|
|
7991
|
+
}
|
|
7992
|
+
}
|
|
7993
|
+
);
|
|
7994
|
+
};
|
|
7995
|
+
const invalidateSandboxInstance = async (targetSandbox, reason) => {
|
|
7996
|
+
if (sandbox === targetSandbox) {
|
|
7997
|
+
clearSession();
|
|
7998
|
+
}
|
|
7999
|
+
logWarn(
|
|
8000
|
+
"sandbox_network_policy_restore_failed",
|
|
8001
|
+
traceContext,
|
|
8002
|
+
{
|
|
8003
|
+
"error.message": reason instanceof Error ? reason.message : String(reason)
|
|
8004
|
+
},
|
|
8005
|
+
"Sandbox network policy restore failed; discarding sandbox instance"
|
|
8006
|
+
);
|
|
8007
|
+
try {
|
|
8008
|
+
await targetSandbox.stop({ blocking: true });
|
|
8009
|
+
} catch {
|
|
8010
|
+
}
|
|
8011
|
+
};
|
|
8012
|
+
const recreateUnavailableSandbox = async (source) => {
|
|
8013
|
+
setSpanAttributes({
|
|
8014
|
+
"app.sandbox.recovery.attempted": true,
|
|
8015
|
+
"app.sandbox.recovery.source": source
|
|
8016
|
+
});
|
|
8017
|
+
clearSession();
|
|
8018
|
+
const replacement = await createFreshSandbox();
|
|
8019
|
+
setSpanAttributes({
|
|
8020
|
+
"app.sandbox.recovery.succeeded": true
|
|
8021
|
+
});
|
|
8022
|
+
return replacement;
|
|
8023
|
+
};
|
|
8024
|
+
const createSandboxFromSnapshot = async (snapshotId, sandboxCredentials, emitStatus) => {
|
|
7727
8025
|
for (let attempt = 0; attempt < SNAPSHOT_BOOT_RETRY_COUNT; attempt += 1) {
|
|
7728
8026
|
try {
|
|
7729
|
-
|
|
8027
|
+
if (emitStatus) {
|
|
8028
|
+
await emitSandboxStatus(
|
|
8029
|
+
"snapshot_boot",
|
|
8030
|
+
emitStatus,
|
|
8031
|
+
makeAssistantStatus("loading", "sandbox")
|
|
8032
|
+
);
|
|
8033
|
+
}
|
|
7730
8034
|
return await Sandbox.create({
|
|
7731
8035
|
timeout: timeoutMs,
|
|
7732
8036
|
source: {
|
|
@@ -7744,309 +8048,265 @@ function createSandboxExecutor(options) {
|
|
|
7744
8048
|
}
|
|
7745
8049
|
throw new Error(`Failed to boot sandbox from snapshot ${snapshotId}`);
|
|
7746
8050
|
};
|
|
7747
|
-
const
|
|
7748
|
-
|
|
7749
|
-
sandbox
|
|
7750
|
-
|
|
7751
|
-
|
|
8051
|
+
const setSnapshotAttributes = (snapshot) => {
|
|
8052
|
+
setSpanAttributes({
|
|
8053
|
+
"app.sandbox.source": snapshot.snapshotId ? "snapshot" : "created",
|
|
8054
|
+
"app.sandbox.snapshot.cache_hit": snapshot.cacheHit,
|
|
8055
|
+
"app.sandbox.snapshot.resolve_outcome": snapshot.resolveOutcome,
|
|
8056
|
+
...snapshot.profileHash ? {
|
|
8057
|
+
"app.sandbox.snapshot.profile_hash": snapshot.profileHash
|
|
8058
|
+
} : {},
|
|
8059
|
+
"app.sandbox.snapshot.dependency_count": snapshot.dependencyCount,
|
|
8060
|
+
...snapshot.rebuildReason ? {
|
|
8061
|
+
"app.sandbox.snapshot.rebuild_reason": snapshot.rebuildReason
|
|
8062
|
+
} : {}
|
|
8063
|
+
});
|
|
8064
|
+
};
|
|
8065
|
+
const createSandboxFromResolvedSnapshot = async (params) => {
|
|
8066
|
+
const { runtime, snapshot, sandboxCredentials, status } = params;
|
|
8067
|
+
if (!snapshot.snapshotId) {
|
|
8068
|
+
await emitSandboxStatus(
|
|
8069
|
+
"fresh_runtime_boot",
|
|
8070
|
+
status,
|
|
8071
|
+
makeAssistantStatus("loading", "sandbox")
|
|
8072
|
+
);
|
|
8073
|
+
return await Sandbox.create({
|
|
8074
|
+
timeout: timeoutMs,
|
|
8075
|
+
runtime,
|
|
8076
|
+
...sandboxCredentials ?? {}
|
|
8077
|
+
});
|
|
7752
8078
|
}
|
|
7753
|
-
logWarn(
|
|
7754
|
-
"sandbox_network_policy_restore_failed",
|
|
7755
|
-
traceContext,
|
|
7756
|
-
{
|
|
7757
|
-
"error.message": reason instanceof Error ? reason.message : String(reason)
|
|
7758
|
-
},
|
|
7759
|
-
"Sandbox network policy restore failed; discarding sandbox instance"
|
|
7760
|
-
);
|
|
7761
8079
|
try {
|
|
7762
|
-
await
|
|
7763
|
-
|
|
7764
|
-
|
|
7765
|
-
|
|
7766
|
-
|
|
7767
|
-
|
|
7768
|
-
|
|
7769
|
-
|
|
7770
|
-
{
|
|
7771
|
-
"app.sandbox.skills_count": availableSkills.length
|
|
7772
|
-
},
|
|
7773
|
-
async () => {
|
|
7774
|
-
const filesToWrite = await buildSkillSyncFiles(availableSkills);
|
|
7775
|
-
const bytesWritten = filesToWrite.reduce(
|
|
7776
|
-
(total, file) => total + file.content.length,
|
|
7777
|
-
0
|
|
7778
|
-
);
|
|
7779
|
-
const directories = collectDirectories(filesToWrite);
|
|
7780
|
-
await withSandboxSpan(
|
|
7781
|
-
"sandbox.sync_writeFiles",
|
|
7782
|
-
"sandbox.sync.write",
|
|
7783
|
-
{
|
|
7784
|
-
"app.sandbox.sync.files_written": filesToWrite.length,
|
|
7785
|
-
"app.sandbox.sync.bytes_written": bytesWritten,
|
|
7786
|
-
"app.sandbox.sync.directories_ensured": directories.length
|
|
7787
|
-
},
|
|
7788
|
-
async () => {
|
|
7789
|
-
try {
|
|
7790
|
-
for (const directory of directories) {
|
|
7791
|
-
try {
|
|
7792
|
-
await targetSandbox.mkDir(directory);
|
|
7793
|
-
} catch (error) {
|
|
7794
|
-
if (!isAlreadyExistsError(error)) {
|
|
7795
|
-
throw error;
|
|
7796
|
-
}
|
|
7797
|
-
}
|
|
7798
|
-
}
|
|
7799
|
-
await targetSandbox.writeFiles(filesToWrite);
|
|
7800
|
-
const executableFiles = filesToWrite.map((file) => file.path).filter(
|
|
7801
|
-
(filePath) => filePath.startsWith(`${SANDBOX_RUNTIME_BIN_DIR}/`)
|
|
7802
|
-
);
|
|
7803
|
-
for (const filePath of executableFiles) {
|
|
7804
|
-
const chmod = await runNonInteractiveCommand(targetSandbox, {
|
|
7805
|
-
cmd: "chmod",
|
|
7806
|
-
args: ["0755", filePath],
|
|
7807
|
-
cwd: SANDBOX_WORKSPACE_ROOT
|
|
7808
|
-
});
|
|
7809
|
-
if (chmod.exitCode !== 0) {
|
|
7810
|
-
throw new Error(
|
|
7811
|
-
`sandbox chmod failed for ${filePath}: ${await chmod.stderr() || await chmod.stdout() || `exit ${chmod.exitCode}`}`
|
|
7812
|
-
);
|
|
7813
|
-
}
|
|
7814
|
-
}
|
|
7815
|
-
} catch (error) {
|
|
7816
|
-
throwSandboxOperationError("sandbox writeFiles", error, true);
|
|
7817
|
-
}
|
|
7818
|
-
}
|
|
7819
|
-
);
|
|
8080
|
+
return await createSandboxFromSnapshot(
|
|
8081
|
+
snapshot.snapshotId,
|
|
8082
|
+
sandboxCredentials,
|
|
8083
|
+
status.emit
|
|
8084
|
+
);
|
|
8085
|
+
} catch (error) {
|
|
8086
|
+
if (!isSnapshotMissingError(error)) {
|
|
8087
|
+
throw error;
|
|
7820
8088
|
}
|
|
7821
|
-
|
|
8089
|
+
setSpanAttributes({
|
|
8090
|
+
"app.sandbox.snapshot.rebuild_after_missing": true
|
|
8091
|
+
});
|
|
8092
|
+
const rebuiltSnapshot = await resolveRuntimeDependencySnapshot({
|
|
8093
|
+
runtime,
|
|
8094
|
+
timeoutMs,
|
|
8095
|
+
forceRebuild: true,
|
|
8096
|
+
staleSnapshotId: snapshot.snapshotId,
|
|
8097
|
+
onProgress: status.reportSnapshotPhase
|
|
8098
|
+
});
|
|
8099
|
+
if (!rebuiltSnapshot.snapshotId) {
|
|
8100
|
+
throw error;
|
|
8101
|
+
}
|
|
8102
|
+
return await createSandboxFromSnapshot(
|
|
8103
|
+
rebuiltSnapshot.snapshotId,
|
|
8104
|
+
sandboxCredentials,
|
|
8105
|
+
status.emit
|
|
8106
|
+
);
|
|
8107
|
+
}
|
|
7822
8108
|
};
|
|
7823
|
-
const
|
|
7824
|
-
|
|
7825
|
-
|
|
7826
|
-
|
|
7827
|
-
|
|
7828
|
-
|
|
7829
|
-
|
|
7830
|
-
"
|
|
7831
|
-
"
|
|
7832
|
-
|
|
7833
|
-
|
|
7834
|
-
|
|
7835
|
-
|
|
7836
|
-
|
|
7837
|
-
|
|
7838
|
-
|
|
7839
|
-
|
|
7840
|
-
|
|
7841
|
-
|
|
7842
|
-
|
|
7843
|
-
|
|
7844
|
-
|
|
7845
|
-
|
|
7846
|
-
|
|
7847
|
-
|
|
7848
|
-
|
|
7849
|
-
|
|
7850
|
-
|
|
7851
|
-
|
|
7852
|
-
|
|
7853
|
-
|
|
7854
|
-
|
|
7855
|
-
await emitStatus(status);
|
|
7856
|
-
};
|
|
7857
|
-
const reportSnapshotPhase = async (phase) => {
|
|
7858
|
-
if (phase === "resolve_start") {
|
|
7859
|
-
await emitSandboxStatus(
|
|
7860
|
-
makeAssistantStatus("loading", "sandbox snapshot cache")
|
|
7861
|
-
);
|
|
7862
|
-
return;
|
|
7863
|
-
}
|
|
7864
|
-
if (phase === "waiting_for_lock") {
|
|
7865
|
-
await emitSandboxStatus(
|
|
7866
|
-
makeAssistantStatus("loading", "sandbox snapshot build")
|
|
7867
|
-
);
|
|
7868
|
-
return;
|
|
7869
|
-
}
|
|
7870
|
-
if (phase === "building_snapshot") {
|
|
7871
|
-
await emitSandboxStatus(
|
|
7872
|
-
makeAssistantStatus("creating", "sandbox snapshot")
|
|
7873
|
-
);
|
|
7874
|
-
return;
|
|
7875
|
-
}
|
|
7876
|
-
if (phase === "cache_hit") {
|
|
7877
|
-
await emitSandboxStatus(
|
|
7878
|
-
makeAssistantStatus("loading", "sandbox snapshot")
|
|
7879
|
-
);
|
|
7880
|
-
}
|
|
7881
|
-
};
|
|
7882
|
-
let createdSandbox;
|
|
7883
|
-
try {
|
|
7884
|
-
createdSandbox = await withSandboxSpan(
|
|
7885
|
-
"sandbox.create",
|
|
7886
|
-
"sandbox.create",
|
|
7887
|
-
{
|
|
7888
|
-
"app.sandbox.reused": false,
|
|
7889
|
-
"app.sandbox.timeout_ms": timeoutMs,
|
|
7890
|
-
"app.sandbox.runtime": runtime
|
|
7891
|
-
},
|
|
7892
|
-
async () => {
|
|
7893
|
-
await emitSandboxStatus(
|
|
7894
|
-
makeAssistantStatus("loading", "sandbox runtime")
|
|
7895
|
-
);
|
|
7896
|
-
const snapshot = await resolveRuntimeDependencySnapshot({
|
|
7897
|
-
runtime,
|
|
7898
|
-
timeoutMs,
|
|
7899
|
-
onProgress: reportSnapshotPhase
|
|
7900
|
-
});
|
|
7901
|
-
setSpanAttributes({
|
|
7902
|
-
"app.sandbox.source": snapshot.snapshotId ? "snapshot" : "created",
|
|
7903
|
-
"app.sandbox.snapshot.cache_hit": snapshot.cacheHit,
|
|
7904
|
-
"app.sandbox.snapshot.resolve_outcome": snapshot.resolveOutcome,
|
|
7905
|
-
...snapshot.profileHash ? {
|
|
7906
|
-
"app.sandbox.snapshot.profile_hash": snapshot.profileHash
|
|
7907
|
-
} : {},
|
|
7908
|
-
"app.sandbox.snapshot.dependency_count": snapshot.dependencyCount,
|
|
7909
|
-
...snapshot.rebuildReason ? {
|
|
7910
|
-
"app.sandbox.snapshot.rebuild_reason": snapshot.rebuildReason
|
|
7911
|
-
} : {}
|
|
7912
|
-
});
|
|
7913
|
-
if (!snapshot.snapshotId) {
|
|
7914
|
-
await emitSandboxStatus(
|
|
7915
|
-
makeAssistantStatus("loading", "sandbox")
|
|
7916
|
-
);
|
|
7917
|
-
return await Sandbox.create({
|
|
7918
|
-
timeout: timeoutMs,
|
|
7919
|
-
runtime,
|
|
7920
|
-
...sandboxCredentials ?? {}
|
|
7921
|
-
});
|
|
7922
|
-
}
|
|
7923
|
-
try {
|
|
7924
|
-
return await createSandboxFromSnapshot(
|
|
7925
|
-
snapshot.snapshotId,
|
|
7926
|
-
sandboxCredentials,
|
|
7927
|
-
emitSandboxStatus
|
|
7928
|
-
);
|
|
7929
|
-
} catch (error) {
|
|
7930
|
-
if (!isSnapshotMissingError(error)) {
|
|
7931
|
-
throw error;
|
|
7932
|
-
}
|
|
7933
|
-
setSpanAttributes({
|
|
7934
|
-
"app.sandbox.snapshot.rebuild_after_missing": true
|
|
7935
|
-
});
|
|
7936
|
-
const rebuiltSnapshot = await resolveRuntimeDependencySnapshot({
|
|
7937
|
-
runtime,
|
|
7938
|
-
timeoutMs,
|
|
7939
|
-
forceRebuild: true,
|
|
7940
|
-
staleSnapshotId: snapshot.snapshotId,
|
|
7941
|
-
onProgress: reportSnapshotPhase
|
|
7942
|
-
});
|
|
7943
|
-
if (!rebuiltSnapshot.snapshotId) {
|
|
7944
|
-
throw error;
|
|
7945
|
-
}
|
|
7946
|
-
return await createSandboxFromSnapshot(
|
|
7947
|
-
rebuiltSnapshot.snapshotId,
|
|
7948
|
-
sandboxCredentials,
|
|
7949
|
-
emitSandboxStatus
|
|
7950
|
-
);
|
|
7951
|
-
}
|
|
7952
|
-
}
|
|
7953
|
-
);
|
|
7954
|
-
} catch (error) {
|
|
7955
|
-
return handleSetupFailure(error);
|
|
7956
|
-
}
|
|
7957
|
-
try {
|
|
7958
|
-
await upsertSkillsToSandbox(createdSandbox);
|
|
7959
|
-
} catch (error) {
|
|
7960
|
-
return handleSetupFailure(error);
|
|
7961
|
-
}
|
|
7962
|
-
return assignSandbox(createdSandbox);
|
|
7963
|
-
};
|
|
7964
|
-
if (!sandbox && sandboxIdHint && dependencyProfileHash !== options?.sandboxDependencyProfileHash) {
|
|
7965
|
-
setSpanAttributes({
|
|
7966
|
-
"app.sandbox.reused": false,
|
|
7967
|
-
"app.sandbox.recreate.reason": "dependency_profile_mismatch",
|
|
7968
|
-
...options?.sandboxDependencyProfileHash ? {
|
|
7969
|
-
"app.sandbox.previous_profile_hash": options.sandboxDependencyProfileHash
|
|
7970
|
-
} : {},
|
|
7971
|
-
...dependencyProfileHash ? { "app.sandbox.current_profile_hash": dependencyProfileHash } : {}
|
|
7972
|
-
});
|
|
7973
|
-
sandboxIdHint = void 0;
|
|
7974
|
-
}
|
|
7975
|
-
const recoverUnavailableSandbox = async (source) => {
|
|
7976
|
-
setSpanAttributes({
|
|
7977
|
-
"app.sandbox.recovery.attempted": true,
|
|
7978
|
-
"app.sandbox.recovery.source": source
|
|
7979
|
-
});
|
|
7980
|
-
sandbox = null;
|
|
7981
|
-
sandboxIdHint = void 0;
|
|
7982
|
-
toolExecutors = void 0;
|
|
7983
|
-
const replacement = await createFreshSandbox();
|
|
7984
|
-
setSpanAttributes({
|
|
7985
|
-
"app.sandbox.recovery.succeeded": true
|
|
7986
|
-
});
|
|
7987
|
-
return replacement;
|
|
7988
|
-
};
|
|
7989
|
-
if (sandbox) {
|
|
7990
|
-
const cachedSandbox = sandbox;
|
|
7991
|
-
try {
|
|
7992
|
-
await withSandboxSpan(
|
|
7993
|
-
"sandbox.reuse_cached",
|
|
7994
|
-
"sandbox.acquire.cached",
|
|
7995
|
-
{
|
|
7996
|
-
"app.sandbox.reused": true,
|
|
7997
|
-
"app.sandbox.source": "memory"
|
|
7998
|
-
},
|
|
7999
|
-
async () => {
|
|
8000
|
-
await upsertSkillsToSandbox(cachedSandbox);
|
|
8001
|
-
}
|
|
8002
|
-
);
|
|
8003
|
-
return cachedSandbox;
|
|
8004
|
-
} catch (error) {
|
|
8005
|
-
if (isSandboxUnavailableError(error)) {
|
|
8006
|
-
return recoverUnavailableSandbox("memory");
|
|
8007
|
-
}
|
|
8008
|
-
return handleSetupFailure(error);
|
|
8009
|
-
}
|
|
8109
|
+
const createFreshSandbox = async () => {
|
|
8110
|
+
const runtime = SANDBOX_RUNTIME;
|
|
8111
|
+
const sandboxCredentials = getVercelSandboxCredentials();
|
|
8112
|
+
const status = createStatusEmitter(options?.onStatus);
|
|
8113
|
+
let createdSandbox;
|
|
8114
|
+
try {
|
|
8115
|
+
createdSandbox = await withSandboxSpan(
|
|
8116
|
+
"sandbox.create",
|
|
8117
|
+
"sandbox.create",
|
|
8118
|
+
{
|
|
8119
|
+
"app.sandbox.reused": false,
|
|
8120
|
+
"app.sandbox.timeout_ms": timeoutMs,
|
|
8121
|
+
"app.sandbox.runtime": runtime
|
|
8122
|
+
},
|
|
8123
|
+
async () => {
|
|
8124
|
+
await emitSandboxStatus(
|
|
8125
|
+
"runtime_dependency_resolve",
|
|
8126
|
+
status,
|
|
8127
|
+
makeAssistantStatus("loading", "sandbox runtime")
|
|
8128
|
+
);
|
|
8129
|
+
const snapshot = await resolveRuntimeDependencySnapshot({
|
|
8130
|
+
runtime,
|
|
8131
|
+
timeoutMs,
|
|
8132
|
+
onProgress: status.reportSnapshotPhase
|
|
8133
|
+
});
|
|
8134
|
+
setSnapshotAttributes(snapshot);
|
|
8135
|
+
return await createSandboxFromResolvedSnapshot({
|
|
8136
|
+
runtime,
|
|
8137
|
+
snapshot,
|
|
8138
|
+
sandboxCredentials,
|
|
8139
|
+
status
|
|
8140
|
+
});
|
|
8010
8141
|
}
|
|
8011
|
-
|
|
8012
|
-
|
|
8013
|
-
|
|
8014
|
-
|
|
8015
|
-
|
|
8016
|
-
|
|
8017
|
-
|
|
8018
|
-
|
|
8019
|
-
|
|
8020
|
-
|
|
8021
|
-
|
|
8022
|
-
|
|
8023
|
-
|
|
8024
|
-
|
|
8025
|
-
|
|
8026
|
-
|
|
8027
|
-
|
|
8028
|
-
|
|
8142
|
+
);
|
|
8143
|
+
} catch (error) {
|
|
8144
|
+
return failSetup(error);
|
|
8145
|
+
}
|
|
8146
|
+
try {
|
|
8147
|
+
await syncSkills(createdSandbox);
|
|
8148
|
+
} catch (error) {
|
|
8149
|
+
return failSetup(error);
|
|
8150
|
+
}
|
|
8151
|
+
return rememberSandbox(createdSandbox);
|
|
8152
|
+
};
|
|
8153
|
+
const discardHintIfProfileChanged = () => {
|
|
8154
|
+
if (sandbox || !sandboxIdHint || dependencyProfileHash === options?.sandboxDependencyProfileHash) {
|
|
8155
|
+
return;
|
|
8156
|
+
}
|
|
8157
|
+
setSpanAttributes({
|
|
8158
|
+
"app.sandbox.reused": false,
|
|
8159
|
+
"app.sandbox.recreate.reason": "dependency_profile_mismatch",
|
|
8160
|
+
...options?.sandboxDependencyProfileHash ? {
|
|
8161
|
+
"app.sandbox.previous_profile_hash": options.sandboxDependencyProfileHash
|
|
8162
|
+
} : {},
|
|
8163
|
+
...dependencyProfileHash ? { "app.sandbox.current_profile_hash": dependencyProfileHash } : {}
|
|
8164
|
+
});
|
|
8165
|
+
sandboxIdHint = void 0;
|
|
8166
|
+
};
|
|
8167
|
+
const tryReuseCachedSandbox = async () => {
|
|
8168
|
+
const cachedSandbox = sandbox;
|
|
8169
|
+
if (!cachedSandbox) {
|
|
8170
|
+
return null;
|
|
8171
|
+
}
|
|
8172
|
+
try {
|
|
8173
|
+
await ensureSandboxReachable(cachedSandbox, "memory");
|
|
8174
|
+
return cachedSandbox;
|
|
8175
|
+
} catch (error) {
|
|
8176
|
+
if (isSandboxUnavailableError(error)) {
|
|
8177
|
+
return await recreateUnavailableSandbox("memory");
|
|
8178
|
+
}
|
|
8179
|
+
return failSetup(error);
|
|
8180
|
+
}
|
|
8181
|
+
};
|
|
8182
|
+
const tryRestoreHintedSandbox = async () => {
|
|
8183
|
+
if (!sandboxIdHint) {
|
|
8184
|
+
return null;
|
|
8185
|
+
}
|
|
8186
|
+
let hintedSandbox = null;
|
|
8187
|
+
try {
|
|
8188
|
+
const sandboxCredentials = getVercelSandboxCredentials();
|
|
8189
|
+
hintedSandbox = await withSandboxSpan(
|
|
8190
|
+
"sandbox.get",
|
|
8191
|
+
"sandbox.get",
|
|
8192
|
+
{
|
|
8193
|
+
"app.sandbox.reused": true,
|
|
8194
|
+
"app.sandbox.source": "id_hint"
|
|
8195
|
+
},
|
|
8196
|
+
async () => await Sandbox.get({
|
|
8197
|
+
sandboxId: sandboxIdHint,
|
|
8198
|
+
...sandboxCredentials ?? {}
|
|
8199
|
+
})
|
|
8200
|
+
);
|
|
8201
|
+
} catch {
|
|
8202
|
+
return null;
|
|
8203
|
+
}
|
|
8204
|
+
try {
|
|
8205
|
+
await syncSkills(hintedSandbox);
|
|
8206
|
+
return rememberSandbox(hintedSandbox);
|
|
8207
|
+
} catch (error) {
|
|
8208
|
+
if (isSandboxUnavailableError(error)) {
|
|
8209
|
+
return await recreateUnavailableSandbox("id_hint");
|
|
8210
|
+
}
|
|
8211
|
+
return failSetup(error);
|
|
8212
|
+
}
|
|
8213
|
+
};
|
|
8214
|
+
const acquireSandbox = async () => {
|
|
8215
|
+
return await withSandboxSpan(
|
|
8216
|
+
"sandbox.acquire",
|
|
8217
|
+
"sandbox.acquire",
|
|
8218
|
+
{
|
|
8219
|
+
"app.sandbox.id_hint_present": Boolean(sandboxIdHint),
|
|
8220
|
+
"app.sandbox.timeout_ms": timeoutMs,
|
|
8221
|
+
"app.sandbox.runtime": SANDBOX_RUNTIME,
|
|
8222
|
+
"app.sandbox.skills_count": availableSkills.length
|
|
8223
|
+
},
|
|
8224
|
+
async () => {
|
|
8225
|
+
discardHintIfProfileChanged();
|
|
8226
|
+
const cachedSandbox = await tryReuseCachedSandbox();
|
|
8227
|
+
if (cachedSandbox) {
|
|
8228
|
+
return cachedSandbox;
|
|
8029
8229
|
}
|
|
8030
|
-
|
|
8031
|
-
|
|
8032
|
-
|
|
8033
|
-
return assignSandbox(acquiredSandbox);
|
|
8034
|
-
} catch (error) {
|
|
8035
|
-
if (isSandboxUnavailableError(error)) {
|
|
8036
|
-
return recoverUnavailableSandbox("id_hint");
|
|
8037
|
-
}
|
|
8038
|
-
return handleSetupFailure(error);
|
|
8039
|
-
}
|
|
8230
|
+
const hintedSandbox = await tryRestoreHintedSandbox();
|
|
8231
|
+
if (hintedSandbox) {
|
|
8232
|
+
return hintedSandbox;
|
|
8040
8233
|
}
|
|
8041
|
-
return createFreshSandbox();
|
|
8234
|
+
return await createFreshSandbox();
|
|
8042
8235
|
}
|
|
8043
8236
|
);
|
|
8044
8237
|
};
|
|
8045
|
-
const
|
|
8046
|
-
|
|
8047
|
-
|
|
8238
|
+
const getMaxOutputLength = () => {
|
|
8239
|
+
const maxOutputLength = Number.parseInt(
|
|
8240
|
+
process.env.SANDBOX_BASH_MAX_OUTPUT_CHARS ?? "",
|
|
8241
|
+
10
|
|
8242
|
+
);
|
|
8243
|
+
return Number.isFinite(maxOutputLength) && maxOutputLength > 0 ? maxOutputLength : DEFAULT_MAX_OUTPUT_LENGTH;
|
|
8244
|
+
};
|
|
8245
|
+
const readCommandOutput = async (commandResult2) => {
|
|
8246
|
+
const boundedOutputLength = getMaxOutputLength();
|
|
8247
|
+
const stdoutRaw = await commandResult2.stdout();
|
|
8248
|
+
const stderrRaw = await commandResult2.stderr();
|
|
8249
|
+
const stdout = truncateOutput(stdoutRaw, boundedOutputLength);
|
|
8250
|
+
const stderr = truncateOutput(stderrRaw, boundedOutputLength);
|
|
8251
|
+
return {
|
|
8252
|
+
stdout: stdout.value,
|
|
8253
|
+
stderr: stderr.value,
|
|
8254
|
+
exitCode: commandResult2.exitCode,
|
|
8255
|
+
stdoutTruncated: stdout.truncated,
|
|
8256
|
+
stderrTruncated: stderr.truncated
|
|
8257
|
+
};
|
|
8258
|
+
};
|
|
8259
|
+
const withTemporaryHeaderTransforms = async (sandboxInstance, headerTransforms, callback) => {
|
|
8260
|
+
if (!headerTransforms || headerTransforms.length === 0) {
|
|
8261
|
+
return await callback();
|
|
8262
|
+
}
|
|
8263
|
+
const restoreNetworkPolicy = sandboxInstance.networkPolicy ?? "allow-all";
|
|
8264
|
+
const policy = mergeNetworkPolicyWithHeaderTransforms(
|
|
8265
|
+
restoreNetworkPolicy,
|
|
8266
|
+
headerTransforms
|
|
8267
|
+
);
|
|
8268
|
+
await sandboxInstance.updateNetworkPolicy(policy);
|
|
8269
|
+
let callbackError;
|
|
8270
|
+
let restoreError;
|
|
8271
|
+
let result;
|
|
8272
|
+
try {
|
|
8273
|
+
result = await callback();
|
|
8274
|
+
} catch (error) {
|
|
8275
|
+
callbackError = error;
|
|
8276
|
+
throw error;
|
|
8277
|
+
} finally {
|
|
8278
|
+
try {
|
|
8279
|
+
await sandboxInstance.updateNetworkPolicy(restoreNetworkPolicy);
|
|
8280
|
+
} catch (error) {
|
|
8281
|
+
restoreError = error;
|
|
8282
|
+
await invalidateSandboxInstance(sandboxInstance, error);
|
|
8283
|
+
}
|
|
8048
8284
|
}
|
|
8049
|
-
|
|
8285
|
+
if (restoreError && !callbackError) {
|
|
8286
|
+
throw restoreError;
|
|
8287
|
+
}
|
|
8288
|
+
return result;
|
|
8289
|
+
};
|
|
8290
|
+
const extendKeepAlive = async (activeSandbox) => {
|
|
8291
|
+
const keepAliveMs = parseKeepAliveMs();
|
|
8292
|
+
if (keepAliveMs === 0) {
|
|
8293
|
+
return;
|
|
8294
|
+
}
|
|
8295
|
+
try {
|
|
8296
|
+
await withSandboxSpan(
|
|
8297
|
+
"sandbox.keepalive.extend",
|
|
8298
|
+
"sandbox.keepalive",
|
|
8299
|
+
{
|
|
8300
|
+
"app.sandbox.keepalive_ms": keepAliveMs
|
|
8301
|
+
},
|
|
8302
|
+
async () => {
|
|
8303
|
+
await activeSandbox.extendTimeout(keepAliveMs);
|
|
8304
|
+
}
|
|
8305
|
+
);
|
|
8306
|
+
} catch {
|
|
8307
|
+
}
|
|
8308
|
+
};
|
|
8309
|
+
const buildToolExecutors = async (sandboxInstance) => {
|
|
8050
8310
|
const toolkit = await withSandboxSpan(
|
|
8051
8311
|
"sandbox.bash_tool.init",
|
|
8052
8312
|
"sandbox.tool.init",
|
|
@@ -8054,8 +8314,8 @@ function createSandboxExecutor(options) {
|
|
|
8054
8314
|
"app.sandbox.tool_name": "bash",
|
|
8055
8315
|
"app.sandbox.destination": SANDBOX_WORKSPACE_ROOT
|
|
8056
8316
|
},
|
|
8057
|
-
async () => createBashTool2({
|
|
8058
|
-
sandbox:
|
|
8317
|
+
async () => await createBashTool2({
|
|
8318
|
+
sandbox: sandboxInstance,
|
|
8059
8319
|
destination: SANDBOX_WORKSPACE_ROOT
|
|
8060
8320
|
})
|
|
8061
8321
|
);
|
|
@@ -8064,63 +8324,24 @@ function createSandboxExecutor(options) {
|
|
|
8064
8324
|
if (!executeReadFile || !executeWriteFile) {
|
|
8065
8325
|
throw new Error("bash-tool did not return executable tool handlers");
|
|
8066
8326
|
}
|
|
8067
|
-
|
|
8327
|
+
return {
|
|
8068
8328
|
bash: async (input) => {
|
|
8069
|
-
const restoreNetworkPolicy = activeSandbox.networkPolicy ?? "allow-all";
|
|
8070
|
-
const headerTransforms = input.headerTransforms;
|
|
8071
|
-
if (headerTransforms && headerTransforms.length > 0) {
|
|
8072
|
-
const policy = mergeNetworkPolicyWithHeaderTransforms(
|
|
8073
|
-
restoreNetworkPolicy,
|
|
8074
|
-
headerTransforms
|
|
8075
|
-
);
|
|
8076
|
-
await activeSandbox.updateNetworkPolicy(policy);
|
|
8077
|
-
}
|
|
8078
8329
|
const script = buildNonInteractiveShellScript(input.command, {
|
|
8079
8330
|
env: input.env,
|
|
8080
8331
|
pathPrefix: `${SANDBOX_RUNTIME_BIN_DIR}:$PATH`
|
|
8081
8332
|
});
|
|
8082
|
-
|
|
8083
|
-
|
|
8084
|
-
|
|
8085
|
-
|
|
8086
|
-
|
|
8087
|
-
|
|
8088
|
-
|
|
8089
|
-
|
|
8090
|
-
|
|
8091
|
-
|
|
8092
|
-
process.env.SANDBOX_BASH_MAX_OUTPUT_CHARS ?? "",
|
|
8093
|
-
10
|
|
8094
|
-
);
|
|
8095
|
-
const boundedOutputLength = Number.isFinite(maxOutputLength) && maxOutputLength > 0 ? maxOutputLength : DEFAULT_MAX_OUTPUT_LENGTH;
|
|
8096
|
-
const stdoutRaw = await commandResult2.stdout();
|
|
8097
|
-
const stderrRaw = await commandResult2.stderr();
|
|
8098
|
-
const stdout = truncateOutput(stdoutRaw, boundedOutputLength);
|
|
8099
|
-
const stderr = truncateOutput(stderrRaw, boundedOutputLength);
|
|
8100
|
-
result = {
|
|
8101
|
-
stdout: stdout.value,
|
|
8102
|
-
stderr: stderr.value,
|
|
8103
|
-
exitCode: commandResult2.exitCode,
|
|
8104
|
-
stdoutTruncated: stdout.truncated,
|
|
8105
|
-
stderrTruncated: stderr.truncated
|
|
8106
|
-
};
|
|
8107
|
-
} catch (error) {
|
|
8108
|
-
commandError = error;
|
|
8109
|
-
throw error;
|
|
8110
|
-
} finally {
|
|
8111
|
-
if (headerTransforms && headerTransforms.length > 0) {
|
|
8112
|
-
try {
|
|
8113
|
-
await activeSandbox.updateNetworkPolicy(restoreNetworkPolicy);
|
|
8114
|
-
} catch (error) {
|
|
8115
|
-
restoreError = error;
|
|
8116
|
-
await invalidateSandboxInstance(activeSandbox, error);
|
|
8117
|
-
}
|
|
8333
|
+
return await withTemporaryHeaderTransforms(
|
|
8334
|
+
sandboxInstance,
|
|
8335
|
+
input.headerTransforms,
|
|
8336
|
+
async () => {
|
|
8337
|
+
const commandResult2 = await sandboxInstance.runCommand({
|
|
8338
|
+
cmd: "bash",
|
|
8339
|
+
args: ["-c", script],
|
|
8340
|
+
cwd: SANDBOX_WORKSPACE_ROOT
|
|
8341
|
+
});
|
|
8342
|
+
return await readCommandOutput(commandResult2);
|
|
8118
8343
|
}
|
|
8119
|
-
|
|
8120
|
-
if (restoreError && !commandError) {
|
|
8121
|
-
throw restoreError;
|
|
8122
|
-
}
|
|
8123
|
-
return result;
|
|
8344
|
+
);
|
|
8124
8345
|
},
|
|
8125
8346
|
readFile: async (input) => await executeReadFile(input, {
|
|
8126
8347
|
toolCallId: "sandbox-read-file",
|
|
@@ -8131,210 +8352,319 @@ function createSandboxExecutor(options) {
|
|
|
8131
8352
|
messages: []
|
|
8132
8353
|
})
|
|
8133
8354
|
};
|
|
8134
|
-
return toolExecutors;
|
|
8135
8355
|
};
|
|
8136
|
-
const
|
|
8137
|
-
const rawInput = params.input ?? {};
|
|
8138
|
-
const bashCommand = params.toolName === "bash" ? String(rawInput.command ?? "").trim() : void 0;
|
|
8139
|
-
if (params.toolName === "bash") {
|
|
8140
|
-
if (!bashCommand) {
|
|
8141
|
-
throw new Error("command is required");
|
|
8142
|
-
}
|
|
8143
|
-
if (options?.runBashCustomCommand) {
|
|
8144
|
-
const custom = await options.runBashCustomCommand(bashCommand);
|
|
8145
|
-
if (custom.handled) {
|
|
8146
|
-
return { result: custom.result };
|
|
8147
|
-
}
|
|
8148
|
-
}
|
|
8149
|
-
}
|
|
8356
|
+
const ensureReadySandbox = async () => {
|
|
8150
8357
|
const activeSandbox = await acquireSandbox();
|
|
8151
|
-
|
|
8152
|
-
|
|
8153
|
-
|
|
8154
|
-
|
|
8155
|
-
if (
|
|
8156
|
-
|
|
8157
|
-
await withSandboxSpan(
|
|
8158
|
-
"sandbox.keepalive.extend",
|
|
8159
|
-
"sandbox.keepalive",
|
|
8160
|
-
{
|
|
8161
|
-
"app.sandbox.keepalive_ms": keepAliveMs
|
|
8162
|
-
},
|
|
8163
|
-
async () => {
|
|
8164
|
-
await activeSandbox.extendTimeout(keepAliveMs);
|
|
8165
|
-
}
|
|
8166
|
-
);
|
|
8167
|
-
} catch {
|
|
8168
|
-
}
|
|
8358
|
+
await extendKeepAlive(activeSandbox);
|
|
8359
|
+
return activeSandbox;
|
|
8360
|
+
};
|
|
8361
|
+
const loadToolExecutors = async (activeSandbox) => {
|
|
8362
|
+
if (toolExecutors) {
|
|
8363
|
+
return toolExecutors;
|
|
8169
8364
|
}
|
|
8170
|
-
|
|
8171
|
-
|
|
8172
|
-
|
|
8173
|
-
|
|
8174
|
-
|
|
8175
|
-
|
|
8176
|
-
|
|
8177
|
-
|
|
8178
|
-
|
|
8179
|
-
|
|
8180
|
-
|
|
8181
|
-
|
|
8182
|
-
|
|
8183
|
-
|
|
8184
|
-
|
|
8185
|
-
|
|
8186
|
-
|
|
8187
|
-
|
|
8188
|
-
|
|
8189
|
-
|
|
8190
|
-
|
|
8191
|
-
|
|
8192
|
-
|
|
8365
|
+
toolExecutors = await buildToolExecutors(activeSandbox);
|
|
8366
|
+
return toolExecutors;
|
|
8367
|
+
};
|
|
8368
|
+
return {
|
|
8369
|
+
configureSkills(skills) {
|
|
8370
|
+
availableSkills = [...skills];
|
|
8371
|
+
},
|
|
8372
|
+
configureReferenceFiles(files) {
|
|
8373
|
+
availableReferenceFiles = [...files];
|
|
8374
|
+
},
|
|
8375
|
+
getSandboxId() {
|
|
8376
|
+
return sandbox?.sandboxId ?? sandboxIdHint;
|
|
8377
|
+
},
|
|
8378
|
+
getDependencyProfileHash() {
|
|
8379
|
+
return dependencyProfileHash;
|
|
8380
|
+
},
|
|
8381
|
+
async createSandbox() {
|
|
8382
|
+
return await acquireSandbox();
|
|
8383
|
+
},
|
|
8384
|
+
async ensureToolExecutors() {
|
|
8385
|
+
return await loadToolExecutors(await ensureReadySandbox());
|
|
8386
|
+
},
|
|
8387
|
+
async dispose() {
|
|
8388
|
+
const activeSandbox = sandbox;
|
|
8389
|
+
if (!activeSandbox) {
|
|
8390
|
+
return;
|
|
8391
|
+
}
|
|
8392
|
+
await withSandboxSpan(
|
|
8393
|
+
"sandbox.stop",
|
|
8394
|
+
"sandbox.stop",
|
|
8193
8395
|
{
|
|
8194
|
-
"
|
|
8396
|
+
"app.sandbox.stop.blocking": true
|
|
8195
8397
|
},
|
|
8196
8398
|
async () => {
|
|
8197
|
-
|
|
8198
|
-
const response = await executeBash({
|
|
8199
|
-
command,
|
|
8200
|
-
...headerTransforms ? { headerTransforms } : {},
|
|
8201
|
-
...env ? { env } : {}
|
|
8202
|
-
});
|
|
8203
|
-
setSpanAttributes({
|
|
8204
|
-
"process.exit.code": response.exitCode,
|
|
8205
|
-
"app.sandbox.stdout_bytes": Buffer.byteLength(
|
|
8206
|
-
response.stdout ?? "",
|
|
8207
|
-
"utf8"
|
|
8208
|
-
),
|
|
8209
|
-
"app.sandbox.stderr_bytes": Buffer.byteLength(
|
|
8210
|
-
response.stderr ?? "",
|
|
8211
|
-
"utf8"
|
|
8212
|
-
),
|
|
8213
|
-
...response.exitCode !== 0 ? { "error.type": "nonzero_exit" } : {}
|
|
8214
|
-
});
|
|
8215
|
-
setSpanStatus(response.exitCode === 0 ? "ok" : "error");
|
|
8216
|
-
return response;
|
|
8217
|
-
} catch (error) {
|
|
8218
|
-
setSpanAttributes({
|
|
8219
|
-
"error.type": error instanceof Error ? error.name : "sandbox_execute_error"
|
|
8220
|
-
});
|
|
8221
|
-
setSpanStatus("error");
|
|
8222
|
-
throw error;
|
|
8223
|
-
}
|
|
8399
|
+
await activeSandbox.stop({ blocking: true });
|
|
8224
8400
|
}
|
|
8225
8401
|
);
|
|
8226
|
-
|
|
8227
|
-
|
|
8228
|
-
ok: result.exitCode === 0,
|
|
8229
|
-
command,
|
|
8230
|
-
cwd: SANDBOX_WORKSPACE_ROOT,
|
|
8231
|
-
exit_code: result.exitCode,
|
|
8232
|
-
signal: null,
|
|
8233
|
-
timed_out: false,
|
|
8234
|
-
stdout: result.stdout,
|
|
8235
|
-
stderr: result.stderr,
|
|
8236
|
-
stdout_truncated: result.stdoutTruncated,
|
|
8237
|
-
stderr_truncated: result.stderrTruncated
|
|
8238
|
-
}
|
|
8239
|
-
};
|
|
8402
|
+
sandbox = null;
|
|
8403
|
+
toolExecutors = void 0;
|
|
8240
8404
|
}
|
|
8241
|
-
|
|
8242
|
-
|
|
8243
|
-
|
|
8244
|
-
|
|
8245
|
-
|
|
8246
|
-
|
|
8247
|
-
|
|
8248
|
-
|
|
8249
|
-
|
|
8250
|
-
|
|
8251
|
-
|
|
8252
|
-
|
|
8253
|
-
|
|
8254
|
-
|
|
8255
|
-
|
|
8405
|
+
};
|
|
8406
|
+
}
|
|
8407
|
+
|
|
8408
|
+
// src/chat/sandbox/sandbox.ts
|
|
8409
|
+
var SANDBOX_TOOL_NAMES = /* @__PURE__ */ new Set(["bash", "readFile", "writeFile"]);
|
|
8410
|
+
function parseHeaderTransforms(raw) {
|
|
8411
|
+
if (!Array.isArray(raw)) {
|
|
8412
|
+
return void 0;
|
|
8413
|
+
}
|
|
8414
|
+
return raw.filter(
|
|
8415
|
+
(value) => Boolean(value && typeof value === "object")
|
|
8416
|
+
).map((transform) => ({
|
|
8417
|
+
domain: String(transform.domain ?? "").trim(),
|
|
8418
|
+
headers: transform.headers && typeof transform.headers === "object" && !Array.isArray(transform.headers) ? Object.fromEntries(
|
|
8419
|
+
Object.entries(transform.headers).filter(([, value]) => typeof value === "string").map(([key, value]) => [key, value])
|
|
8420
|
+
) : {}
|
|
8421
|
+
})).filter(
|
|
8422
|
+
(transform) => transform.domain.length > 0 && Object.keys(transform.headers).length > 0
|
|
8423
|
+
);
|
|
8424
|
+
}
|
|
8425
|
+
function parseEnv(raw) {
|
|
8426
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
8427
|
+
return void 0;
|
|
8428
|
+
}
|
|
8429
|
+
return Object.fromEntries(
|
|
8430
|
+
Object.entries(raw).filter(([, value]) => typeof value === "string").map(([key, value]) => [key, value])
|
|
8431
|
+
);
|
|
8432
|
+
}
|
|
8433
|
+
function createSandboxWorkspace(sandbox) {
|
|
8434
|
+
return {
|
|
8435
|
+
sandboxId: sandbox.sandboxId,
|
|
8436
|
+
readFileToBuffer(input) {
|
|
8437
|
+
return sandbox.readFileToBuffer(input);
|
|
8438
|
+
},
|
|
8439
|
+
runCommand(input) {
|
|
8440
|
+
return sandbox.runCommand(input);
|
|
8441
|
+
}
|
|
8442
|
+
};
|
|
8443
|
+
}
|
|
8444
|
+
function createSandboxExecutor(options) {
|
|
8445
|
+
let availableSkills = [];
|
|
8446
|
+
let referenceFiles = [];
|
|
8447
|
+
const traceContext = options?.traceContext ?? {};
|
|
8448
|
+
const sessionManager = createSandboxSessionManager({
|
|
8449
|
+
sandboxId: options?.sandboxId,
|
|
8450
|
+
sandboxDependencyProfileHash: options?.sandboxDependencyProfileHash,
|
|
8451
|
+
timeoutMs: options?.timeoutMs,
|
|
8452
|
+
traceContext,
|
|
8453
|
+
onStatus: options?.onStatus
|
|
8454
|
+
});
|
|
8455
|
+
const withSandboxSpan = (name, op, attributes, callback) => withSpan(name, op, traceContext, callback, attributes);
|
|
8456
|
+
const logSandboxBootRequest = (trigger, details = {}) => {
|
|
8457
|
+
if (sessionManager.getSandboxId()) {
|
|
8458
|
+
return;
|
|
8459
|
+
}
|
|
8460
|
+
logInfo(
|
|
8461
|
+
"sandbox_boot_requested",
|
|
8462
|
+
traceContext,
|
|
8463
|
+
{
|
|
8464
|
+
"app.sandbox.boot.trigger": trigger,
|
|
8465
|
+
...details
|
|
8466
|
+
},
|
|
8467
|
+
"Sandbox boot requested"
|
|
8468
|
+
);
|
|
8469
|
+
};
|
|
8470
|
+
const executeBashTool = async (rawInput, command) => {
|
|
8471
|
+
const headerTransforms = parseHeaderTransforms(rawInput.headerTransforms);
|
|
8472
|
+
const env = parseEnv(rawInput.env);
|
|
8473
|
+
logSandboxBootRequest("tool.bash", {
|
|
8474
|
+
"app.sandbox.command_length": command.length
|
|
8475
|
+
});
|
|
8476
|
+
const executeBash = (await sessionManager.ensureToolExecutors()).bash;
|
|
8477
|
+
const result = await withSandboxSpan(
|
|
8478
|
+
"bash",
|
|
8479
|
+
"process.exec",
|
|
8480
|
+
{
|
|
8481
|
+
"process.executable.name": "bash"
|
|
8482
|
+
},
|
|
8483
|
+
async () => {
|
|
8484
|
+
try {
|
|
8485
|
+
const response = await executeBash({
|
|
8486
|
+
command,
|
|
8487
|
+
...headerTransforms ? { headerTransforms } : {},
|
|
8488
|
+
...env ? { env } : {}
|
|
8489
|
+
});
|
|
8490
|
+
setSpanAttributes({
|
|
8491
|
+
"process.exit.code": response.exitCode,
|
|
8492
|
+
"app.sandbox.stdout_bytes": Buffer.byteLength(
|
|
8493
|
+
response.stdout ?? "",
|
|
8494
|
+
"utf8"
|
|
8495
|
+
),
|
|
8496
|
+
"app.sandbox.stderr_bytes": Buffer.byteLength(
|
|
8497
|
+
response.stderr ?? "",
|
|
8498
|
+
"utf8"
|
|
8499
|
+
),
|
|
8500
|
+
...response.exitCode !== 0 ? { "error.type": "nonzero_exit" } : {}
|
|
8501
|
+
});
|
|
8502
|
+
setSpanStatus(response.exitCode === 0 ? "ok" : "error");
|
|
8503
|
+
return response;
|
|
8504
|
+
} catch (error) {
|
|
8505
|
+
setSpanAttributes({
|
|
8506
|
+
"error.type": error instanceof Error ? error.name : "sandbox_execute_error"
|
|
8507
|
+
});
|
|
8508
|
+
setSpanStatus("error");
|
|
8509
|
+
throw error;
|
|
8510
|
+
}
|
|
8511
|
+
}
|
|
8512
|
+
);
|
|
8513
|
+
return {
|
|
8514
|
+
result: {
|
|
8515
|
+
ok: result.exitCode === 0,
|
|
8516
|
+
command,
|
|
8517
|
+
cwd: SANDBOX_WORKSPACE_ROOT,
|
|
8518
|
+
exit_code: result.exitCode,
|
|
8519
|
+
signal: null,
|
|
8520
|
+
timed_out: false,
|
|
8521
|
+
stdout: result.stdout,
|
|
8522
|
+
stderr: result.stderr,
|
|
8523
|
+
stdout_truncated: result.stdoutTruncated,
|
|
8524
|
+
stderr_truncated: result.stderrTruncated
|
|
8525
|
+
}
|
|
8526
|
+
};
|
|
8527
|
+
};
|
|
8528
|
+
const executeReadFileTool = async (rawInput) => {
|
|
8529
|
+
const filePath = String(rawInput.path ?? "").trim();
|
|
8530
|
+
if (!filePath) {
|
|
8531
|
+
throw new Error("path is required");
|
|
8532
|
+
}
|
|
8533
|
+
if (!sessionManager.getSandboxId()) {
|
|
8534
|
+
const hostPath = resolveHostSkillPath(availableSkills, filePath) ?? resolveHostDataPath(referenceFiles, filePath);
|
|
8535
|
+
if (hostPath) {
|
|
8536
|
+
try {
|
|
8537
|
+
const content = await fs4.readFile(hostPath, "utf8");
|
|
8256
8538
|
setSpanAttributes({
|
|
8539
|
+
"app.sandbox.path.length": filePath.length,
|
|
8257
8540
|
"app.sandbox.read.bytes": Buffer.byteLength(content, "utf8"),
|
|
8258
|
-
"app.sandbox.read.chars": content.length
|
|
8541
|
+
"app.sandbox.read.chars": content.length,
|
|
8542
|
+
"app.skill.virtual_read": true
|
|
8259
8543
|
});
|
|
8260
8544
|
setSpanStatus("ok");
|
|
8261
8545
|
return {
|
|
8262
|
-
|
|
8263
|
-
|
|
8264
|
-
|
|
8546
|
+
result: {
|
|
8547
|
+
content,
|
|
8548
|
+
path: filePath,
|
|
8549
|
+
success: true
|
|
8550
|
+
}
|
|
8265
8551
|
};
|
|
8266
|
-
}
|
|
8267
|
-
|
|
8268
|
-
|
|
8269
|
-
}
|
|
8270
|
-
if (params.toolName === "writeFile") {
|
|
8271
|
-
const filePath = String(rawInput.path ?? "").trim();
|
|
8272
|
-
if (!filePath) {
|
|
8273
|
-
throw new Error("path is required");
|
|
8274
|
-
}
|
|
8275
|
-
const content = String(rawInput.content ?? "");
|
|
8276
|
-
const executeWriteFile = (await getToolExecutors()).writeFile;
|
|
8277
|
-
await withSandboxSpan(
|
|
8278
|
-
"sandbox.writeFile",
|
|
8279
|
-
"sandbox.fs.write",
|
|
8280
|
-
{
|
|
8281
|
-
"app.sandbox.path.length": filePath.length,
|
|
8282
|
-
"app.sandbox.write.bytes": Buffer.byteLength(content, "utf8")
|
|
8283
|
-
},
|
|
8284
|
-
async () => {
|
|
8285
|
-
try {
|
|
8286
|
-
await executeWriteFile({ path: filePath, content });
|
|
8287
|
-
setSpanStatus("ok");
|
|
8288
|
-
} catch (error) {
|
|
8289
|
-
throwSandboxOperationError("sandbox writeFile", error);
|
|
8552
|
+
} catch (error) {
|
|
8553
|
+
if (!isHostFileMissingError(error)) {
|
|
8554
|
+
throw error;
|
|
8290
8555
|
}
|
|
8291
8556
|
}
|
|
8292
|
-
|
|
8293
|
-
|
|
8294
|
-
|
|
8295
|
-
|
|
8557
|
+
}
|
|
8558
|
+
}
|
|
8559
|
+
logSandboxBootRequest("tool.readFile", {
|
|
8560
|
+
"file.path": filePath
|
|
8561
|
+
});
|
|
8562
|
+
const executeReadFile = (await sessionManager.ensureToolExecutors()).readFile;
|
|
8563
|
+
const result = await withSandboxSpan(
|
|
8564
|
+
"sandbox.readFile",
|
|
8565
|
+
"sandbox.fs.read",
|
|
8566
|
+
{
|
|
8567
|
+
"app.sandbox.path.length": filePath.length
|
|
8568
|
+
},
|
|
8569
|
+
async () => {
|
|
8570
|
+
const response = await executeReadFile({ path: filePath });
|
|
8571
|
+
const content = String(response.content ?? "");
|
|
8572
|
+
setSpanAttributes({
|
|
8573
|
+
"app.sandbox.read.bytes": Buffer.byteLength(content, "utf8"),
|
|
8574
|
+
"app.sandbox.read.chars": content.length
|
|
8575
|
+
});
|
|
8576
|
+
setSpanStatus("ok");
|
|
8577
|
+
return {
|
|
8578
|
+
content,
|
|
8296
8579
|
path: filePath,
|
|
8297
|
-
|
|
8298
|
-
}
|
|
8299
|
-
}
|
|
8300
|
-
|
|
8301
|
-
|
|
8580
|
+
success: true
|
|
8581
|
+
};
|
|
8582
|
+
}
|
|
8583
|
+
);
|
|
8584
|
+
return { result };
|
|
8302
8585
|
};
|
|
8303
|
-
const
|
|
8304
|
-
|
|
8305
|
-
|
|
8306
|
-
|
|
8586
|
+
const executeWriteFileTool = async (rawInput) => {
|
|
8587
|
+
const filePath = String(rawInput.path ?? "").trim();
|
|
8588
|
+
if (!filePath) {
|
|
8589
|
+
throw new Error("path is required");
|
|
8590
|
+
}
|
|
8591
|
+
const content = String(rawInput.content ?? "");
|
|
8592
|
+
logSandboxBootRequest("tool.writeFile", {
|
|
8593
|
+
"file.path": filePath
|
|
8594
|
+
});
|
|
8595
|
+
const executeWriteFile = (await sessionManager.ensureToolExecutors()).writeFile;
|
|
8307
8596
|
await withSandboxSpan(
|
|
8308
|
-
"sandbox.
|
|
8309
|
-
"sandbox.
|
|
8597
|
+
"sandbox.writeFile",
|
|
8598
|
+
"sandbox.fs.write",
|
|
8310
8599
|
{
|
|
8311
|
-
"app.sandbox.
|
|
8600
|
+
"app.sandbox.path.length": filePath.length,
|
|
8601
|
+
"app.sandbox.write.bytes": Buffer.byteLength(content, "utf8")
|
|
8312
8602
|
},
|
|
8313
8603
|
async () => {
|
|
8314
|
-
|
|
8604
|
+
try {
|
|
8605
|
+
await executeWriteFile({ path: filePath, content });
|
|
8606
|
+
} catch (error) {
|
|
8607
|
+
throwSandboxOperationError("sandbox writeFile", error);
|
|
8608
|
+
}
|
|
8609
|
+
setSpanStatus("ok");
|
|
8315
8610
|
}
|
|
8316
8611
|
);
|
|
8317
|
-
|
|
8318
|
-
|
|
8612
|
+
return {
|
|
8613
|
+
result: {
|
|
8614
|
+
ok: true,
|
|
8615
|
+
path: filePath,
|
|
8616
|
+
bytes_written: Buffer.byteLength(content, "utf8")
|
|
8617
|
+
}
|
|
8618
|
+
};
|
|
8619
|
+
};
|
|
8620
|
+
const execute = async (params) => {
|
|
8621
|
+
const rawInput = params.input ?? {};
|
|
8622
|
+
const bashCommand = params.toolName === "bash" ? String(rawInput.command ?? "").trim() : void 0;
|
|
8623
|
+
if (params.toolName === "bash") {
|
|
8624
|
+
if (!bashCommand) {
|
|
8625
|
+
throw new Error("command is required");
|
|
8626
|
+
}
|
|
8627
|
+
if (options?.runBashCustomCommand) {
|
|
8628
|
+
const custom = await options.runBashCustomCommand(bashCommand);
|
|
8629
|
+
if (custom.handled) {
|
|
8630
|
+
return { result: custom.result };
|
|
8631
|
+
}
|
|
8632
|
+
}
|
|
8633
|
+
return await executeBashTool(rawInput, bashCommand);
|
|
8634
|
+
}
|
|
8635
|
+
if (params.toolName === "readFile") {
|
|
8636
|
+
return await executeReadFileTool(rawInput);
|
|
8637
|
+
}
|
|
8638
|
+
if (params.toolName === "writeFile") {
|
|
8639
|
+
return await executeWriteFileTool(rawInput);
|
|
8640
|
+
}
|
|
8641
|
+
throw new Error(`unsupported sandbox tool: ${params.toolName}`);
|
|
8319
8642
|
};
|
|
8320
8643
|
return {
|
|
8321
8644
|
configureSkills(skills) {
|
|
8322
8645
|
availableSkills = [...skills];
|
|
8646
|
+
sessionManager.configureSkills(skills);
|
|
8647
|
+
},
|
|
8648
|
+
configureReferenceFiles(files) {
|
|
8649
|
+
referenceFiles = [...files];
|
|
8650
|
+
sessionManager.configureReferenceFiles(files);
|
|
8323
8651
|
},
|
|
8324
8652
|
getSandboxId() {
|
|
8325
|
-
return
|
|
8653
|
+
return sessionManager.getSandboxId();
|
|
8326
8654
|
},
|
|
8327
8655
|
getDependencyProfileHash() {
|
|
8328
|
-
return
|
|
8656
|
+
return sessionManager.getDependencyProfileHash();
|
|
8329
8657
|
},
|
|
8330
8658
|
canExecute(toolName) {
|
|
8331
8659
|
return SANDBOX_TOOL_NAMES.has(toolName);
|
|
8332
8660
|
},
|
|
8333
8661
|
async createSandbox() {
|
|
8334
|
-
return await
|
|
8662
|
+
return createSandboxWorkspace(await sessionManager.createSandbox());
|
|
8335
8663
|
},
|
|
8336
8664
|
execute,
|
|
8337
|
-
dispose
|
|
8665
|
+
async dispose() {
|
|
8666
|
+
await sessionManager.dispose();
|
|
8667
|
+
}
|
|
8338
8668
|
};
|
|
8339
8669
|
}
|
|
8340
8670
|
|
|
@@ -8347,7 +8677,7 @@ function shouldEmitDevAgentTrace() {
|
|
|
8347
8677
|
function buildToolStatus(toolName, input) {
|
|
8348
8678
|
const obj = input && typeof input === "object" ? input : void 0;
|
|
8349
8679
|
const command = obj ? compactStatusCommand(obj.command) : void 0;
|
|
8350
|
-
const
|
|
8680
|
+
const path7 = obj ? compactStatusPath(obj.path) : void 0;
|
|
8351
8681
|
const filename = obj ? compactStatusFilename(obj.path) : void 0;
|
|
8352
8682
|
const query = obj ? compactStatusText(obj.query, 70) : void 0;
|
|
8353
8683
|
const domain = obj ? extractStatusUrlDomain(obj.url) : void 0;
|
|
@@ -8362,8 +8692,8 @@ function buildToolStatus(toolName, input) {
|
|
|
8362
8692
|
if (filename && toolName === "writeFile") {
|
|
8363
8693
|
return makeAssistantStatus("updating", filename);
|
|
8364
8694
|
}
|
|
8365
|
-
if (
|
|
8366
|
-
return makeAssistantStatus("updating",
|
|
8695
|
+
if (path7 && toolName === "writeFile") {
|
|
8696
|
+
return makeAssistantStatus("updating", path7);
|
|
8367
8697
|
}
|
|
8368
8698
|
if (skillName && toolName === "loadSkill") {
|
|
8369
8699
|
return makeAssistantStatus("loading", skillName);
|
|
@@ -9156,6 +9486,14 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
9156
9486
|
let lastKnownSandboxDependencyProfileHash = context.sandbox?.sandboxDependencyProfileHash;
|
|
9157
9487
|
let loadedSkillNamesForResume = [];
|
|
9158
9488
|
let mcpToolManager;
|
|
9489
|
+
let sandboxExecutor;
|
|
9490
|
+
const getSandboxMetadata = () => sandboxExecutor ? {
|
|
9491
|
+
sandboxId: sandboxExecutor.getSandboxId(),
|
|
9492
|
+
sandboxDependencyProfileHash: sandboxExecutor.getDependencyProfileHash()
|
|
9493
|
+
} : {
|
|
9494
|
+
sandboxId: lastKnownSandboxId,
|
|
9495
|
+
sandboxDependencyProfileHash: lastKnownSandboxDependencyProfileHash
|
|
9496
|
+
};
|
|
9159
9497
|
try {
|
|
9160
9498
|
const shouldTrace = shouldEmitDevAgentTrace();
|
|
9161
9499
|
const spanContext = {
|
|
@@ -9228,7 +9566,7 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
9228
9566
|
resolveConfiguration: async (key) => configurationValues[key]
|
|
9229
9567
|
});
|
|
9230
9568
|
const providerAuthActions = /* @__PURE__ */ new Map();
|
|
9231
|
-
|
|
9569
|
+
sandboxExecutor = createSandboxExecutor({
|
|
9232
9570
|
sandboxId: context.sandbox?.sandboxId,
|
|
9233
9571
|
sandboxDependencyProfileHash: context.sandbox?.sandboxDependencyProfileHash,
|
|
9234
9572
|
traceContext: spanContext,
|
|
@@ -9255,10 +9593,53 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
9255
9593
|
return result.handled ? { handled: true, result: result.result } : { handled: false };
|
|
9256
9594
|
}
|
|
9257
9595
|
});
|
|
9258
|
-
|
|
9259
|
-
lastKnownSandboxDependencyProfileHash = sandboxExecutor.getDependencyProfileHash();
|
|
9596
|
+
const currentSandboxExecutor = sandboxExecutor;
|
|
9260
9597
|
sandboxExecutor.configureSkills(availableSkills);
|
|
9261
|
-
|
|
9598
|
+
sandboxExecutor.configureReferenceFiles(listReferenceFiles());
|
|
9599
|
+
let sandboxPromise;
|
|
9600
|
+
let sandboxPromiseId;
|
|
9601
|
+
const clearSandboxPromise = () => {
|
|
9602
|
+
sandboxPromise = void 0;
|
|
9603
|
+
sandboxPromiseId = void 0;
|
|
9604
|
+
};
|
|
9605
|
+
const getSandbox = (reason) => {
|
|
9606
|
+
const currentSandboxId = currentSandboxExecutor.getSandboxId();
|
|
9607
|
+
if (sandboxPromise && sandboxPromiseId && currentSandboxId !== sandboxPromiseId) {
|
|
9608
|
+
clearSandboxPromise();
|
|
9609
|
+
}
|
|
9610
|
+
if (!sandboxPromise) {
|
|
9611
|
+
logInfo(
|
|
9612
|
+
"sandbox_boot_requested",
|
|
9613
|
+
spanContext,
|
|
9614
|
+
{
|
|
9615
|
+
"app.sandbox.boot.trigger": reason.trigger,
|
|
9616
|
+
...reason.path ? { "file.path": reason.path } : {},
|
|
9617
|
+
...reason.cmd ? { "process.executable.name": reason.cmd } : {},
|
|
9618
|
+
...reason.cwd ? { "file.directory": reason.cwd } : {}
|
|
9619
|
+
},
|
|
9620
|
+
"Lazy sandbox boot requested"
|
|
9621
|
+
);
|
|
9622
|
+
sandboxPromise = currentSandboxExecutor.createSandbox().then((sandbox2) => {
|
|
9623
|
+
sandboxPromiseId = sandbox2.sandboxId;
|
|
9624
|
+
return sandbox2;
|
|
9625
|
+
}).catch((error) => {
|
|
9626
|
+
clearSandboxPromise();
|
|
9627
|
+
throw error;
|
|
9628
|
+
});
|
|
9629
|
+
}
|
|
9630
|
+
return sandboxPromise;
|
|
9631
|
+
};
|
|
9632
|
+
const sandbox = {
|
|
9633
|
+
readFileToBuffer: async (input) => (await getSandbox({
|
|
9634
|
+
trigger: "workspace.readFileToBuffer",
|
|
9635
|
+
path: input.path
|
|
9636
|
+
})).readFileToBuffer(input),
|
|
9637
|
+
runCommand: async (input) => (await getSandbox({
|
|
9638
|
+
trigger: "workspace.runCommand",
|
|
9639
|
+
cmd: input.cmd,
|
|
9640
|
+
cwd: input.cwd
|
|
9641
|
+
})).runCommand(input)
|
|
9642
|
+
};
|
|
9262
9643
|
for (const skillName of existingCheckpoint?.loadedSkillNames ?? []) {
|
|
9263
9644
|
const preloaded = await skillSandbox.loadSkill(skillName);
|
|
9264
9645
|
if (preloaded) {
|
|
@@ -9393,20 +9774,37 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
9393
9774
|
activeSkills,
|
|
9394
9775
|
invokedSkill
|
|
9395
9776
|
),
|
|
9396
|
-
runtimeMetadata: getRuntimeMetadata()
|
|
9777
|
+
runtimeMetadata: getRuntimeMetadata(),
|
|
9778
|
+
threadParticipants: context.threadParticipants
|
|
9397
9779
|
});
|
|
9398
9780
|
const userContentParts = [{ type: "text", text: userTurnText }];
|
|
9399
9781
|
for (const attachment of context.userAttachments ?? []) {
|
|
9400
|
-
if (attachment.
|
|
9782
|
+
if (attachment.promptText) {
|
|
9783
|
+
userContentParts.push({
|
|
9784
|
+
type: "text",
|
|
9785
|
+
text: attachment.promptText
|
|
9786
|
+
});
|
|
9787
|
+
} else if (attachment.mediaType.startsWith("image/")) {
|
|
9788
|
+
if (!attachment.data) {
|
|
9789
|
+
throw new Error("Image attachment is missing image data");
|
|
9790
|
+
}
|
|
9401
9791
|
userContentParts.push({
|
|
9402
9792
|
type: "image",
|
|
9403
9793
|
data: attachment.data.toString("base64"),
|
|
9404
9794
|
mimeType: attachment.mediaType
|
|
9405
9795
|
});
|
|
9406
9796
|
} else {
|
|
9797
|
+
if (!attachment.data) {
|
|
9798
|
+
throw new Error("Attachment is missing attachment data");
|
|
9799
|
+
}
|
|
9800
|
+
const promptAttachment = {
|
|
9801
|
+
data: attachment.data,
|
|
9802
|
+
mediaType: attachment.mediaType,
|
|
9803
|
+
filename: attachment.filename
|
|
9804
|
+
};
|
|
9407
9805
|
userContentParts.push({
|
|
9408
9806
|
type: "text",
|
|
9409
|
-
text: encodeNonImageAttachmentForPrompt(
|
|
9807
|
+
text: encodeNonImageAttachmentForPrompt(promptAttachment)
|
|
9410
9808
|
});
|
|
9411
9809
|
}
|
|
9412
9810
|
}
|
|
@@ -9603,8 +10001,8 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
9603
10001
|
replyFiles,
|
|
9604
10002
|
artifactStatePatch,
|
|
9605
10003
|
toolCalls,
|
|
9606
|
-
sandboxId:
|
|
9607
|
-
sandboxDependencyProfileHash:
|
|
10004
|
+
sandboxId: currentSandboxExecutor.getSandboxId(),
|
|
10005
|
+
sandboxDependencyProfileHash: currentSandboxExecutor.getDependencyProfileHash(),
|
|
9608
10006
|
generatedFileCount: generatedFiles.length,
|
|
9609
10007
|
hasTextDeltaCallback: Boolean(context.onTextDelta),
|
|
9610
10008
|
shouldTrace,
|
|
@@ -9655,8 +10053,7 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
9655
10053
|
const message = error instanceof Error ? error.message : String(error);
|
|
9656
10054
|
return {
|
|
9657
10055
|
text: `Error: ${message}`,
|
|
9658
|
-
|
|
9659
|
-
sandboxDependencyProfileHash: lastKnownSandboxDependencyProfileHash,
|
|
10056
|
+
...getSandboxMetadata(),
|
|
9660
10057
|
diagnostics: {
|
|
9661
10058
|
outcome: "provider_error",
|
|
9662
10059
|
modelId: botConfig.modelId,
|
|
@@ -10260,9 +10657,9 @@ async function GET4(request, provider, waitUntil) {
|
|
|
10260
10657
|
}
|
|
10261
10658
|
|
|
10262
10659
|
// src/chat/slack/app-home.ts
|
|
10263
|
-
import
|
|
10264
|
-
import
|
|
10265
|
-
var
|
|
10660
|
+
import fs5 from "fs";
|
|
10661
|
+
import path6 from "path";
|
|
10662
|
+
var DEFAULT_DESCRIPTION_TEXT = "I help your team investigate, summarize, and act on work in Slack.";
|
|
10266
10663
|
var MAX_HOME_SKILLS = 6;
|
|
10267
10664
|
var MAX_SECTION_TEXT_CHARS = 3e3;
|
|
10268
10665
|
var HIDDEN_HOME_SKILLS = /* @__PURE__ */ new Set(["jr-rpc"]);
|
|
@@ -10272,16 +10669,16 @@ function clampSectionText(text) {
|
|
|
10272
10669
|
}
|
|
10273
10670
|
return `${text.slice(0, MAX_SECTION_TEXT_CHARS - 1)}\u2026`;
|
|
10274
10671
|
}
|
|
10275
|
-
function
|
|
10276
|
-
const
|
|
10672
|
+
function loadDescriptionText() {
|
|
10673
|
+
const descriptionPath = path6.join(homeDir(), "DESCRIPTION.md");
|
|
10277
10674
|
try {
|
|
10278
|
-
const raw =
|
|
10675
|
+
const raw = fs5.readFileSync(descriptionPath, "utf8").trim();
|
|
10279
10676
|
if (raw.length > 0) {
|
|
10280
10677
|
return clampSectionText(raw);
|
|
10281
10678
|
}
|
|
10282
10679
|
} catch {
|
|
10283
10680
|
}
|
|
10284
|
-
return
|
|
10681
|
+
return DEFAULT_DESCRIPTION_TEXT;
|
|
10285
10682
|
}
|
|
10286
10683
|
async function buildSkillsSummaryText() {
|
|
10287
10684
|
const skills = (await discoverSkills()).filter(
|
|
@@ -10301,7 +10698,10 @@ async function buildSkillsSummaryText() {
|
|
|
10301
10698
|
}
|
|
10302
10699
|
async function hasConnectedAccount(userId, plugin, userTokenStore) {
|
|
10303
10700
|
if (plugin.manifest.credentials?.type === "oauth-bearer") {
|
|
10304
|
-
|
|
10701
|
+
const stored = await userTokenStore.get(userId, plugin.manifest.name);
|
|
10702
|
+
return Boolean(
|
|
10703
|
+
stored && hasRequiredOAuthScope(stored.scope, plugin.manifest.oauth?.scope)
|
|
10704
|
+
);
|
|
10305
10705
|
}
|
|
10306
10706
|
if (plugin.manifest.mcp) {
|
|
10307
10707
|
return Boolean(
|
|
@@ -10312,7 +10712,7 @@ async function hasConnectedAccount(userId, plugin, userTokenStore) {
|
|
|
10312
10712
|
}
|
|
10313
10713
|
async function buildHomeView(userId, userTokenStore) {
|
|
10314
10714
|
const runtimeMetadata = getRuntimeMetadata();
|
|
10315
|
-
const
|
|
10715
|
+
const descriptionText = loadDescriptionText();
|
|
10316
10716
|
const skillsSummaryText = await buildSkillsSummaryText();
|
|
10317
10717
|
const providers = getPluginProviders();
|
|
10318
10718
|
const connectedSections = [];
|
|
@@ -10357,7 +10757,7 @@ ${plugin.manifest.description}`
|
|
|
10357
10757
|
type: "section",
|
|
10358
10758
|
text: {
|
|
10359
10759
|
type: "mrkdwn",
|
|
10360
|
-
text:
|
|
10760
|
+
text: descriptionText
|
|
10361
10761
|
}
|
|
10362
10762
|
},
|
|
10363
10763
|
{ type: "divider" },
|
|
@@ -10564,7 +10964,10 @@ async function GET5(request, provider, waitUntil) {
|
|
|
10564
10964
|
const tokenData = await tokenResponse.json();
|
|
10565
10965
|
let parsedTokenResponse;
|
|
10566
10966
|
try {
|
|
10567
|
-
parsedTokenResponse = parseOAuthTokenResponse(
|
|
10967
|
+
parsedTokenResponse = parseOAuthTokenResponse(
|
|
10968
|
+
tokenData,
|
|
10969
|
+
providerConfig.scope
|
|
10970
|
+
);
|
|
10568
10971
|
} catch {
|
|
10569
10972
|
return htmlErrorResponse(
|
|
10570
10973
|
"Connection failed",
|
|
@@ -10572,6 +10975,13 @@ async function GET5(request, provider, waitUntil) {
|
|
|
10572
10975
|
500
|
|
10573
10976
|
);
|
|
10574
10977
|
}
|
|
10978
|
+
if (!hasRequiredOAuthScope(parsedTokenResponse.scope, providerConfig.scope)) {
|
|
10979
|
+
return htmlErrorResponse(
|
|
10980
|
+
"Connection failed",
|
|
10981
|
+
`The ${providerLabel} authorization did not grant the access Junior requires. Return to Slack and ask Junior to connect your ${providerLabel} account again.`,
|
|
10982
|
+
400
|
|
10983
|
+
);
|
|
10984
|
+
}
|
|
10575
10985
|
const userTokenStore = createUserTokenStore();
|
|
10576
10986
|
await userTokenStore.set(stored.userId, provider, parsedTokenResponse);
|
|
10577
10987
|
waitUntil(async () => {
|
|
@@ -10625,7 +11035,7 @@ var replyDecisionSchema = z.object({
|
|
|
10625
11035
|
var ROUTER_CONFIDENCE_THRESHOLD = 0.8;
|
|
10626
11036
|
var LEADING_SLACK_MENTION_RE = /^\s*<@([A-Z0-9]+)(?:\|([^>]+))?>[\s,:-]*/i;
|
|
10627
11037
|
var LEADING_NAMED_MENTION_RE = /^\s*@([a-z0-9._-]+)\b[\s,:-]*/i;
|
|
10628
|
-
var TRANSCRIPT_MESSAGE_LINE_RE = /^\[(assistant|system|user)\]\s+[^:]
|
|
11038
|
+
var TRANSCRIPT_MESSAGE_LINE_RE = /^\[(assistant|system|user)\]\s+([^:]+):\s+([\s\S]+)$/i;
|
|
10629
11039
|
var THREAD_OPTOUT_PATTERNS = [
|
|
10630
11040
|
/\bstop (?:watching|replying|participating)\b/i,
|
|
10631
11041
|
/\bstay out\b/i,
|
|
@@ -10633,6 +11043,11 @@ var THREAD_OPTOUT_PATTERNS = [
|
|
|
10633
11043
|
/\bunsubscribe\b/i,
|
|
10634
11044
|
/\bleave (?:this )?thread\b/i
|
|
10635
11045
|
];
|
|
11046
|
+
var ACKNOWLEDGMENT_ONLY_RE = /^(?:thanks(?: you)?|thank you|thx|ty|got it|sounds good|sgtm|lgtm|ok(?:ay)?|cool|nice|perfect|awesome|great|makes sense|understood|roger|yep|yup|kk|on it|will do)(?:[.!]+)?$/i;
|
|
11047
|
+
var DIRECTED_FOLLOW_UP_CUE_RE = /\b(?:you said|you just said|your last response|your last answer|what did you just say|what do you mean|what did you mean|explain(?: that| this| it| more)?|clarify(?: that| this| it)?|expand(?: on)?(?: that| this| it)?|elaborate(?: on)?(?: that| this| it)?|say more)\b/i;
|
|
11048
|
+
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;
|
|
11049
|
+
var GENERIC_IMMEDIATE_SIDE_CONVERSATION_RE = /^(?:is that (?:the )?right (?:approach|call|move)|(?:can|could|would) you check on this)\??$/i;
|
|
11050
|
+
var RECENT_THREAD_WINDOW = 6;
|
|
10636
11051
|
function escapeRegExp(value) {
|
|
10637
11052
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
10638
11053
|
}
|
|
@@ -10669,36 +11084,113 @@ function isThreadOptOutInstruction(rawText, text) {
|
|
|
10669
11084
|
(pattern) => pattern.test(rawText) || pattern.test(text)
|
|
10670
11085
|
);
|
|
10671
11086
|
}
|
|
10672
|
-
function
|
|
11087
|
+
function isAcknowledgmentOnly(text) {
|
|
11088
|
+
return ACKNOWLEDGMENT_ONLY_RE.test(text.trim());
|
|
11089
|
+
}
|
|
11090
|
+
function hasDirectedFollowUpCue(text) {
|
|
11091
|
+
return DIRECTED_FOLLOW_UP_CUE_RE.test(text.trim());
|
|
11092
|
+
}
|
|
11093
|
+
function isTerseClarification(text) {
|
|
11094
|
+
return TERSE_CLARIFICATION_RE.test(text.trim());
|
|
11095
|
+
}
|
|
11096
|
+
function isGenericImmediateSideConversation(text) {
|
|
11097
|
+
const trimmed = text.trim();
|
|
11098
|
+
if (GENERIC_IMMEDIATE_SIDE_CONVERSATION_RE.test(trimmed)) {
|
|
11099
|
+
return true;
|
|
11100
|
+
}
|
|
11101
|
+
if (!trimmed.toLowerCase().startsWith("what about")) {
|
|
11102
|
+
return false;
|
|
11103
|
+
}
|
|
11104
|
+
const wordCount = trimmed.split(/\s+/).map((part) => part.trim()).filter(Boolean).length;
|
|
11105
|
+
return wordCount > 3;
|
|
11106
|
+
}
|
|
11107
|
+
function parseTranscriptMessages(conversationContext) {
|
|
10673
11108
|
if (!conversationContext) {
|
|
10674
|
-
return
|
|
10675
|
-
latestPriorMessageRole: "[none]",
|
|
10676
|
-
latestPriorAssistantMessage: "[none]"
|
|
10677
|
-
};
|
|
11109
|
+
return [];
|
|
10678
11110
|
}
|
|
11111
|
+
const messages = [];
|
|
10679
11112
|
const lines = conversationContext.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
|
10680
|
-
|
|
10681
|
-
|
|
10682
|
-
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
10683
|
-
const match = lines[index]?.match(TRANSCRIPT_MESSAGE_LINE_RE);
|
|
11113
|
+
for (const line of lines) {
|
|
11114
|
+
const match = line.match(TRANSCRIPT_MESSAGE_LINE_RE);
|
|
10684
11115
|
if (!match) {
|
|
10685
11116
|
continue;
|
|
10686
11117
|
}
|
|
10687
|
-
|
|
10688
|
-
|
|
10689
|
-
|
|
10690
|
-
|
|
10691
|
-
|
|
11118
|
+
messages.push({
|
|
11119
|
+
role: match[1].toLowerCase(),
|
|
11120
|
+
author: match[2]?.trim() || "unknown",
|
|
11121
|
+
text: match[3]?.trim() || ""
|
|
11122
|
+
});
|
|
11123
|
+
}
|
|
11124
|
+
return messages;
|
|
11125
|
+
}
|
|
11126
|
+
function buildRouterSignals(input) {
|
|
11127
|
+
const transcriptMessages = parseTranscriptMessages(input.conversationContext);
|
|
11128
|
+
const recentMessages = transcriptMessages.filter((message) => message.role !== "system").slice(-RECENT_THREAD_WINDOW);
|
|
11129
|
+
const latestPriorMessage = [...transcriptMessages].reverse().find((message) => message.role !== "system");
|
|
11130
|
+
const latestPriorAssistantMessage = [...transcriptMessages].reverse().find((message) => message.role === "assistant");
|
|
11131
|
+
let humanMessagesSinceLastAssistant;
|
|
11132
|
+
let humanMessageCount = 0;
|
|
11133
|
+
for (let index = transcriptMessages.length - 1; index >= 0; index -= 1) {
|
|
11134
|
+
const message = transcriptMessages[index];
|
|
11135
|
+
if (!message || message.role === "system") {
|
|
11136
|
+
continue;
|
|
10692
11137
|
}
|
|
10693
|
-
if (
|
|
11138
|
+
if (message.role === "assistant") {
|
|
11139
|
+
humanMessagesSinceLastAssistant = humanMessageCount;
|
|
10694
11140
|
break;
|
|
10695
11141
|
}
|
|
11142
|
+
humanMessageCount += 1;
|
|
10696
11143
|
}
|
|
10697
11144
|
return {
|
|
10698
|
-
|
|
10699
|
-
|
|
11145
|
+
assistantWasLastSpeaker: latestPriorMessage?.role === "assistant",
|
|
11146
|
+
currentMessageHasDirectedFollowUpCue: hasDirectedFollowUpCue(input.text),
|
|
11147
|
+
currentMessageHasAttachments: Boolean(input.hasAttachments),
|
|
11148
|
+
currentMessageIsTerseClarification: isTerseClarification(input.text),
|
|
11149
|
+
humanMessagesSinceLastAssistant,
|
|
11150
|
+
latestPriorAssistantMessage: latestPriorAssistantMessage?.text || "[none]",
|
|
11151
|
+
latestPriorMessageRole: latestPriorMessage?.role || "[none]",
|
|
11152
|
+
recentMessages
|
|
10700
11153
|
};
|
|
10701
11154
|
}
|
|
11155
|
+
function buildRouterPrompt(rawText, signals) {
|
|
11156
|
+
const recentThread = signals.recentMessages.length > 0 ? signals.recentMessages.map(
|
|
11157
|
+
(message) => escapeXml(`[${message.role}] ${message.author}: ${message.text}`)
|
|
11158
|
+
).join("\n") : "[none]";
|
|
11159
|
+
return [
|
|
11160
|
+
`<latest-message>${escapeXml(rawText.trim() || "[attachment-only message]")}</latest-message>`,
|
|
11161
|
+
"<routing-signals>",
|
|
11162
|
+
`assistant_was_last_speaker=${signals.assistantWasLastSpeaker ? "true" : "false"}`,
|
|
11163
|
+
`human_messages_since_last_assistant=${signals.humanMessagesSinceLastAssistant ?? "none"}`,
|
|
11164
|
+
`latest_prior_message_role=${escapeXml(signals.latestPriorMessageRole)}`,
|
|
11165
|
+
`current_message_has_directed_follow_up_cue=${signals.currentMessageHasDirectedFollowUpCue ? "true" : "false"}`,
|
|
11166
|
+
`current_message_is_terse_clarification=${signals.currentMessageIsTerseClarification ? "true" : "false"}`,
|
|
11167
|
+
`current_message_has_attachments=${signals.currentMessageHasAttachments ? "true" : "false"}`,
|
|
11168
|
+
"</routing-signals>",
|
|
11169
|
+
`<latest-prior-assistant-message>${escapeXml(
|
|
11170
|
+
signals.latestPriorAssistantMessage
|
|
11171
|
+
)}</latest-prior-assistant-message>`,
|
|
11172
|
+
"<recent-thread>",
|
|
11173
|
+
recentThread,
|
|
11174
|
+
"</recent-thread>"
|
|
11175
|
+
].join("\n");
|
|
11176
|
+
}
|
|
11177
|
+
function getReplyConfidenceThreshold(signals) {
|
|
11178
|
+
let threshold = ROUTER_CONFIDENCE_THRESHOLD;
|
|
11179
|
+
if (signals.assistantWasLastSpeaker && signals.humanMessagesSinceLastAssistant === 0) {
|
|
11180
|
+
if (signals.currentMessageHasDirectedFollowUpCue || signals.currentMessageIsTerseClarification) {
|
|
11181
|
+
threshold = 0.65;
|
|
11182
|
+
} else {
|
|
11183
|
+
threshold = 0.9;
|
|
11184
|
+
}
|
|
11185
|
+
} else if (signals.humanMessagesSinceLastAssistant === 1) {
|
|
11186
|
+
threshold = signals.currentMessageHasDirectedFollowUpCue ? 0.8 : 0.9;
|
|
11187
|
+
} else if (signals.humanMessagesSinceLastAssistant === void 0) {
|
|
11188
|
+
threshold = 0.85;
|
|
11189
|
+
} else if (signals.humanMessagesSinceLastAssistant >= 2) {
|
|
11190
|
+
threshold = 0.9;
|
|
11191
|
+
}
|
|
11192
|
+
return Math.max(0.6, Math.min(0.9, threshold));
|
|
11193
|
+
}
|
|
10702
11194
|
function getSubscribedReplyPreflightDecision(args) {
|
|
10703
11195
|
const text = args.text.trim();
|
|
10704
11196
|
const rawText = args.rawText.trim();
|
|
@@ -10719,54 +11211,27 @@ function getSubscribedReplyPreflightDecision(args) {
|
|
|
10719
11211
|
reasonDetail: leadingOtherPartyAddress
|
|
10720
11212
|
};
|
|
10721
11213
|
}
|
|
10722
|
-
function buildRouterSystemPrompt(botUserName
|
|
10723
|
-
const { latestPriorMessageRole, latestPriorAssistantMessage } = getTranscriptMessageHints(conversationContext);
|
|
11214
|
+
function buildRouterSystemPrompt(botUserName) {
|
|
10724
11215
|
return [
|
|
10725
11216
|
"You are a message router for a Slack assistant named Junior in a subscribed Slack thread.",
|
|
10726
11217
|
"Decide whether Junior should reply to the latest message.",
|
|
10727
11218
|
"Subscribed threads are passive by default.",
|
|
10728
|
-
"
|
|
10729
|
-
"
|
|
10730
|
-
"",
|
|
10731
|
-
"
|
|
10732
|
-
"
|
|
10733
|
-
"",
|
|
10734
|
-
"
|
|
10735
|
-
"
|
|
10736
|
-
"
|
|
10737
|
-
"",
|
|
10738
|
-
"
|
|
10739
|
-
"- Questions between humans: 'Is that the right approach?', 'Can you check on this?', 'Did you deploy that?'",
|
|
10740
|
-
"- Acknowledgments: 'thanks', '+1', 'lgtm', 'ok cool', 'sounds good', 'nice'",
|
|
10741
|
-
"- Status updates: 'I just pushed a fix', 'Deploying now', 'Build is green'",
|
|
10742
|
-
"- General thread discussion: 'What about the billing issue?', 'I think we should revert'",
|
|
10743
|
-
"- Reactions to work: 'That looks wrong', 'Nice catch', 'Hmm interesting'",
|
|
10744
|
-
"",
|
|
10745
|
-
"Examples of messages Junior SHOULD reply to (should_reply=true):",
|
|
10746
|
-
"- Direct follow-ups to Junior's response: 'Can you explain that last point in more detail?'",
|
|
10747
|
-
"- Self-referential follow-ups after Junior just answered: 'What did you just say about the budget?', 'Can you explain your last response in more detail?'",
|
|
10748
|
-
"- Explicit requests for Junior's help: 'Junior, what's causing this error?'",
|
|
10749
|
-
"",
|
|
10750
|
-
"Treat a message as directed at Junior when it explicitly refers to Junior's immediately previous reply",
|
|
10751
|
-
"using language like 'you just said', 'your last response', 'your last answer', or similar self-reference.",
|
|
10752
|
-
"Do not confuse that with general topic continuation. A message like 'What about the billing worker timeline?'",
|
|
10753
|
-
"still should_reply=false unless it clearly asks Junior for help.",
|
|
10754
|
-
"",
|
|
10755
|
-
"When in doubt, should_reply=false. Most messages in a thread are human-to-human conversation.",
|
|
10756
|
-
"",
|
|
10757
|
-
"If the user is clearly telling Junior to stop watching, replying, or participating in the thread,",
|
|
10758
|
-
"set should_unsubscribe=true and should_reply=false.",
|
|
10759
|
-
"Use should_unsubscribe only for clear thread opt-out instructions, not for ordinary side conversation.",
|
|
10760
|
-
"If uncertain, set should_reply=false and use low confidence.",
|
|
11219
|
+
"Reply true only when the latest message is aimed at Junior.",
|
|
11220
|
+
"Use who currently has the conversation floor, not just topic overlap.",
|
|
11221
|
+
"If Junior was the last speaker, only a clear turn back to Junior should count as an implicit follow-up.",
|
|
11222
|
+
"Terse clarifications like 'which one?' or 'why?' right after Junior answers can be should_reply=true.",
|
|
11223
|
+
"Direct self-reference to Junior's prior answer like 'what did you just say?' or 'explain that more' can be should_reply=true.",
|
|
11224
|
+
"If one or more humans spoke after Junior, require a clear turn back to Junior. Shared domain vocabulary alone is not enough.",
|
|
11225
|
+
"Questions like 'what about auth?' or 'can you check on this?' are usually human-to-human unless the thread clearly turns back to Junior.",
|
|
11226
|
+
"A vague question like 'is that the right approach?' is still should_reply=false unless it clearly turns back to Junior.",
|
|
11227
|
+
"Acknowledgments, reactions, status chatter, and team coordination should be should_reply=false.",
|
|
11228
|
+
"If the latest message clearly tells Junior to stop watching, replying, or participating, set should_unsubscribe=true and should_reply=false.",
|
|
11229
|
+
"When uncertain, prefer should_reply=false with low confidence.",
|
|
10761
11230
|
"",
|
|
10762
11231
|
"Return JSON with should_reply, should_unsubscribe, confidence, and a short reason.",
|
|
10763
11232
|
"Do not return any extra keys.",
|
|
10764
11233
|
"",
|
|
10765
|
-
`<assistant-name>${escapeXml(botUserName)}</assistant-name
|
|
10766
|
-
`<explicit-mention>${isExplicitMention ? "true" : "false"}</explicit-mention>`,
|
|
10767
|
-
`<latest-prior-message-role>${escapeXml(latestPriorMessageRole)}</latest-prior-message-role>`,
|
|
10768
|
-
`<latest-prior-assistant-message>${escapeXml(latestPriorAssistantMessage)}</latest-prior-assistant-message>`,
|
|
10769
|
-
`<thread-context>${escapeXml(conversationContext?.trim() || "[none]")}</thread-context>`
|
|
11234
|
+
`<assistant-name>${escapeXml(botUserName)}</assistant-name>`
|
|
10770
11235
|
].join("\n");
|
|
10771
11236
|
}
|
|
10772
11237
|
async function decideSubscribedThreadReply(args) {
|
|
@@ -10781,11 +11246,16 @@ async function decideSubscribedThreadReply(args) {
|
|
|
10781
11246
|
if (preflightDecision) {
|
|
10782
11247
|
return preflightDecision;
|
|
10783
11248
|
}
|
|
11249
|
+
const signals = buildRouterSignals(args.input);
|
|
10784
11250
|
if (!text && !args.input.hasAttachments) {
|
|
10785
11251
|
return { shouldReply: false, reason: "empty_message" /* EmptyMessage */ };
|
|
10786
11252
|
}
|
|
10787
|
-
if (!
|
|
10788
|
-
return {
|
|
11253
|
+
if (!args.input.isExplicitMention && !args.input.hasAttachments && isAcknowledgmentOnly(text)) {
|
|
11254
|
+
return {
|
|
11255
|
+
shouldReply: false,
|
|
11256
|
+
reason: "side_conversation" /* SideConversation */,
|
|
11257
|
+
reasonDetail: "acknowledgment"
|
|
11258
|
+
};
|
|
10789
11259
|
}
|
|
10790
11260
|
if (args.input.isExplicitMention) {
|
|
10791
11261
|
if (isThreadOptOutInstruction(rawText, text)) {
|
|
@@ -10801,18 +11271,21 @@ async function decideSubscribedThreadReply(args) {
|
|
|
10801
11271
|
reason: "explicit_mention" /* ExplicitMention */
|
|
10802
11272
|
};
|
|
10803
11273
|
}
|
|
11274
|
+
if (signals.assistantWasLastSpeaker && signals.humanMessagesSinceLastAssistant === 0 && !signals.currentMessageHasAttachments && !signals.currentMessageHasDirectedFollowUpCue && !signals.currentMessageIsTerseClarification && isGenericImmediateSideConversation(text)) {
|
|
11275
|
+
return {
|
|
11276
|
+
shouldReply: false,
|
|
11277
|
+
reason: "side_conversation" /* SideConversation */,
|
|
11278
|
+
reasonDetail: "generic immediate side conversation"
|
|
11279
|
+
};
|
|
11280
|
+
}
|
|
10804
11281
|
try {
|
|
10805
11282
|
const result = await args.completeObject({
|
|
10806
11283
|
modelId: args.modelId,
|
|
10807
11284
|
schema: replyDecisionSchema,
|
|
10808
11285
|
maxTokens: 120,
|
|
10809
11286
|
temperature: 0,
|
|
10810
|
-
system: buildRouterSystemPrompt(
|
|
10811
|
-
|
|
10812
|
-
args.input.conversationContext,
|
|
10813
|
-
args.input.isExplicitMention
|
|
10814
|
-
),
|
|
10815
|
-
prompt: rawText,
|
|
11287
|
+
system: buildRouterSystemPrompt(args.botUserName),
|
|
11288
|
+
prompt: buildRouterPrompt(rawText, signals),
|
|
10816
11289
|
metadata: {
|
|
10817
11290
|
modelId: args.modelId,
|
|
10818
11291
|
threadId: args.input.context.threadId ?? "",
|
|
@@ -10823,6 +11296,7 @@ async function decideSubscribedThreadReply(args) {
|
|
|
10823
11296
|
});
|
|
10824
11297
|
const parsed = replyDecisionSchema.parse(result.object);
|
|
10825
11298
|
const reason = parsed.reason?.trim() || "classifier";
|
|
11299
|
+
const replyConfidenceThreshold = getReplyConfidenceThreshold(signals);
|
|
10826
11300
|
if (parsed.should_unsubscribe) {
|
|
10827
11301
|
if (parsed.confidence < ROUTER_CONFIDENCE_THRESHOLD) {
|
|
10828
11302
|
return {
|
|
@@ -10845,7 +11319,7 @@ async function decideSubscribedThreadReply(args) {
|
|
|
10845
11319
|
reasonDetail: reason
|
|
10846
11320
|
};
|
|
10847
11321
|
}
|
|
10848
|
-
if (parsed.confidence <
|
|
11322
|
+
if (parsed.confidence < replyConfidenceThreshold) {
|
|
10849
11323
|
return {
|
|
10850
11324
|
shouldReply: false,
|
|
10851
11325
|
reason: "low_confidence" /* LowConfidence */,
|
|
@@ -11440,16 +11914,166 @@ var MAX_USER_ATTACHMENTS = 3;
|
|
|
11440
11914
|
var MAX_USER_ATTACHMENT_BYTES = 5 * 1024 * 1024;
|
|
11441
11915
|
var MAX_MESSAGE_IMAGE_ATTACHMENTS = 3;
|
|
11442
11916
|
var MAX_VISION_SUMMARY_CHARS = 500;
|
|
11443
|
-
|
|
11917
|
+
function isVisionEnabled() {
|
|
11918
|
+
return Boolean(botConfig.visionModelId);
|
|
11919
|
+
}
|
|
11920
|
+
var ImageAttachmentProcessingError = class extends Error {
|
|
11921
|
+
constructor(message) {
|
|
11922
|
+
super(message);
|
|
11923
|
+
this.name = "ImageAttachmentProcessingError";
|
|
11924
|
+
}
|
|
11925
|
+
};
|
|
11926
|
+
function buildImageAttachmentPromptText(args) {
|
|
11927
|
+
return [
|
|
11928
|
+
"<image-attachment>",
|
|
11929
|
+
`filename: ${args.filename ?? "unnamed"}`,
|
|
11930
|
+
`media_type: ${args.mediaType}`,
|
|
11931
|
+
"<summary>",
|
|
11932
|
+
args.summary,
|
|
11933
|
+
"</summary>",
|
|
11934
|
+
"</image-attachment>"
|
|
11935
|
+
].join("\n");
|
|
11936
|
+
}
|
|
11937
|
+
async function summarizeImageWithVision(args) {
|
|
11938
|
+
const visionModelId = botConfig.visionModelId;
|
|
11939
|
+
if (!visionModelId) {
|
|
11940
|
+
return void 0;
|
|
11941
|
+
}
|
|
11942
|
+
const result = await args.completeText({
|
|
11943
|
+
modelId: visionModelId,
|
|
11944
|
+
temperature: 0,
|
|
11945
|
+
maxTokens: args.maxTokens,
|
|
11946
|
+
messages: [
|
|
11947
|
+
{
|
|
11948
|
+
role: "user",
|
|
11949
|
+
content: [
|
|
11950
|
+
{
|
|
11951
|
+
type: "text",
|
|
11952
|
+
text: args.prompt
|
|
11953
|
+
},
|
|
11954
|
+
{
|
|
11955
|
+
type: "image",
|
|
11956
|
+
data: args.imageData.toString("base64"),
|
|
11957
|
+
mimeType: args.mimeType
|
|
11958
|
+
}
|
|
11959
|
+
],
|
|
11960
|
+
timestamp: Date.now()
|
|
11961
|
+
}
|
|
11962
|
+
],
|
|
11963
|
+
metadata: {
|
|
11964
|
+
modelId: visionModelId,
|
|
11965
|
+
...args.metadata
|
|
11966
|
+
}
|
|
11967
|
+
});
|
|
11968
|
+
const summary = result.text.trim().replace(/\s+/g, " ");
|
|
11969
|
+
return summary || void 0;
|
|
11970
|
+
}
|
|
11971
|
+
function truncateVisionSummary(summary) {
|
|
11972
|
+
return summary.slice(0, MAX_VISION_SUMMARY_CHARS);
|
|
11973
|
+
}
|
|
11974
|
+
function getCachedImageSummaries(args) {
|
|
11975
|
+
if (!args.conversation || !args.messageTs) {
|
|
11976
|
+
return [];
|
|
11977
|
+
}
|
|
11978
|
+
const conversationMessage = args.conversation.messages.find(
|
|
11979
|
+
(message) => getConversationMessageSlackTs(message) === args.messageTs
|
|
11980
|
+
);
|
|
11981
|
+
if (!conversationMessage) {
|
|
11982
|
+
return [];
|
|
11983
|
+
}
|
|
11984
|
+
return (conversationMessage.meta?.imageFileIds ?? []).map(
|
|
11985
|
+
(fileId) => args.conversation?.vision.byFileId[fileId]?.summary?.trim()
|
|
11986
|
+
);
|
|
11987
|
+
}
|
|
11988
|
+
function createImageAttachmentProcessingError(attachment) {
|
|
11989
|
+
const label = attachment.filename ? `"${attachment.filename}"` : "this image";
|
|
11990
|
+
return new ImageAttachmentProcessingError(
|
|
11991
|
+
`Image attachment ${label} could not be analyzed`
|
|
11992
|
+
);
|
|
11993
|
+
}
|
|
11994
|
+
async function resolveUserAttachmentsWithDeps(attachments, context, deps) {
|
|
11444
11995
|
if (!attachments || attachments.length === 0) {
|
|
11445
11996
|
return [];
|
|
11446
11997
|
}
|
|
11447
11998
|
const results = [];
|
|
11999
|
+
const cachedImageSummaries = getCachedImageSummaries({
|
|
12000
|
+
conversation: context.conversation,
|
|
12001
|
+
messageTs: context.messageTs
|
|
12002
|
+
});
|
|
12003
|
+
let nextCachedImageSummaryIndex = 0;
|
|
11448
12004
|
for (const attachment of attachments) {
|
|
11449
12005
|
if (results.length >= MAX_USER_ATTACHMENTS) break;
|
|
11450
12006
|
if (attachment.type !== "image" && attachment.type !== "file") continue;
|
|
11451
12007
|
const mediaType = attachment.mimeType ?? "application/octet-stream";
|
|
12008
|
+
const isImageAttachment = attachment.type === "image" || mediaType.startsWith("image/");
|
|
12009
|
+
if (isImageAttachment && !isVisionEnabled()) {
|
|
12010
|
+
continue;
|
|
12011
|
+
}
|
|
11452
12012
|
try {
|
|
12013
|
+
const resolvedAttachment = {
|
|
12014
|
+
mediaType,
|
|
12015
|
+
filename: attachment.name
|
|
12016
|
+
};
|
|
12017
|
+
if (isImageAttachment) {
|
|
12018
|
+
const cachedSummary = cachedImageSummaries[nextCachedImageSummaryIndex];
|
|
12019
|
+
nextCachedImageSummaryIndex += 1;
|
|
12020
|
+
if (cachedSummary) {
|
|
12021
|
+
resolvedAttachment.promptText = buildImageAttachmentPromptText({
|
|
12022
|
+
filename: attachment.name,
|
|
12023
|
+
mediaType,
|
|
12024
|
+
summary: cachedSummary
|
|
12025
|
+
});
|
|
12026
|
+
results.push(resolvedAttachment);
|
|
12027
|
+
continue;
|
|
12028
|
+
}
|
|
12029
|
+
let imageData = null;
|
|
12030
|
+
if (attachment.fetchData) {
|
|
12031
|
+
imageData = await attachment.fetchData();
|
|
12032
|
+
} else if (attachment.data instanceof Buffer) {
|
|
12033
|
+
imageData = attachment.data;
|
|
12034
|
+
}
|
|
12035
|
+
if (!imageData) {
|
|
12036
|
+
throw createImageAttachmentProcessingError({
|
|
12037
|
+
filename: attachment.name
|
|
12038
|
+
});
|
|
12039
|
+
}
|
|
12040
|
+
if (imageData.byteLength > MAX_USER_ATTACHMENT_BYTES) {
|
|
12041
|
+
throw createImageAttachmentProcessingError({
|
|
12042
|
+
filename: attachment.name
|
|
12043
|
+
});
|
|
12044
|
+
}
|
|
12045
|
+
const summary = await summarizeImageWithVision({
|
|
12046
|
+
completeText: deps.completeText,
|
|
12047
|
+
imageData,
|
|
12048
|
+
mimeType: mediaType,
|
|
12049
|
+
maxTokens: 220,
|
|
12050
|
+
prompt: [
|
|
12051
|
+
"Extract concise, factual context from this user-provided image.",
|
|
12052
|
+
"Focus on visible text, UI state, charts, diagrams, errors, names, and other concrete details useful for answering the user's current request.",
|
|
12053
|
+
"Do not speculate.",
|
|
12054
|
+
"Return plain text only."
|
|
12055
|
+
].join(" "),
|
|
12056
|
+
metadata: {
|
|
12057
|
+
threadId: context.threadId ?? "",
|
|
12058
|
+
channelId: context.channelId ?? "",
|
|
12059
|
+
requesterId: context.requesterId ?? "",
|
|
12060
|
+
runId: context.runId ?? "",
|
|
12061
|
+
filename: attachment.name ?? ""
|
|
12062
|
+
}
|
|
12063
|
+
});
|
|
12064
|
+
if (!summary) {
|
|
12065
|
+
throw createImageAttachmentProcessingError({
|
|
12066
|
+
filename: attachment.name
|
|
12067
|
+
});
|
|
12068
|
+
}
|
|
12069
|
+
resolvedAttachment.promptText = buildImageAttachmentPromptText({
|
|
12070
|
+
filename: attachment.name,
|
|
12071
|
+
mediaType,
|
|
12072
|
+
summary: truncateVisionSummary(summary)
|
|
12073
|
+
});
|
|
12074
|
+
results.push(resolvedAttachment);
|
|
12075
|
+
continue;
|
|
12076
|
+
}
|
|
11453
12077
|
let data = null;
|
|
11454
12078
|
if (attachment.fetchData) {
|
|
11455
12079
|
data = await attachment.fetchData();
|
|
@@ -11476,12 +12100,32 @@ async function resolveUserAttachments(attachments, context) {
|
|
|
11476
12100
|
);
|
|
11477
12101
|
continue;
|
|
11478
12102
|
}
|
|
11479
|
-
|
|
11480
|
-
|
|
11481
|
-
mediaType,
|
|
11482
|
-
filename: attachment.name
|
|
11483
|
-
});
|
|
12103
|
+
resolvedAttachment.data = data;
|
|
12104
|
+
results.push(resolvedAttachment);
|
|
11484
12105
|
} catch (error) {
|
|
12106
|
+
if (isImageAttachment) {
|
|
12107
|
+
const attachmentError = error instanceof ImageAttachmentProcessingError ? error : createImageAttachmentProcessingError({
|
|
12108
|
+
filename: attachment.name
|
|
12109
|
+
});
|
|
12110
|
+
logWarn(
|
|
12111
|
+
"image_attachment_processing_failed",
|
|
12112
|
+
{
|
|
12113
|
+
slackThreadId: context.threadId,
|
|
12114
|
+
slackUserId: context.requesterId,
|
|
12115
|
+
slackChannelId: context.channelId,
|
|
12116
|
+
runId: context.runId,
|
|
12117
|
+
assistantUserName: botConfig.userName,
|
|
12118
|
+
modelId: botConfig.visionModelId ?? botConfig.modelId
|
|
12119
|
+
},
|
|
12120
|
+
{
|
|
12121
|
+
"error.message": error instanceof Error ? error.message : String(error),
|
|
12122
|
+
"file.mime_type": mediaType,
|
|
12123
|
+
...attachment.name ? { "file.name": attachment.name } : {}
|
|
12124
|
+
},
|
|
12125
|
+
"Image attachment processing failed"
|
|
12126
|
+
);
|
|
12127
|
+
throw attachmentError;
|
|
12128
|
+
}
|
|
11485
12129
|
logWarn(
|
|
11486
12130
|
"attachment_resolution_failed",
|
|
11487
12131
|
{
|
|
@@ -11503,35 +12147,23 @@ async function resolveUserAttachments(attachments, context) {
|
|
|
11503
12147
|
return results;
|
|
11504
12148
|
}
|
|
11505
12149
|
async function summarizeConversationImage(args, deps) {
|
|
12150
|
+
const visionModelId = botConfig.visionModelId;
|
|
12151
|
+
if (!visionModelId) {
|
|
12152
|
+
return void 0;
|
|
12153
|
+
}
|
|
11506
12154
|
try {
|
|
11507
|
-
const
|
|
11508
|
-
|
|
11509
|
-
|
|
12155
|
+
const summary = await summarizeImageWithVision({
|
|
12156
|
+
completeText: deps.completeText,
|
|
12157
|
+
imageData: args.imageData,
|
|
12158
|
+
mimeType: args.mimeType,
|
|
11510
12159
|
maxTokens: 220,
|
|
11511
|
-
|
|
11512
|
-
|
|
11513
|
-
|
|
11514
|
-
|
|
11515
|
-
|
|
11516
|
-
|
|
11517
|
-
text: [
|
|
11518
|
-
"Extract concise, factual context from this image for future thread turns.",
|
|
11519
|
-
"Focus on visible text, names, titles, companies, and candidate-identifying details.",
|
|
11520
|
-
"Do not speculate.",
|
|
11521
|
-
"Return plain text only."
|
|
11522
|
-
].join(" ")
|
|
11523
|
-
},
|
|
11524
|
-
{
|
|
11525
|
-
type: "image",
|
|
11526
|
-
data: args.imageData.toString("base64"),
|
|
11527
|
-
mimeType: args.mimeType
|
|
11528
|
-
}
|
|
11529
|
-
],
|
|
11530
|
-
timestamp: Date.now()
|
|
11531
|
-
}
|
|
11532
|
-
],
|
|
12160
|
+
prompt: [
|
|
12161
|
+
"Extract concise, factual context from this image for future thread turns.",
|
|
12162
|
+
"Focus on visible text, names, titles, companies, and candidate-identifying details.",
|
|
12163
|
+
"Do not speculate.",
|
|
12164
|
+
"Return plain text only."
|
|
12165
|
+
].join(" "),
|
|
11533
12166
|
metadata: {
|
|
11534
|
-
modelId: botConfig.modelId,
|
|
11535
12167
|
threadId: args.context.threadId ?? "",
|
|
11536
12168
|
channelId: args.context.channelId ?? "",
|
|
11537
12169
|
requesterId: args.context.requesterId ?? "",
|
|
@@ -11539,11 +12171,10 @@ async function summarizeConversationImage(args, deps) {
|
|
|
11539
12171
|
fileId: args.fileId
|
|
11540
12172
|
}
|
|
11541
12173
|
});
|
|
11542
|
-
const summary = result.text.trim().replace(/\s+/g, " ");
|
|
11543
12174
|
if (!summary) {
|
|
11544
12175
|
return void 0;
|
|
11545
12176
|
}
|
|
11546
|
-
return summary
|
|
12177
|
+
return truncateVisionSummary(summary);
|
|
11547
12178
|
} catch (error) {
|
|
11548
12179
|
logWarn(
|
|
11549
12180
|
"conversation_image_vision_failed",
|
|
@@ -11553,7 +12184,7 @@ async function summarizeConversationImage(args, deps) {
|
|
|
11553
12184
|
slackChannelId: args.context.channelId,
|
|
11554
12185
|
runId: args.context.runId,
|
|
11555
12186
|
assistantUserName: botConfig.userName,
|
|
11556
|
-
modelId:
|
|
12187
|
+
modelId: visionModelId
|
|
11557
12188
|
},
|
|
11558
12189
|
{
|
|
11559
12190
|
"error.message": error instanceof Error ? error.message : String(error),
|
|
@@ -11566,6 +12197,9 @@ async function summarizeConversationImage(args, deps) {
|
|
|
11566
12197
|
}
|
|
11567
12198
|
}
|
|
11568
12199
|
async function hydrateConversationVisionContextWithDeps(conversation, context, deps) {
|
|
12200
|
+
if (!isVisionEnabled()) {
|
|
12201
|
+
return;
|
|
12202
|
+
}
|
|
11569
12203
|
if (!context.channelId || !context.threadTs) {
|
|
11570
12204
|
return;
|
|
11571
12205
|
}
|
|
@@ -11766,6 +12400,7 @@ async function hydrateConversationVisionContextWithDeps(conversation, context, d
|
|
|
11766
12400
|
}
|
|
11767
12401
|
function createVisionContextService(deps) {
|
|
11768
12402
|
return {
|
|
12403
|
+
resolveUserAttachments: async (attachments, context) => await resolveUserAttachmentsWithDeps(attachments, context, deps),
|
|
11769
12404
|
hydrateConversationVisionContext: async (conversation, context) => await hydrateConversationVisionContextWithDeps(
|
|
11770
12405
|
conversation,
|
|
11771
12406
|
context,
|
|
@@ -11859,6 +12494,19 @@ function getExecutionFailureReason(reply) {
|
|
|
11859
12494
|
}
|
|
11860
12495
|
return "empty assistant turn";
|
|
11861
12496
|
}
|
|
12497
|
+
function buildParticipants(messages) {
|
|
12498
|
+
const seen = /* @__PURE__ */ new Set();
|
|
12499
|
+
const participants = [];
|
|
12500
|
+
for (const message of messages) {
|
|
12501
|
+
const { userId, userName, fullName } = message.author ?? {};
|
|
12502
|
+
if (!userId || message.author?.isBot) continue;
|
|
12503
|
+
if (!seen.has(userId)) {
|
|
12504
|
+
seen.add(userId);
|
|
12505
|
+
participants.push({ userId, userName, fullName });
|
|
12506
|
+
}
|
|
12507
|
+
}
|
|
12508
|
+
return participants;
|
|
12509
|
+
}
|
|
11862
12510
|
function createReplyToThread(deps) {
|
|
11863
12511
|
return async function replyToThread(thread, message, options = {}) {
|
|
11864
12512
|
if (message.author.isMe) {
|
|
@@ -11901,6 +12549,7 @@ function createReplyToThread(deps) {
|
|
|
11901
12549
|
runId
|
|
11902
12550
|
}
|
|
11903
12551
|
});
|
|
12552
|
+
const slackMessageTs = getSlackMessageTs(message);
|
|
11904
12553
|
const turnId = buildDeterministicTurnId(message.id);
|
|
11905
12554
|
startActiveTurn({
|
|
11906
12555
|
conversation: preparedState.conversation,
|
|
@@ -11945,13 +12594,15 @@ function createReplyToThread(deps) {
|
|
|
11945
12594
|
if (resolvedUserName) {
|
|
11946
12595
|
setTags({ slackUserName: resolvedUserName });
|
|
11947
12596
|
}
|
|
11948
|
-
const userAttachments = await resolveUserAttachments(
|
|
12597
|
+
const userAttachments = await deps.resolveUserAttachments(
|
|
11949
12598
|
message.attachments,
|
|
11950
12599
|
{
|
|
11951
12600
|
threadId,
|
|
11952
12601
|
requesterId: message.author.userId,
|
|
11953
12602
|
channelId,
|
|
11954
|
-
runId
|
|
12603
|
+
runId,
|
|
12604
|
+
conversation: preparedState.conversation,
|
|
12605
|
+
messageTs: slackMessageTs
|
|
11955
12606
|
}
|
|
11956
12607
|
);
|
|
11957
12608
|
const progress = createProgressReporter({
|
|
@@ -12015,6 +12666,9 @@ function createReplyToThread(deps) {
|
|
|
12015
12666
|
let shouldPersistFailureState = true;
|
|
12016
12667
|
try {
|
|
12017
12668
|
const toolChannelId = preparedState.artifacts.assistantContextChannelId ?? channelId;
|
|
12669
|
+
const threadParticipants = buildParticipants(
|
|
12670
|
+
preparedState.conversation.messages
|
|
12671
|
+
);
|
|
12018
12672
|
const reply = await deps.services.generateAssistantReply(userText, {
|
|
12019
12673
|
assistant: {
|
|
12020
12674
|
userName: botConfig.userName
|
|
@@ -12044,6 +12698,7 @@ function createReplyToThread(deps) {
|
|
|
12044
12698
|
sandboxId: preparedState.sandboxId,
|
|
12045
12699
|
sandboxDependencyProfileHash: preparedState.sandboxDependencyProfileHash
|
|
12046
12700
|
},
|
|
12701
|
+
threadParticipants,
|
|
12047
12702
|
onStatus: (status) => progress.setStatus(status),
|
|
12048
12703
|
onTextDelta: (deltaText) => {
|
|
12049
12704
|
if (explicitChannelPostIntent) {
|
|
@@ -12340,6 +12995,7 @@ function createPrepareTurnState(deps) {
|
|
|
12340
12995
|
}
|
|
12341
12996
|
);
|
|
12342
12997
|
const normalizedUserText = normalizeConversationText(args.userText) || "[non-text message]";
|
|
12998
|
+
const slackTs = getSlackMessageTs(args.message);
|
|
12343
12999
|
const incomingUserMessage = {
|
|
12344
13000
|
id: args.message.id,
|
|
12345
13001
|
role: "user",
|
|
@@ -12353,7 +13009,7 @@ function createPrepareTurnState(deps) {
|
|
|
12353
13009
|
},
|
|
12354
13010
|
meta: {
|
|
12355
13011
|
explicitMention: args.explicitMention,
|
|
12356
|
-
slackTs
|
|
13012
|
+
slackTs,
|
|
12357
13013
|
imagesHydrated: !messageHasPotentialImageAttachment
|
|
12358
13014
|
}
|
|
12359
13015
|
};
|
|
@@ -12361,7 +13017,7 @@ function createPrepareTurnState(deps) {
|
|
|
12361
13017
|
conversation,
|
|
12362
13018
|
incomingUserMessage
|
|
12363
13019
|
);
|
|
12364
|
-
if (
|
|
13020
|
+
if (isVisionEnabled() && (!conversation.vision.backfillCompletedAtMs || messageHasPotentialImageAttachment)) {
|
|
12365
13021
|
await deps.hydrateConversationVisionContext(conversation, {
|
|
12366
13022
|
threadId: args.context.threadId,
|
|
12367
13023
|
channelId: args.context.channelId,
|
|
@@ -12408,6 +13064,7 @@ function createSlackRuntime(options) {
|
|
|
12408
13064
|
const replyToThread = createReplyToThread({
|
|
12409
13065
|
getSlackAdapter: options.getSlackAdapter,
|
|
12410
13066
|
prepareTurnState,
|
|
13067
|
+
resolveUserAttachments: services.visionContext.resolveUserAttachments,
|
|
12411
13068
|
services: services.replyExecutor
|
|
12412
13069
|
});
|
|
12413
13070
|
return createSlackTurnRuntime({
|
|
@@ -12439,6 +13096,7 @@ function createSlackRuntime(options) {
|
|
|
12439
13096
|
}) => {
|
|
12440
13097
|
const conversation = coerceThreadConversationState(await thread.state);
|
|
12441
13098
|
const normalizedUserText = normalizeConversationText(userText) || "[non-text message]";
|
|
13099
|
+
const slackTs = getSlackMessageTs(message);
|
|
12442
13100
|
upsertConversationMessage(conversation, {
|
|
12443
13101
|
id: message.id,
|
|
12444
13102
|
role: "user",
|
|
@@ -12452,7 +13110,7 @@ function createSlackRuntime(options) {
|
|
|
12452
13110
|
},
|
|
12453
13111
|
meta: {
|
|
12454
13112
|
explicitMention: Boolean(message.isMention),
|
|
12455
|
-
slackTs
|
|
13113
|
+
slackTs,
|
|
12456
13114
|
replied: false,
|
|
12457
13115
|
skippedReason: decision.reason,
|
|
12458
13116
|
imagesHydrated: true
|
|
@@ -12974,9 +13632,121 @@ function getProductionBot() {
|
|
|
12974
13632
|
return productionBot;
|
|
12975
13633
|
}
|
|
12976
13634
|
|
|
13635
|
+
// src/chat/ingress/message-changed.ts
|
|
13636
|
+
import { Message } from "chat";
|
|
13637
|
+
function getEditedMentionMessageId(messageTs) {
|
|
13638
|
+
return `${messageTs}:message_changed_mention`;
|
|
13639
|
+
}
|
|
13640
|
+
function isMessageChangedEnvelope(value) {
|
|
13641
|
+
if (!value || typeof value !== "object") return false;
|
|
13642
|
+
const v = value;
|
|
13643
|
+
if (v.type !== "event_callback") return false;
|
|
13644
|
+
const event = v.event;
|
|
13645
|
+
if (!event || typeof event !== "object") return false;
|
|
13646
|
+
return event.type === "message" && event.subtype === "message_changed" && typeof event.channel === "string" && typeof event.message === "object" && event.message !== null && typeof event.previous_message === "object" && event.previous_message !== null;
|
|
13647
|
+
}
|
|
13648
|
+
function textMentionsBot(text, botUserId) {
|
|
13649
|
+
return text.includes(`<@${botUserId}>`);
|
|
13650
|
+
}
|
|
13651
|
+
function extractMessageChangedMention(body, botUserId, adapter) {
|
|
13652
|
+
if (!isMessageChangedEnvelope(body)) return null;
|
|
13653
|
+
const { event } = body;
|
|
13654
|
+
const newText = event.message.text ?? "";
|
|
13655
|
+
const prevText = event.previous_message.text ?? "";
|
|
13656
|
+
if (!textMentionsBot(newText, botUserId)) return null;
|
|
13657
|
+
if (textMentionsBot(prevText, botUserId)) return null;
|
|
13658
|
+
const channelId = event.channel;
|
|
13659
|
+
const messageTs = event.message.ts;
|
|
13660
|
+
const threadTs = event.message.thread_ts ?? messageTs;
|
|
13661
|
+
const userId = event.message.user ?? "unknown";
|
|
13662
|
+
const threadId = `slack:${channelId}:${threadTs}`;
|
|
13663
|
+
const raw = {
|
|
13664
|
+
channel: channelId,
|
|
13665
|
+
ts: messageTs,
|
|
13666
|
+
thread_ts: threadTs,
|
|
13667
|
+
user: userId
|
|
13668
|
+
};
|
|
13669
|
+
const message = new Message({
|
|
13670
|
+
id: getEditedMentionMessageId(messageTs),
|
|
13671
|
+
threadId,
|
|
13672
|
+
text: newText,
|
|
13673
|
+
isMention: true,
|
|
13674
|
+
attachments: [],
|
|
13675
|
+
metadata: { dateSent: new Date(Number(messageTs) * 1e3), edited: true },
|
|
13676
|
+
formatted: { type: "root", children: [] },
|
|
13677
|
+
raw,
|
|
13678
|
+
author: {
|
|
13679
|
+
userId,
|
|
13680
|
+
userName: userId,
|
|
13681
|
+
fullName: userId,
|
|
13682
|
+
isBot: false,
|
|
13683
|
+
isMe: false
|
|
13684
|
+
}
|
|
13685
|
+
});
|
|
13686
|
+
Object.defineProperty(message, "adapter", {
|
|
13687
|
+
configurable: true,
|
|
13688
|
+
enumerable: false,
|
|
13689
|
+
value: adapter,
|
|
13690
|
+
writable: true
|
|
13691
|
+
});
|
|
13692
|
+
return { threadId, message };
|
|
13693
|
+
}
|
|
13694
|
+
|
|
12977
13695
|
// src/handlers/webhooks.ts
|
|
12978
|
-
|
|
12979
|
-
|
|
13696
|
+
function getSlackPayloadTeamId(body) {
|
|
13697
|
+
if (!body || typeof body !== "object") {
|
|
13698
|
+
return void 0;
|
|
13699
|
+
}
|
|
13700
|
+
const teamId = body.team_id;
|
|
13701
|
+
return typeof teamId === "string" && teamId.length > 0 ? teamId : void 0;
|
|
13702
|
+
}
|
|
13703
|
+
async function handleAuthenticatedSlackMessageChangedMention(args) {
|
|
13704
|
+
const slackAdapter = args.bot.getAdapter("slack");
|
|
13705
|
+
const authAdapter = slackAdapter;
|
|
13706
|
+
const timestamp = args.request.headers.get("x-slack-request-timestamp");
|
|
13707
|
+
const signature = args.request.headers.get("x-slack-signature");
|
|
13708
|
+
if (!authAdapter.verifySignature(args.rawBody, timestamp, signature)) {
|
|
13709
|
+
return;
|
|
13710
|
+
}
|
|
13711
|
+
const webhookOptions = {
|
|
13712
|
+
waitUntil: (task) => args.waitUntil(task)
|
|
13713
|
+
};
|
|
13714
|
+
const dispatch = () => {
|
|
13715
|
+
const botUserId = authAdapter.botUserId;
|
|
13716
|
+
if (!botUserId) {
|
|
13717
|
+
return false;
|
|
13718
|
+
}
|
|
13719
|
+
const result = extractMessageChangedMention(
|
|
13720
|
+
args.body,
|
|
13721
|
+
botUserId,
|
|
13722
|
+
slackAdapter
|
|
13723
|
+
);
|
|
13724
|
+
if (!result) {
|
|
13725
|
+
return false;
|
|
13726
|
+
}
|
|
13727
|
+
args.bot.processMessage(
|
|
13728
|
+
slackAdapter,
|
|
13729
|
+
result.threadId,
|
|
13730
|
+
result.message,
|
|
13731
|
+
webhookOptions
|
|
13732
|
+
);
|
|
13733
|
+
return true;
|
|
13734
|
+
};
|
|
13735
|
+
if (authAdapter.defaultBotToken) {
|
|
13736
|
+
dispatch();
|
|
13737
|
+
return;
|
|
13738
|
+
}
|
|
13739
|
+
const teamId = getSlackPayloadTeamId(args.body);
|
|
13740
|
+
if (!teamId || !authAdapter.resolveTokenForTeam || !authAdapter.requestContext) {
|
|
13741
|
+
return;
|
|
13742
|
+
}
|
|
13743
|
+
const context = await authAdapter.resolveTokenForTeam(teamId);
|
|
13744
|
+
if (!context) {
|
|
13745
|
+
return;
|
|
13746
|
+
}
|
|
13747
|
+
authAdapter.requestContext.run(context, dispatch);
|
|
13748
|
+
}
|
|
13749
|
+
async function handlePlatformWebhook(request, platform, waitUntil, bot = getProductionBot()) {
|
|
12980
13750
|
const handler = bot.webhooks[platform];
|
|
12981
13751
|
const requestContext = createRequestContext(request, { platform });
|
|
12982
13752
|
const requestUrl = new URL(request.url);
|
|
@@ -12994,6 +13764,34 @@ async function POST(request, platform, waitUntil) {
|
|
|
12994
13764
|
);
|
|
12995
13765
|
return new Response(`Unknown platform: ${platform}`, { status: 404 });
|
|
12996
13766
|
}
|
|
13767
|
+
let rebuiltRequest = request;
|
|
13768
|
+
if (platform === "slack") {
|
|
13769
|
+
const rawBody = await request.text();
|
|
13770
|
+
let parsedBody;
|
|
13771
|
+
try {
|
|
13772
|
+
parsedBody = JSON.parse(rawBody);
|
|
13773
|
+
} catch {
|
|
13774
|
+
parsedBody = void 0;
|
|
13775
|
+
}
|
|
13776
|
+
if (parsedBody && isMessageChangedEnvelope(parsedBody)) {
|
|
13777
|
+
try {
|
|
13778
|
+
await handleAuthenticatedSlackMessageChangedMention({
|
|
13779
|
+
body: parsedBody,
|
|
13780
|
+
bot,
|
|
13781
|
+
rawBody,
|
|
13782
|
+
request,
|
|
13783
|
+
waitUntil
|
|
13784
|
+
});
|
|
13785
|
+
} catch (error) {
|
|
13786
|
+
logException(error, "slack_message_changed_side_channel_failed");
|
|
13787
|
+
}
|
|
13788
|
+
}
|
|
13789
|
+
rebuiltRequest = new Request(request.url, {
|
|
13790
|
+
method: request.method,
|
|
13791
|
+
headers: request.headers,
|
|
13792
|
+
body: rawBody
|
|
13793
|
+
});
|
|
13794
|
+
}
|
|
12997
13795
|
try {
|
|
12998
13796
|
return await withSpan(
|
|
12999
13797
|
"http.server.request",
|
|
@@ -13001,7 +13799,7 @@ async function POST(request, platform, waitUntil) {
|
|
|
13001
13799
|
requestContext,
|
|
13002
13800
|
async () => {
|
|
13003
13801
|
try {
|
|
13004
|
-
const response = await handler(
|
|
13802
|
+
const response = await handler(rebuiltRequest, {
|
|
13005
13803
|
waitUntil: (task) => waitUntil(task)
|
|
13006
13804
|
});
|
|
13007
13805
|
if (response.status >= 400) {
|
|
@@ -13047,6 +13845,9 @@ async function POST(request, platform, waitUntil) {
|
|
|
13047
13845
|
}
|
|
13048
13846
|
});
|
|
13049
13847
|
}
|
|
13848
|
+
async function POST(request, platform, waitUntil) {
|
|
13849
|
+
return handlePlatformWebhook(request, platform, waitUntil);
|
|
13850
|
+
}
|
|
13050
13851
|
|
|
13051
13852
|
// src/app.ts
|
|
13052
13853
|
async function defaultWaitUntil() {
|