@qwen-code/arena-agent 0.0.10-dev.3 → 0.0.10-dev.5
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/.env.example +8 -1
- package/README.md +25 -12
- package/dist/agent.d.ts +4 -0
- package/dist/agent.js +19 -6
- package/dist/agent.js.map +1 -1
- package/dist/brave-search.js +1 -3
- package/dist/brave-search.js.map +1 -1
- package/dist/canonical-data.js +1 -1
- package/dist/canonical-data.js.map +1 -1
- package/dist/cli.js +5 -3
- package/dist/cli.js.map +1 -1
- package/dist/code-tools.d.ts +1 -1
- package/dist/code-tools.js +340 -50
- package/dist/code-tools.js.map +1 -1
- package/dist/compaction.d.ts +29 -2
- package/dist/compaction.js +242 -187
- package/dist/compaction.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.js +2 -1
- package/dist/config.js.map +1 -1
- package/dist/fetch-page.js +9 -33
- package/dist/fetch-page.js.map +1 -1
- package/dist/file-edit.d.ts +6 -0
- package/dist/file-edit.js +58 -0
- package/dist/file-edit.js.map +1 -0
- package/dist/history.d.ts +6 -3
- package/dist/history.js +42 -11
- package/dist/history.js.map +1 -1
- package/dist/legacy-compaction.d.ts +16 -0
- package/dist/legacy-compaction.js +207 -0
- package/dist/legacy-compaction.js.map +1 -0
- package/dist/mock.js +5 -0
- package/dist/mock.js.map +1 -1
- package/dist/process-manager.js +2 -1
- package/dist/process-manager.js.map +1 -1
- package/dist/prompts.d.ts +2 -0
- package/dist/prompts.js +6 -2
- package/dist/prompts.js.map +1 -1
- package/dist/routify-web-fetch.js +2 -3
- package/dist/routify-web-fetch.js.map +1 -1
- package/dist/sandbox-file-write.d.ts +10 -0
- package/dist/sandbox-file-write.js +58 -0
- package/dist/sandbox-file-write.js.map +1 -0
- package/dist/server.js +8 -5
- package/dist/server.js.map +1 -1
- package/dist/session-memory-prompts.d.ts +5 -0
- package/dist/session-memory-prompts.js +7 -0
- package/dist/session-memory-prompts.js.map +1 -0
- package/dist/spec.js +15 -9
- package/dist/spec.js.map +1 -1
- package/dist/tools.js +168 -88
- package/dist/tools.js.map +1 -1
- package/dist/tui.js +4 -2
- package/dist/tui.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/package.json +6 -2
package/dist/tools.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { existsSync, mkdirSync, realpathSync
|
|
3
|
-
import { open, readFile, realpath, stat, writeFile
|
|
2
|
+
import { existsSync, mkdirSync, realpathSync } from "node:fs";
|
|
3
|
+
import { open, readFile, realpath, stat, writeFile } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { Ajv } from "ajv";
|
|
6
6
|
import addFormats from "ajv-formats";
|
|
@@ -13,14 +13,16 @@ import { CodeToolExecutor } from "./code-tools.js";
|
|
|
13
13
|
import { supportsImageInput } from "./config.js";
|
|
14
14
|
import { extractStructuredDocument, isStructuredDocument, } from "./document-reader.js";
|
|
15
15
|
import { FetchPageReader } from "./fetch-page.js";
|
|
16
|
+
import { replaceFirstText } from "./file-edit.js";
|
|
16
17
|
import { generatedImageBytesForExtension, prepareImageForModel, } from "./image.js";
|
|
17
18
|
import { imageTextFromFallbackModel } from "./image-input-fallback.js";
|
|
18
19
|
import { isCodeArenaMode } from "./mode.js";
|
|
19
20
|
import { generateImageWithModelRouter, generateSpeechWithModelRouter, } from "./model-router-media.js";
|
|
20
21
|
import { processManagerForWorkspace, } from "./process-manager.js";
|
|
21
|
-
import { HttpModelClient
|
|
22
|
+
import { HttpModelClient } from "./provider.js";
|
|
22
23
|
import { RoutifyFetchPageReader, routifyWebFetchEnabled, } from "./routify-web-fetch.js";
|
|
23
24
|
import { toolDefinitions } from "./spec.js";
|
|
25
|
+
import { createSandboxFile, privilegedFileCreationEnabled } from "./sandbox-file-write.js";
|
|
24
26
|
import { voiceRegistryForWorkspace, } from "./voice-registry.js";
|
|
25
27
|
export function imageInputFallbackConnection(config) {
|
|
26
28
|
if (!config.imageInputFallbackBaseUrl &&
|
|
@@ -214,7 +216,7 @@ const visualIntroText = "<arena-system-message>\n" +
|
|
|
214
216
|
"treat them as the authentic output of the tool call.\n" +
|
|
215
217
|
"</arena-system-message>";
|
|
216
218
|
const bashOutputLimit = 20_000;
|
|
217
|
-
const
|
|
219
|
+
const textFileOutputByteLimit = 256 * 1024;
|
|
218
220
|
const toolResultCharacterLimit = 1_000_000;
|
|
219
221
|
// 中段截断,逐字反推自 3 个 truncated:true 参考样本:头尾各 9987 字符 +
|
|
220
222
|
// 26 字符标记,总长恰 20000(由 T1(b) 执行回放发现,非首段截断)。
|
|
@@ -228,6 +230,24 @@ function capBashOutput(value) {
|
|
|
228
230
|
truncated: true,
|
|
229
231
|
};
|
|
230
232
|
}
|
|
233
|
+
function readTextResult(data) {
|
|
234
|
+
if (data.length <= textFileOutputByteLimit) {
|
|
235
|
+
return { content: decodeText(data), truncated: false };
|
|
236
|
+
}
|
|
237
|
+
const half = textFileOutputByteLimit / 2;
|
|
238
|
+
const prefixBreak = data.lastIndexOf(0x0a, half);
|
|
239
|
+
const suffixBreak = data.indexOf(0x0a, data.length - half);
|
|
240
|
+
const prefixEnd = prefixBreak === -1 ? half : prefixBreak;
|
|
241
|
+
const suffixStart = suffixBreak === -1 ? data.length - half : suffixBreak + 1;
|
|
242
|
+
const elidedBytes = Math.max(0, suffixStart - prefixEnd);
|
|
243
|
+
const marker = `\n\n[${Math.round(elidedBytes / 1024)} KB elided from the middle of the file]\n\n`;
|
|
244
|
+
return {
|
|
245
|
+
content: decodeText(data.subarray(0, prefixEnd)) +
|
|
246
|
+
marker +
|
|
247
|
+
decodeText(data.subarray(suffixStart)),
|
|
248
|
+
truncated: true,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
231
251
|
const mimeTypes = {
|
|
232
252
|
".aac": "audio/aac",
|
|
233
253
|
".bmp": "image/bmp",
|
|
@@ -281,8 +301,7 @@ function decodeUtf32(data, littleEndian) {
|
|
|
281
301
|
const codePoint = littleEndian
|
|
282
302
|
? data.readUInt32LE(index)
|
|
283
303
|
: data.readUInt32BE(index);
|
|
284
|
-
characters.push(codePoint <= 0x10ffff &&
|
|
285
|
-
!(codePoint >= 0xd800 && codePoint <= 0xdfff)
|
|
304
|
+
characters.push(codePoint <= 0x10ffff && !(codePoint >= 0xd800 && codePoint <= 0xdfff)
|
|
286
305
|
? String.fromCodePoint(codePoint)
|
|
287
306
|
: "\ufffd");
|
|
288
307
|
}
|
|
@@ -560,12 +579,15 @@ class ArenaToolExecutor {
|
|
|
560
579
|
config;
|
|
561
580
|
workspace;
|
|
562
581
|
workspaceReal;
|
|
582
|
+
tmpReal;
|
|
583
|
+
varTmpReal;
|
|
563
584
|
validators = new Map();
|
|
564
585
|
pendingVisualContent = new Map();
|
|
565
586
|
askUserHandler;
|
|
566
587
|
imageInputFallbackClient;
|
|
567
588
|
signal;
|
|
568
589
|
workspaceContext;
|
|
590
|
+
nativeSandboxPaths;
|
|
569
591
|
pageReader;
|
|
570
592
|
routifyPageReader;
|
|
571
593
|
processManager;
|
|
@@ -587,6 +609,13 @@ class ArenaToolExecutor {
|
|
|
587
609
|
this.workspace = path.resolve(config.workspace);
|
|
588
610
|
mkdirSync(this.workspace, { recursive: true });
|
|
589
611
|
this.workspaceReal = realpathSync(this.workspace);
|
|
612
|
+
// A Linux /home/user workspace is already an OS-level sandbox. Other
|
|
613
|
+
// callers still confine ordinary workspace paths, while /tmp and /var/tmp
|
|
614
|
+
// always refer to the actual system directories seen by Bash.
|
|
615
|
+
this.nativeSandboxPaths =
|
|
616
|
+
process.platform === "linux" && this.workspaceReal === "/home/user";
|
|
617
|
+
this.tmpReal = realpathSync("/tmp");
|
|
618
|
+
this.varTmpReal = realpathSync("/var/tmp");
|
|
590
619
|
this.processManager = processManagerForWorkspace(this.workspaceReal);
|
|
591
620
|
this.voiceRegistry = voiceRegistryForWorkspace(this.workspaceReal);
|
|
592
621
|
this.askUserHandler = options.askUserHandler;
|
|
@@ -747,10 +776,9 @@ class ArenaToolExecutor {
|
|
|
747
776
|
const message = errorMessage(error);
|
|
748
777
|
const durationMs = Date.now() - startedAt;
|
|
749
778
|
if (name === "bash") {
|
|
750
|
-
const stderr = this.virtualizeBashOutput(message);
|
|
751
779
|
return {
|
|
752
780
|
status: "shell_error",
|
|
753
|
-
stderr,
|
|
781
|
+
stderr: message,
|
|
754
782
|
stdout: "",
|
|
755
783
|
exit_code: null,
|
|
756
784
|
duration_ms: durationMs,
|
|
@@ -892,28 +920,57 @@ class ArenaToolExecutor {
|
|
|
892
920
|
if (!raw)
|
|
893
921
|
throw new Error("path must not be empty");
|
|
894
922
|
let candidate;
|
|
895
|
-
|
|
896
|
-
|
|
923
|
+
let confinementRoot = this.workspace;
|
|
924
|
+
let confinementRootReal = this.workspaceReal;
|
|
925
|
+
const expanded = raw === "~"
|
|
926
|
+
? "/home/user"
|
|
927
|
+
: raw.startsWith("~/")
|
|
928
|
+
? `/home/user/${raw.slice(2)}`
|
|
929
|
+
: raw;
|
|
930
|
+
const virtualAbsolute = path.posix.resolve("/home/user", expanded);
|
|
931
|
+
let suffix;
|
|
932
|
+
if (virtualAbsolute === "/var/tmp" ||
|
|
933
|
+
virtualAbsolute.startsWith("/var/tmp/")) {
|
|
934
|
+
confinementRoot = "/var/tmp";
|
|
935
|
+
confinementRootReal = this.varTmpReal;
|
|
936
|
+
suffix = virtualAbsolute.slice("/var/tmp".length).replace(/^\/+/, "");
|
|
937
|
+
candidate = path.join(confinementRoot, suffix);
|
|
897
938
|
}
|
|
898
|
-
else if (
|
|
899
|
-
|
|
939
|
+
else if (virtualAbsolute === "/tmp" ||
|
|
940
|
+
virtualAbsolute.startsWith("/tmp/")) {
|
|
941
|
+
confinementRoot = "/tmp";
|
|
942
|
+
confinementRootReal = this.tmpReal;
|
|
943
|
+
suffix = virtualAbsolute.slice("/tmp".length).replace(/^\/+/, "");
|
|
944
|
+
candidate = path.join(confinementRoot, suffix);
|
|
900
945
|
}
|
|
901
|
-
else if (
|
|
946
|
+
else if (this.nativeSandboxPaths && path.posix.isAbsolute(raw)) {
|
|
947
|
+
confinementRoot = "/";
|
|
948
|
+
confinementRootReal = "/";
|
|
949
|
+
candidate = raw;
|
|
950
|
+
}
|
|
951
|
+
else if (!this.workspaceContext) {
|
|
952
|
+
if (virtualAbsolute === "/home/user" ||
|
|
953
|
+
virtualAbsolute.startsWith("/home/user/")) {
|
|
954
|
+
suffix = virtualAbsolute.slice("/home/user".length).replace(/^\/+/, "");
|
|
955
|
+
}
|
|
956
|
+
else {
|
|
957
|
+
throw new Error(`path is outside the workspace: ${raw}`);
|
|
958
|
+
}
|
|
959
|
+
candidate = path.join(confinementRoot, suffix);
|
|
960
|
+
}
|
|
961
|
+
else if (raw === "/home/user") {
|
|
902
962
|
candidate = this.workspace;
|
|
903
963
|
}
|
|
904
|
-
else if (
|
|
905
|
-
candidate = path.join(this.workspace, raw.slice(
|
|
964
|
+
else if (raw.startsWith("/home/user/")) {
|
|
965
|
+
candidate = path.join(this.workspace, raw.slice("/home/user/".length));
|
|
906
966
|
}
|
|
907
967
|
else {
|
|
908
|
-
const defaultCwd = this.workspaceContext
|
|
909
|
-
? path.resolve(this.workspaceContext.cwd)
|
|
910
|
-
: this.workspace;
|
|
968
|
+
const defaultCwd = path.resolve(this.workspaceContext.cwd);
|
|
911
969
|
candidate = path.isAbsolute(raw) ? raw : path.join(defaultCwd, raw);
|
|
912
970
|
}
|
|
913
971
|
const absolute = path.resolve(candidate);
|
|
914
|
-
const lexicalRelative = path.relative(
|
|
915
|
-
if (lexicalRelative.startsWith("..") ||
|
|
916
|
-
path.isAbsolute(lexicalRelative)) {
|
|
972
|
+
const lexicalRelative = path.relative(confinementRoot, absolute);
|
|
973
|
+
if (lexicalRelative.startsWith("..") || path.isAbsolute(lexicalRelative)) {
|
|
917
974
|
throw new Error(`path is outside the workspace: ${raw}`);
|
|
918
975
|
}
|
|
919
976
|
let ancestor = absolute;
|
|
@@ -925,33 +982,45 @@ class ArenaToolExecutor {
|
|
|
925
982
|
}
|
|
926
983
|
const ancestorReal = await realpath(ancestor);
|
|
927
984
|
const resolved = path.resolve(ancestorReal, path.relative(ancestor, absolute));
|
|
928
|
-
const realRelative = path.relative(
|
|
985
|
+
const realRelative = path.relative(confinementRootReal, resolved);
|
|
929
986
|
if (realRelative.startsWith("..") || path.isAbsolute(realRelative)) {
|
|
930
987
|
throw new Error(`path is outside the workspace: ${raw}`);
|
|
931
988
|
}
|
|
932
989
|
return resolved;
|
|
933
990
|
}
|
|
934
991
|
relativePath(target) {
|
|
992
|
+
for (const [root, virtualRoot] of [
|
|
993
|
+
[this.varTmpReal, "/var/tmp"],
|
|
994
|
+
[this.tmpReal, "/tmp"],
|
|
995
|
+
]) {
|
|
996
|
+
const relative = path.relative(root, target);
|
|
997
|
+
if (!relative.startsWith("..") && !path.isAbsolute(relative)) {
|
|
998
|
+
const suffix = relative.split(path.sep).join("/");
|
|
999
|
+
return suffix ? `${virtualRoot}/${suffix}` : virtualRoot;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
935
1002
|
return path.relative(this.workspaceReal, target).split(path.sep).join("/");
|
|
936
1003
|
}
|
|
937
|
-
|
|
938
|
-
if (this.workspaceContext)
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
1004
|
+
displayWorkspacePath(rawPath) {
|
|
1005
|
+
if (!this.workspaceContext) {
|
|
1006
|
+
if (rawPath === "/home/user" || rawPath === "~")
|
|
1007
|
+
return ".";
|
|
1008
|
+
if (rawPath.startsWith("/home/user/")) {
|
|
1009
|
+
return rawPath.slice("/home/user/".length);
|
|
1010
|
+
}
|
|
1011
|
+
if (rawPath.startsWith("~/"))
|
|
1012
|
+
return rawPath.slice(2);
|
|
946
1013
|
}
|
|
947
|
-
return
|
|
1014
|
+
return rawPath;
|
|
948
1015
|
}
|
|
949
1016
|
sandboxEnvironment() {
|
|
950
1017
|
const env = {
|
|
951
1018
|
...process.env,
|
|
952
1019
|
ARENA_WORKSPACE: this.workspace,
|
|
953
|
-
|
|
954
|
-
|
|
1020
|
+
// Agent Arena captures use C character classification (grep -P, wc -w).
|
|
1021
|
+
// This controls command behavior, not the UTF-8 encoding of stored files.
|
|
1022
|
+
LANG: "C",
|
|
1023
|
+
LC_ALL: "C",
|
|
955
1024
|
};
|
|
956
1025
|
delete env.FORCE_COLOR;
|
|
957
1026
|
delete env.CLICOLOR_FORCE;
|
|
@@ -959,20 +1028,22 @@ class ArenaToolExecutor {
|
|
|
959
1028
|
}
|
|
960
1029
|
async bash(command, cwd, timeoutSeconds) {
|
|
961
1030
|
const workdir = await this.confinedPath(cwd);
|
|
962
|
-
|
|
1031
|
+
const metadata = await stat(workdir).catch((error) => {
|
|
1032
|
+
// A missing cwd prevents shell startup; captured Bash stderr is empty.
|
|
1033
|
+
// Keep other filesystem failures and start_process diagnostics intact.
|
|
1034
|
+
if (error.code === "ENOENT")
|
|
1035
|
+
throw new Error("");
|
|
1036
|
+
throw error;
|
|
1037
|
+
});
|
|
1038
|
+
if (!metadata.isDirectory()) {
|
|
963
1039
|
throw new Error(`cwd is not a directory: ${cwd}`);
|
|
964
1040
|
}
|
|
965
|
-
// Legacy callers without runtime context retain the captured /home/user
|
|
966
|
-
// contract. Runtime-aware callers execute and report their actual paths.
|
|
967
|
-
const translatedCommand = this.workspaceContext
|
|
968
|
-
? command
|
|
969
|
-
: command.replaceAll("/home/user", "\"$ARENA_WORKSPACE\"");
|
|
970
1041
|
return new Promise((resolve, reject) => {
|
|
971
1042
|
const env = this.sandboxEnvironment();
|
|
972
1043
|
// detached:管道成员(npm、tail…)与 bash 同进程组。只杀 bash 会留下
|
|
973
1044
|
// 孤儿持有 stdout/stderr 管道,close 永不触发,整个 turn 挂死;
|
|
974
1045
|
// 必须杀整组,并在宽限期后强制结算兜底。
|
|
975
|
-
const child = spawn("/bin/bash", ["-lc",
|
|
1046
|
+
const child = spawn("/bin/bash", ["-lc", command], {
|
|
976
1047
|
cwd: workdir,
|
|
977
1048
|
env,
|
|
978
1049
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -1017,8 +1088,8 @@ class ArenaToolExecutor {
|
|
|
1017
1088
|
reject(this.signal.reason || new Error("operation cancelled"));
|
|
1018
1089
|
return;
|
|
1019
1090
|
}
|
|
1020
|
-
const out = capBashOutput(
|
|
1021
|
-
const err = capBashOutput(
|
|
1091
|
+
const out = capBashOutput(stdout);
|
|
1092
|
+
const err = capBashOutput(stderr);
|
|
1022
1093
|
resolve({
|
|
1023
1094
|
status: timedOut ? "timeout" : signal ? "killed" : "completed",
|
|
1024
1095
|
stderr: err.text,
|
|
@@ -1071,12 +1142,9 @@ class ArenaToolExecutor {
|
|
|
1071
1142
|
if (!(await stat(workdir)).isDirectory()) {
|
|
1072
1143
|
throw new Error(`cwd is not a directory: ${cwd}`);
|
|
1073
1144
|
}
|
|
1074
|
-
const translatedCommand = this.workspaceContext
|
|
1075
|
-
? command
|
|
1076
|
-
: command.replaceAll("/home/user", '"$ARENA_WORKSPACE"');
|
|
1077
1145
|
return this.processManager.start({
|
|
1078
1146
|
name,
|
|
1079
|
-
command
|
|
1147
|
+
command,
|
|
1080
1148
|
cwd: workdir,
|
|
1081
1149
|
startupWaitSeconds,
|
|
1082
1150
|
env: this.sandboxEnvironment(),
|
|
@@ -1085,10 +1153,12 @@ class ArenaToolExecutor {
|
|
|
1085
1153
|
}
|
|
1086
1154
|
async askUser(questions, toolCallId, context) {
|
|
1087
1155
|
if (this.askUserHandler) {
|
|
1088
|
-
return this.askUserHandler(toolCallId, questions, context
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1156
|
+
return this.askUserHandler(toolCallId, questions, context
|
|
1157
|
+
? {
|
|
1158
|
+
...context,
|
|
1159
|
+
signal: this.signal || context.signal,
|
|
1160
|
+
}
|
|
1161
|
+
: undefined);
|
|
1092
1162
|
}
|
|
1093
1163
|
// ask_user is not offered in non-interactive sessions; if the model still
|
|
1094
1164
|
// emits it (e.g. resumed history or a hallucinated call), report an error
|
|
@@ -1168,23 +1238,25 @@ class ArenaToolExecutor {
|
|
|
1168
1238
|
catch {
|
|
1169
1239
|
return {
|
|
1170
1240
|
status: "error",
|
|
1171
|
-
message: `Failed to read workspace file: ${rawPath}`,
|
|
1241
|
+
message: `Failed to read workspace file: ${this.displayWorkspacePath(rawPath)}`,
|
|
1172
1242
|
};
|
|
1173
1243
|
}
|
|
1174
|
-
//
|
|
1175
|
-
//
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
if (!source.includes(oldText)) {
|
|
1244
|
+
// Agent Arena replaces the first match and tolerates multiline whitespace
|
|
1245
|
+
// drift. Code Arena deliberately uses a separate unique-match policy.
|
|
1246
|
+
const edited = replaceFirstText(source, oldText, newText, {
|
|
1247
|
+
whitespaceInsensitiveMultiline: true,
|
|
1248
|
+
});
|
|
1249
|
+
if (edited === undefined) {
|
|
1181
1250
|
return {
|
|
1182
1251
|
status: "error",
|
|
1183
1252
|
message: "Context not found. Read the file to verify the text exists.",
|
|
1184
1253
|
};
|
|
1185
1254
|
}
|
|
1186
1255
|
try {
|
|
1187
|
-
|
|
1256
|
+
// Replacement text is literal. String.replace(string, string) expands
|
|
1257
|
+
// $&, $`, $' and $$ sequences and therefore corrupts JavaScript/jQuery
|
|
1258
|
+
// snippets observed in production trajectories.
|
|
1259
|
+
await writeFile(target, edited, "utf8");
|
|
1188
1260
|
}
|
|
1189
1261
|
catch {
|
|
1190
1262
|
return {
|
|
@@ -1195,10 +1267,11 @@ class ArenaToolExecutor {
|
|
|
1195
1267
|
return { status: "success", message: `Edited ${rawPath}.` };
|
|
1196
1268
|
}
|
|
1197
1269
|
async readWorkspaceFile(rawPath, toolCallId) {
|
|
1198
|
-
|
|
1270
|
+
let target;
|
|
1199
1271
|
let fileSize;
|
|
1200
1272
|
let sample;
|
|
1201
1273
|
try {
|
|
1274
|
+
target = await this.confinedPath(rawPath);
|
|
1202
1275
|
const metadata = await stat(target);
|
|
1203
1276
|
if (!metadata.isFile())
|
|
1204
1277
|
throw new Error("not a regular file");
|
|
@@ -1215,23 +1288,13 @@ class ArenaToolExecutor {
|
|
|
1215
1288
|
}
|
|
1216
1289
|
}
|
|
1217
1290
|
catch {
|
|
1218
|
-
// 逐字对齐生产错误文案,路径按调用方原样回显。
|
|
1219
1291
|
return {
|
|
1220
1292
|
status: "error",
|
|
1221
|
-
message: `Failed to read workspace file: ${rawPath}`,
|
|
1293
|
+
message: `Failed to read workspace file: ${this.displayWorkspacePath(rawPath)}`,
|
|
1222
1294
|
};
|
|
1223
1295
|
}
|
|
1224
1296
|
const extension = path.extname(target).toLowerCase();
|
|
1225
1297
|
const kind = workspaceFileKind(target, sample);
|
|
1226
|
-
if (kind === "text" && fileSize > textFileByteLimit) {
|
|
1227
|
-
return {
|
|
1228
|
-
status: "error",
|
|
1229
|
-
message: `File content (${displayByteSize(fileSize)}) exceeds the maximum allowed size ` +
|
|
1230
|
-
`(${displayByteSize(textFileByteLimit)}). Use bash with sed/head/tail to read ` +
|
|
1231
|
-
"specific portions of the file, or grep/rg to search for specific content " +
|
|
1232
|
-
"instead of reading the whole file.",
|
|
1233
|
-
};
|
|
1234
|
-
}
|
|
1235
1298
|
if (kind === "image" && fileSize > 20 * 1024 * 1024) {
|
|
1236
1299
|
throw new Error(`image exceeds 20 MB: ${rawPath}`);
|
|
1237
1300
|
}
|
|
@@ -1258,7 +1321,7 @@ class ArenaToolExecutor {
|
|
|
1258
1321
|
catch {
|
|
1259
1322
|
return {
|
|
1260
1323
|
status: "error",
|
|
1261
|
-
message: `Failed to read workspace file: ${rawPath}`,
|
|
1324
|
+
message: `Failed to read workspace file: ${this.displayWorkspacePath(rawPath)}`,
|
|
1262
1325
|
};
|
|
1263
1326
|
}
|
|
1264
1327
|
if (isStructuredDocument(target)) {
|
|
@@ -1286,12 +1349,13 @@ class ArenaToolExecutor {
|
|
|
1286
1349
|
return result;
|
|
1287
1350
|
}
|
|
1288
1351
|
if (kind === "text") {
|
|
1289
|
-
const
|
|
1352
|
+
const text = readTextResult(data);
|
|
1290
1353
|
return {
|
|
1291
1354
|
kind: "text",
|
|
1292
|
-
lines:
|
|
1293
|
-
size:
|
|
1294
|
-
content,
|
|
1355
|
+
lines: data.reduce((count, byte) => count + (byte === 0x0a ? 1 : 0), 1),
|
|
1356
|
+
size: data.length,
|
|
1357
|
+
content: text.content,
|
|
1358
|
+
...(text.truncated ? { truncated: true } : {}),
|
|
1295
1359
|
};
|
|
1296
1360
|
}
|
|
1297
1361
|
const mimeType = mime.getType(target) ||
|
|
@@ -1341,11 +1405,22 @@ class ArenaToolExecutor {
|
|
|
1341
1405
|
};
|
|
1342
1406
|
}
|
|
1343
1407
|
async writeWorkspaceFile(rawPath, content) {
|
|
1344
|
-
let target;
|
|
1345
1408
|
try {
|
|
1346
|
-
target = await this.confinedPath(rawPath);
|
|
1409
|
+
const target = await this.confinedPath(rawPath);
|
|
1347
1410
|
mkdirSync(path.dirname(target), { recursive: true });
|
|
1348
|
-
|
|
1411
|
+
try {
|
|
1412
|
+
await writeFile(target, content, "utf8");
|
|
1413
|
+
}
|
|
1414
|
+
catch (error) {
|
|
1415
|
+
const code = error.code;
|
|
1416
|
+
if (!["EACCES", "EPERM"].includes(code || "") || !privilegedFileCreationEnabled({
|
|
1417
|
+
platform: process.platform, workspace: this.workspaceReal,
|
|
1418
|
+
docker: existsSync("/.dockerenv"), flag: process.env.ARENA_SANDBOX_PRIVILEGED_FILE_CREATION,
|
|
1419
|
+
uid: process.getuid?.(),
|
|
1420
|
+
}))
|
|
1421
|
+
throw error;
|
|
1422
|
+
await createSandboxFile(target, content);
|
|
1423
|
+
}
|
|
1349
1424
|
}
|
|
1350
1425
|
catch {
|
|
1351
1426
|
return {
|
|
@@ -1368,18 +1443,21 @@ class ArenaToolExecutor {
|
|
|
1368
1443
|
const slug = query
|
|
1369
1444
|
.toLowerCase()
|
|
1370
1445
|
.replace(/[^a-z0-9]+/g, "-")
|
|
1371
|
-
.
|
|
1446
|
+
.replace(/^-+|-+$/g, "")
|
|
1447
|
+
.slice(0, 40) || "results";
|
|
1372
1448
|
for (const value of results) {
|
|
1373
1449
|
if (saved.length >= count)
|
|
1374
1450
|
break;
|
|
1375
1451
|
const item = record(value);
|
|
1376
1452
|
const properties = record(item.properties);
|
|
1377
1453
|
const thumbnail = record(item.thumbnail);
|
|
1378
|
-
|
|
1379
|
-
const
|
|
1380
|
-
|
|
1454
|
+
// Brave's result URL is the source page; properties.url is an image asset.
|
|
1455
|
+
const sourceUrl = stringValue(item.url);
|
|
1456
|
+
const imageUrl = stringValue(properties.url) || stringValue(thumbnail.src);
|
|
1457
|
+
const thumbnailUrl = stringValue(thumbnail.src) || imageUrl;
|
|
1458
|
+
if (!imageUrl)
|
|
1381
1459
|
continue;
|
|
1382
|
-
const downloadUrls = [...new Set([
|
|
1460
|
+
const downloadUrls = [...new Set([imageUrl, thumbnailUrl])];
|
|
1383
1461
|
for (const downloadUrl of downloadUrls) {
|
|
1384
1462
|
try {
|
|
1385
1463
|
const imageResponse = await fetch(downloadUrl, {
|
|
@@ -1394,7 +1472,9 @@ class ArenaToolExecutor {
|
|
|
1394
1472
|
.split(";")[0]
|
|
1395
1473
|
?.trim()
|
|
1396
1474
|
.toLowerCase();
|
|
1397
|
-
const extension =
|
|
1475
|
+
const extension = normalizedType === "image/jpeg"
|
|
1476
|
+
? ".jpg"
|
|
1477
|
+
: Object.entries(mimeTypes).find(([, mime]) => mime === normalizedType)?.[0] || ".jpg";
|
|
1398
1478
|
const target = await this.confinedPath(`image-search/${slug}-${saved.length + 1}${extension}`);
|
|
1399
1479
|
mkdirSync(path.dirname(target), { recursive: true });
|
|
1400
1480
|
await writeFile(target, await responseBytes(imageResponse));
|
|
@@ -1475,7 +1555,7 @@ class ArenaToolExecutor {
|
|
|
1475
1555
|
await writeFile(target, await generatedImageBytesForExtension(generated.bytes, generated.mimeType, extension));
|
|
1476
1556
|
return {
|
|
1477
1557
|
status: "success",
|
|
1478
|
-
file_path:
|
|
1558
|
+
file_path: rawPath,
|
|
1479
1559
|
};
|
|
1480
1560
|
}
|
|
1481
1561
|
async generateSpeech(rawPath, text, voiceId, language) {
|
|
@@ -1511,7 +1591,7 @@ class ArenaToolExecutor {
|
|
|
1511
1591
|
}));
|
|
1512
1592
|
return {
|
|
1513
1593
|
status: "success",
|
|
1514
|
-
file_path:
|
|
1594
|
+
file_path: rawPath,
|
|
1515
1595
|
};
|
|
1516
1596
|
}
|
|
1517
1597
|
}
|