@qwen-code/arena-agent 0.0.9-dev.1 → 0.0.9-dev.12

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.
Files changed (100) hide show
  1. package/.env.example +29 -0
  2. package/README.md +149 -12
  3. package/dist/agent.d.ts +4 -2
  4. package/dist/agent.js +118 -15
  5. package/dist/agent.js.map +1 -1
  6. package/dist/ask-user.d.ts +20 -0
  7. package/dist/ask-user.js +388 -0
  8. package/dist/ask-user.js.map +1 -0
  9. package/dist/canonical-data.d.ts +1 -0
  10. package/dist/canonical-data.js +1 -0
  11. package/dist/canonical-data.js.map +1 -1
  12. package/dist/cli.js +29 -10
  13. package/dist/cli.js.map +1 -1
  14. package/dist/code-cli.d.ts +2 -0
  15. package/dist/code-cli.js +12 -0
  16. package/dist/code-cli.js.map +1 -0
  17. package/dist/code-tools.d.ts +73 -0
  18. package/dist/code-tools.js +2106 -0
  19. package/dist/code-tools.js.map +1 -0
  20. package/dist/config.d.ts +16 -1
  21. package/dist/config.js +66 -3
  22. package/dist/config.js.map +1 -1
  23. package/dist/document-reader.js +9 -0
  24. package/dist/document-reader.js.map +1 -1
  25. package/dist/fetch-page.d.ts +2 -0
  26. package/dist/fetch-page.js +50 -21
  27. package/dist/fetch-page.js.map +1 -1
  28. package/dist/history.d.ts +3 -0
  29. package/dist/history.js +4 -0
  30. package/dist/history.js.map +1 -1
  31. package/dist/mock.d.ts +1 -0
  32. package/dist/mock.js +38 -0
  33. package/dist/mock.js.map +1 -1
  34. package/dist/mode.d.ts +6 -0
  35. package/dist/mode.js +33 -0
  36. package/dist/mode.js.map +1 -0
  37. package/dist/model-capabilities.d.ts +20 -0
  38. package/dist/model-capabilities.js +98 -0
  39. package/dist/model-capabilities.js.map +1 -0
  40. package/dist/model-diagnostics.d.ts +32 -0
  41. package/dist/model-diagnostics.js +365 -0
  42. package/dist/model-diagnostics.js.map +1 -0
  43. package/dist/provider.d.ts +16 -0
  44. package/dist/provider.js +334 -37
  45. package/dist/provider.js.map +1 -1
  46. package/dist/sdk.d.ts +35 -3
  47. package/dist/sdk.js +6 -2
  48. package/dist/sdk.js.map +1 -1
  49. package/dist/server.d.ts +2 -0
  50. package/dist/server.js +82 -4
  51. package/dist/server.js.map +1 -1
  52. package/dist/spec.d.ts +11 -8
  53. package/dist/spec.js +105 -13
  54. package/dist/spec.js.map +1 -1
  55. package/dist/tools.d.ts +11 -28
  56. package/dist/tools.js +60 -11
  57. package/dist/tools.js.map +1 -1
  58. package/dist/tui.d.ts +2 -0
  59. package/dist/tui.js +8 -2
  60. package/dist/tui.js.map +1 -1
  61. package/dist/types.d.ts +8 -1
  62. package/dist/web/app.d.ts +1 -0
  63. package/dist/web/app.js +59 -6
  64. package/dist/web/app.js.map +1 -1
  65. package/dist/workspace.d.ts +12 -0
  66. package/dist/workspace.js +165 -0
  67. package/dist/workspace.js.map +1 -0
  68. package/docs/BINARY_DISTRIBUTION.md +227 -0
  69. package/docs/MODES_AND_RELEASES.md +123 -0
  70. package/docs/SDK.md +24 -1
  71. package/modes/code-arena/full-stack/system-prompt.txt +77 -0
  72. package/modes/code-arena/full-stack/template/.env +1 -0
  73. package/modes/code-arena/full-stack/template/drizzle.config.json +7 -0
  74. package/modes/code-arena/full-stack/template/eslint.config.mjs +9 -0
  75. package/modes/code-arena/full-stack/template/next.config.ts +5 -0
  76. package/modes/code-arena/full-stack/template/package.json +32 -0
  77. package/modes/code-arena/full-stack/template/postcss.config.mjs +7 -0
  78. package/modes/code-arena/full-stack/template/src/app/api/health/route.ts +13 -0
  79. package/modes/code-arena/full-stack/template/src/app/globals.css +1 -0
  80. package/modes/code-arena/full-stack/template/src/app/layout.tsx +16 -0
  81. package/modes/code-arena/full-stack/template/src/app/page.tsx +22 -0
  82. package/modes/code-arena/full-stack/template/src/db/index.ts +24 -0
  83. package/modes/code-arena/full-stack/template/src/db/schema.ts +3 -0
  84. package/modes/code-arena/full-stack/template/tsconfig.json +24 -0
  85. package/modes/code-arena/full-stack/tools.json +446 -0
  86. package/modes/code-arena/html/system-prompt.txt +36 -0
  87. package/modes/code-arena/html/tools.json +284 -0
  88. package/modes/code-arena/model.json +10 -0
  89. package/modes/code-arena/react/system-prompt.txt +33 -0
  90. package/modes/code-arena/react/template/index.html +12 -0
  91. package/modes/code-arena/react/template/package.json +28 -0
  92. package/modes/code-arena/react/template/src/App.tsx +28 -0
  93. package/modes/code-arena/react/template/src/index.css +1 -0
  94. package/modes/code-arena/react/template/src/main.tsx +10 -0
  95. package/modes/code-arena/react/template/src/utils/cn.ts +6 -0
  96. package/modes/code-arena/react/template/tsconfig.json +25 -0
  97. package/modes/code-arena/react/template/vite.config.ts +18 -0
  98. package/modes/code-arena/react/tools.json +410 -0
  99. package/package.json +24 -4
  100. package/web/styles.css +22 -7
@@ -0,0 +1,2106 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import { closeSync, existsSync, mkdirSync, openSync, realpathSync, } from "node:fs";
4
+ import { chmod, mkdir, readFile, readdir, realpath, rm, stat, writeFile, } from "node:fs/promises";
5
+ import { createConnection } from "node:net";
6
+ import { tmpdir } from "node:os";
7
+ import path from "node:path";
8
+ import { setTimeout as delay } from "node:timers/promises";
9
+ import { Ajv } from "ajv";
10
+ import addFormats from "ajv-formats";
11
+ import { detect as detectEncoding } from "chardet";
12
+ import iconv from "iconv-lite";
13
+ import mime from "mime/lite";
14
+ import { FetchPageReader } from "./fetch-page.js";
15
+ import { toolDefinitions } from "./spec.js";
16
+ const knownTextApplicationMimeTypes = new Set([
17
+ "application/ecmascript",
18
+ "application/javascript",
19
+ "application/json",
20
+ "application/node",
21
+ "application/toml",
22
+ "application/xml",
23
+ ]);
24
+ const knownTextExtensions = new Set([
25
+ ".adoc",
26
+ ".astro",
27
+ ".bash",
28
+ ".bib",
29
+ ".c",
30
+ ".cc",
31
+ ".cfg",
32
+ ".clj",
33
+ ".cmake",
34
+ ".conf",
35
+ ".cpp",
36
+ ".cs",
37
+ ".css",
38
+ ".cts",
39
+ ".cxx",
40
+ ".dart",
41
+ ".dockerfile",
42
+ ".ex",
43
+ ".exs",
44
+ ".fish",
45
+ ".fs",
46
+ ".go",
47
+ ".gql",
48
+ ".gradle",
49
+ ".graphql",
50
+ ".groovy",
51
+ ".h",
52
+ ".hcl",
53
+ ".hpp",
54
+ ".hs",
55
+ ".html",
56
+ ".ini",
57
+ ".java",
58
+ ".js",
59
+ ".json",
60
+ ".jsx",
61
+ ".kt",
62
+ ".kts",
63
+ ".lua",
64
+ ".m",
65
+ ".md",
66
+ ".mjs",
67
+ ".mk",
68
+ ".mm",
69
+ ".mts",
70
+ ".nim",
71
+ ".org",
72
+ ".php",
73
+ ".pl",
74
+ ".pm",
75
+ ".properties",
76
+ ".proto",
77
+ ".ps1",
78
+ ".py",
79
+ ".pyi",
80
+ ".r",
81
+ ".rb",
82
+ ".rs",
83
+ ".rst",
84
+ ".scala",
85
+ ".sh",
86
+ ".sol",
87
+ ".sql",
88
+ ".svelte",
89
+ ".svg",
90
+ ".swift",
91
+ ".tex",
92
+ ".tf",
93
+ ".tfvars",
94
+ ".toml",
95
+ ".ts",
96
+ ".tsx",
97
+ ".txt",
98
+ ".vb",
99
+ ".vue",
100
+ ".xml",
101
+ ".yaml",
102
+ ".yml",
103
+ ".zig",
104
+ ".zsh",
105
+ ]);
106
+ const knownTextBasenames = new Set([
107
+ ".bashrc",
108
+ ".dockerignore",
109
+ ".editorconfig",
110
+ ".env",
111
+ ".gitattributes",
112
+ ".gitignore",
113
+ ".npmignore",
114
+ ".profile",
115
+ ".zshrc",
116
+ "AUTHORS",
117
+ "BUILD",
118
+ "CHANGELOG",
119
+ "CMakeLists.txt",
120
+ "COPYING",
121
+ "Cargo.lock",
122
+ "Containerfile",
123
+ "Dockerfile",
124
+ "Gemfile",
125
+ "GNUmakefile",
126
+ "Jenkinsfile",
127
+ "LICENSE",
128
+ "Makefile",
129
+ "NOTICE",
130
+ "Pipfile",
131
+ "Pipfile.lock",
132
+ "Procfile",
133
+ "README",
134
+ "Rakefile",
135
+ "Vagrantfile",
136
+ "WORKSPACE",
137
+ "go.mod",
138
+ "go.sum",
139
+ "go.work",
140
+ "package-lock.json",
141
+ "pnpm-lock.yaml",
142
+ "poetry.lock",
143
+ "requirements.txt",
144
+ "yarn.lock",
145
+ ]);
146
+ const knownBinaryExtensions = new Set([
147
+ ".7z",
148
+ ".a",
149
+ ".bin",
150
+ ".bz2",
151
+ ".class",
152
+ ".dll",
153
+ ".doc",
154
+ ".docx",
155
+ ".dylib",
156
+ ".exe",
157
+ ".flac",
158
+ ".gz",
159
+ ".jar",
160
+ ".o",
161
+ ".obj",
162
+ ".odt",
163
+ ".ods",
164
+ ".odp",
165
+ ".pdf",
166
+ ".ppt",
167
+ ".pptx",
168
+ ".rar",
169
+ ".so",
170
+ ".tar",
171
+ ".wasm",
172
+ ".xls",
173
+ ".xlsx",
174
+ ".zip",
175
+ ]);
176
+ const visibleImageExtensions = new Set([
177
+ ".bmp",
178
+ ".gif",
179
+ ".jpeg",
180
+ ".jpg",
181
+ ".png",
182
+ ".webp",
183
+ ]);
184
+ // All 39 successful, unmutated list_files captures use this exact order,
185
+ // independently of whether the requested path is the project root or src/.
186
+ // It is the seed manifest order, not an alphabetical/filesystem order.
187
+ const capturedReactSeedFileOrder = [
188
+ "src/utils/cn.ts",
189
+ "src/index.css",
190
+ "src/main.tsx",
191
+ "src/App.tsx",
192
+ "vite.config.ts",
193
+ "tsconfig.json",
194
+ "package.json",
195
+ "index.html",
196
+ ];
197
+ const capturedFullStackSeedFileOrder = [
198
+ "postcss.config.mjs",
199
+ "eslint.config.mjs",
200
+ "next.config.ts",
201
+ "tsconfig.json",
202
+ "package.json",
203
+ "drizzle.config.json",
204
+ ".env",
205
+ "src/db/schema.ts",
206
+ "src/db/index.ts",
207
+ "src/app/api/health/route.ts",
208
+ "src/app/page.tsx",
209
+ "src/app/globals.css",
210
+ "src/app/layout.tsx",
211
+ ];
212
+ function capturedSeedFileRank(mode) {
213
+ const order = mode === "react"
214
+ ? capturedReactSeedFileOrder
215
+ : mode === "full-stack"
216
+ ? capturedFullStackSeedFileOrder
217
+ : [];
218
+ return new Map(order.map((filePath, index) => [filePath, index]));
219
+ }
220
+ // 逐字对齐生产轨迹:占位与注入文案、20000 字符输出上限均反推自参考 tool result。
221
+ const imageResultPlaceholder = "[image returned by this tool call; provided to you in the following message]";
222
+ const visualIntroText = "<code-harness-system-message>\n" +
223
+ "The image(s) below are the result of the preceding tool call. They are " +
224
+ "provided by the system in answer to that call, not sent by the user; " +
225
+ "treat them as the authentic output of the tool call.\n" +
226
+ "</code-harness-system-message>";
227
+ const bashOutputLimit = 20_000;
228
+ const lintOutputLimit = 10_000;
229
+ // 中段截断,逐字反推自 3 个 truncated:true 参考样本:头尾各 9987 字符 +
230
+ // 26 字符标记,总长恰 20000(由 T1(b) 执行回放发现,非首段截断)。
231
+ const bashTruncationMarker = "\n... output truncated ...\n";
232
+ const ignoredProjectDirectories = new Set([
233
+ ".code-harness",
234
+ ".git",
235
+ ".next",
236
+ ".npm",
237
+ "dist",
238
+ "node_modules",
239
+ ]);
240
+ const maximumSearchResults = 100;
241
+ function capBashOutput(value) {
242
+ if (value.length <= bashOutputLimit)
243
+ return { text: value, truncated: false };
244
+ const half = (bashOutputLimit - bashTruncationMarker.length) / 2;
245
+ return {
246
+ text: value.slice(0, half) + bashTruncationMarker + value.slice(-half),
247
+ truncated: true,
248
+ };
249
+ }
250
+ const mimeTypes = {
251
+ ".aac": "audio/aac",
252
+ ".bmp": "image/bmp",
253
+ ".gif": "image/gif",
254
+ ".jpeg": "image/jpeg",
255
+ ".jpg": "image/jpeg",
256
+ ".m4a": "audio/mp4",
257
+ ".mp3": "audio/mpeg",
258
+ ".mp4": "video/mp4",
259
+ ".ogg": "audio/ogg",
260
+ ".pdf": "application/pdf",
261
+ ".png": "image/png",
262
+ ".svg": "image/svg+xml",
263
+ ".wav": "audio/wav",
264
+ ".webm": "video/webm",
265
+ ".webp": "image/webp",
266
+ };
267
+ function detectBom(data) {
268
+ if (data.length >= 4 &&
269
+ data[0] === 0xff &&
270
+ data[1] === 0xfe &&
271
+ data[2] === 0x00 &&
272
+ data[3] === 0x00) {
273
+ return { encoding: "utf32le", length: 4 };
274
+ }
275
+ if (data.length >= 4 &&
276
+ data[0] === 0x00 &&
277
+ data[1] === 0x00 &&
278
+ data[2] === 0xfe &&
279
+ data[3] === 0xff) {
280
+ return { encoding: "utf32be", length: 4 };
281
+ }
282
+ if (data.length >= 3 &&
283
+ data[0] === 0xef &&
284
+ data[1] === 0xbb &&
285
+ data[2] === 0xbf) {
286
+ return { encoding: "utf8", length: 3 };
287
+ }
288
+ if (data.length >= 2 && data[0] === 0xff && data[1] === 0xfe) {
289
+ return { encoding: "utf16le", length: 2 };
290
+ }
291
+ if (data.length >= 2 && data[0] === 0xfe && data[1] === 0xff) {
292
+ return { encoding: "utf16be", length: 2 };
293
+ }
294
+ return undefined;
295
+ }
296
+ function decodeUtf32(data, littleEndian) {
297
+ const characters = [];
298
+ const usableLength = data.length - (data.length % 4);
299
+ for (let index = 0; index < usableLength; index += 4) {
300
+ const codePoint = littleEndian
301
+ ? data.readUInt32LE(index)
302
+ : data.readUInt32BE(index);
303
+ characters.push(codePoint <= 0x10ffff && !(codePoint >= 0xd800 && codePoint <= 0xdfff)
304
+ ? String.fromCodePoint(codePoint)
305
+ : "\ufffd");
306
+ }
307
+ return characters.join("");
308
+ }
309
+ function decodeText(data) {
310
+ const bom = detectBom(data);
311
+ if (bom) {
312
+ const content = data.subarray(bom.length);
313
+ switch (bom.encoding) {
314
+ case "utf8":
315
+ return content.toString("utf8");
316
+ case "utf16le":
317
+ return content.toString("utf16le");
318
+ case "utf16be": {
319
+ const swapped = Buffer.from(content.subarray(0, content.length - (content.length % 2)));
320
+ return swapped.swap16().toString("utf16le");
321
+ }
322
+ case "utf32le":
323
+ return decodeUtf32(content, true);
324
+ case "utf32be":
325
+ return decodeUtf32(content, false);
326
+ }
327
+ }
328
+ try {
329
+ return new TextDecoder("utf-8", { fatal: true }).decode(data);
330
+ }
331
+ catch {
332
+ const encoding = detectEncoding(data);
333
+ if (encoding && iconv.encodingExists(encoding)) {
334
+ return iconv.decode(data, encoding);
335
+ }
336
+ return data.toString("utf8");
337
+ }
338
+ }
339
+ function isTextMimeType(value) {
340
+ return (value.startsWith("text/") ||
341
+ value.endsWith("+json") ||
342
+ value.endsWith("+xml") ||
343
+ knownTextApplicationMimeTypes.has(value));
344
+ }
345
+ function sampleLooksBinary(data) {
346
+ if (data.length === 0 || detectBom(data))
347
+ return false;
348
+ let nonPrintable = 0;
349
+ for (const byte of data) {
350
+ if (byte === 0)
351
+ return true;
352
+ if (byte < 9 || (byte > 13 && byte < 32))
353
+ nonPrintable += 1;
354
+ }
355
+ return nonPrintable / data.length > 0.3;
356
+ }
357
+ function workspaceFileKind(filePath, sample) {
358
+ const extension = path.extname(filePath).toLowerCase();
359
+ if (extension === ".svg")
360
+ return "text";
361
+ if (visibleImageExtensions.has(extension))
362
+ return "image";
363
+ const mimeType = mime.getType(filePath);
364
+ if (mimeType?.startsWith("image/"))
365
+ return "image";
366
+ if (mimeType && isTextMimeType(mimeType))
367
+ return "text";
368
+ if (knownBinaryExtensions.has(extension))
369
+ return "binary";
370
+ if (knownTextExtensions.has(extension) ||
371
+ knownTextBasenames.has(path.basename(filePath))) {
372
+ return "text";
373
+ }
374
+ return sampleLooksBinary(sample) ? "binary" : "text";
375
+ }
376
+ function json(value) {
377
+ return JSON.stringify(value);
378
+ }
379
+ function errorMessage(error) {
380
+ return error instanceof Error ? error.message : String(error);
381
+ }
382
+ function record(value) {
383
+ return value !== null && typeof value === "object" && !Array.isArray(value)
384
+ ? value
385
+ : {};
386
+ }
387
+ function stringValue(value) {
388
+ return typeof value === "string" ? value : "";
389
+ }
390
+ function dashScopeApiKey() {
391
+ const apiKey = process.env.DASHSCOPE_API_KEY;
392
+ if (!apiKey) {
393
+ throw new Error("DashScope tools require DASHSCOPE_API_KEY");
394
+ }
395
+ return apiKey;
396
+ }
397
+ function dashScopeRoot() {
398
+ return (process.env.DASHSCOPE_API_BASE ||
399
+ process.env.DASHSCOPE_BASE_URL ||
400
+ "https://dashscope.aliyuncs.com")
401
+ .replace(/\/+$/, "")
402
+ .replace(/\/(?:api\/v1|compatible-mode\/v1)$/i, "");
403
+ }
404
+ function dashScopeUrl(protocol, pathname) {
405
+ const prefix = protocol === "api" ? "/api/v1" : "/compatible-mode/v1";
406
+ const normalized = pathname.startsWith("/") ? pathname : `/${pathname}`;
407
+ return `${dashScopeRoot()}${prefix}${normalized}`;
408
+ }
409
+ async function dashScopeFetch(protocol, pathname, init, label) {
410
+ const headers = new Headers(init.headers);
411
+ headers.set("authorization", `Bearer ${dashScopeApiKey()}`);
412
+ const maximumRetries = Math.max(0, Number(process.env.CODE_HARNESS_TOOL_MAX_RETRIES || 5));
413
+ for (let attempt = 0;; attempt += 1) {
414
+ const response = await fetch(dashScopeUrl(protocol, pathname), {
415
+ ...init,
416
+ headers,
417
+ });
418
+ if (response.ok)
419
+ return response;
420
+ const body = await response.text();
421
+ let detail = body;
422
+ try {
423
+ const payload = record(JSON.parse(body));
424
+ detail =
425
+ stringValue(payload.message) ||
426
+ stringValue(record(payload.error).message) ||
427
+ body;
428
+ }
429
+ catch {
430
+ // Keep the response text as the diagnostic.
431
+ }
432
+ const retryable = response.status === 429 || response.status >= 500;
433
+ if (!retryable || attempt >= maximumRetries) {
434
+ throw new Error(`${label} failed (${response.status} ${response.statusText}): ${detail.slice(0, 2000)}`);
435
+ }
436
+ const retryAfter = Number(response.headers.get("retry-after"));
437
+ const waitMs = Number.isFinite(retryAfter) && retryAfter > 0
438
+ ? Math.min(60_000, retryAfter * 1000)
439
+ : Math.min(30_000, 2_000 * 2 ** attempt);
440
+ await delay(waitMs, undefined, { signal: init.signal || undefined });
441
+ }
442
+ }
443
+ async function dashScopeJson(protocol, pathname, init, label) {
444
+ const response = await dashScopeFetch(protocol, pathname, init, label);
445
+ return record(await response.json());
446
+ }
447
+ function responseMessage(payload) {
448
+ return (stringValue(payload.message) ||
449
+ stringValue(record(payload.output).message) ||
450
+ stringValue(payload.code) ||
451
+ "unknown provider error");
452
+ }
453
+ function contentTypeForFile(filePath) {
454
+ const extension = path.extname(filePath).toLowerCase();
455
+ if ([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".mts", ".cts"].includes(extension)) {
456
+ return "text/javascript";
457
+ }
458
+ if (extension === ".css")
459
+ return "text/css";
460
+ if ([".html", ".htm"].includes(extension))
461
+ return "text/html";
462
+ if (extension === ".json")
463
+ return "application/json";
464
+ if (extension === ".svg")
465
+ return "image/svg+xml";
466
+ return mime.getType(filePath) || "text/plain";
467
+ }
468
+ function expandBraces(pattern) {
469
+ const match = pattern.match(/\{([^{}]+)\}/);
470
+ if (!match || match.index === undefined)
471
+ return [pattern];
472
+ const prefix = pattern.slice(0, match.index);
473
+ const suffix = pattern.slice(match.index + match[0].length);
474
+ return match[1]
475
+ .split(",")
476
+ .flatMap((choice) => expandBraces(`${prefix}${choice}${suffix}`));
477
+ }
478
+ function globRegex(pattern) {
479
+ let source = "^";
480
+ for (let index = 0; index < pattern.length; index += 1) {
481
+ const character = pattern[index];
482
+ if (character === "*" && pattern[index + 1] === "*") {
483
+ index += 1;
484
+ if (pattern[index + 1] === "/") {
485
+ index += 1;
486
+ source += "(?:.*/)?";
487
+ }
488
+ else {
489
+ source += ".*";
490
+ }
491
+ }
492
+ else if (character === "*") {
493
+ source += "[^/]*";
494
+ }
495
+ else if (character === "?") {
496
+ source += "[^/]";
497
+ }
498
+ else {
499
+ source += character.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
500
+ }
501
+ }
502
+ return new RegExp(`${source}$`);
503
+ }
504
+ function matchesGlob(filePath, pattern) {
505
+ const normalized = filePath.split(path.sep).join("/");
506
+ return expandBraces(pattern).some((candidate) => {
507
+ const matcher = globRegex(candidate);
508
+ return (matcher.test(normalized) ||
509
+ (!candidate.includes("/") &&
510
+ matcher.test(path.posix.basename(normalized))));
511
+ });
512
+ }
513
+ function packageNameIsSafe(value) {
514
+ if (!value || value.startsWith("-") || /\s/.test(value))
515
+ return false;
516
+ return /^(?:@[\w.-]+\/)?[\w.-]+(?:@(?:[\w*^<>=|.+-]+))?$/.test(value);
517
+ }
518
+ function valueAtPath(value, instancePath) {
519
+ if (!instancePath)
520
+ return value;
521
+ return instancePath
522
+ .split("/")
523
+ .slice(1)
524
+ .reduce((current, part) => {
525
+ const object = record(current);
526
+ return object[part.replace(/~1/g, "/").replace(/~0/g, "~")];
527
+ }, value);
528
+ }
529
+ function receivedType(value) {
530
+ if (value === undefined)
531
+ return "undefined";
532
+ if (value === null)
533
+ return "null";
534
+ if (Array.isArray(value))
535
+ return "array";
536
+ if (Number.isInteger(value))
537
+ return "integer";
538
+ return typeof value;
539
+ }
540
+ function requiredPropertyType(toolName, property, definitions) {
541
+ const definition = definitions.find((tool) => tool.function.name === toolName);
542
+ const properties = record(definition?.function.parameters.properties);
543
+ const schema = record(properties[property]);
544
+ return stringValue(schema.type) || "string";
545
+ }
546
+ function validationIssues(toolName, value, errors, definitions) {
547
+ const relevant = errors.filter((error) => error.keyword === "required" ||
548
+ error.keyword === "type" ||
549
+ error.keyword === "enum" ||
550
+ error.keyword === "anyOf");
551
+ const issues = [];
552
+ const seen = new Set();
553
+ for (const error of relevant) {
554
+ const missing = error.keyword === "required"
555
+ ? String(record(error.params).missingProperty || "")
556
+ : "";
557
+ const pathParts = error.instancePath
558
+ .split("/")
559
+ .filter(Boolean)
560
+ .map((part) => part.replace(/~1/g, "/").replace(/~0/g, "~"));
561
+ if (missing)
562
+ pathParts.push(missing);
563
+ const key = pathParts.join(".");
564
+ if (!key || seen.has(key))
565
+ continue;
566
+ seen.add(key);
567
+ if (toolName === "web_search" && key === "depth") {
568
+ issues.push({
569
+ code: "invalid_union",
570
+ unionErrors: ["1", "2", "3"].map((expected) => ({
571
+ issues: [
572
+ {
573
+ code: "invalid_literal",
574
+ expected,
575
+ path: ["depth"],
576
+ message: `Invalid literal value, expected "${expected}"`,
577
+ },
578
+ ],
579
+ name: "ZodError",
580
+ })),
581
+ path: ["depth"],
582
+ message: "Invalid input",
583
+ });
584
+ continue;
585
+ }
586
+ const actual = missing ? undefined : valueAtPath(value, error.instancePath);
587
+ const expected = error.keyword === "type"
588
+ ? String(record(error.params).type || "string")
589
+ : requiredPropertyType(toolName, key, definitions);
590
+ const received = receivedType(actual);
591
+ issues.push({
592
+ code: "invalid_type",
593
+ expected,
594
+ received,
595
+ path: pathParts,
596
+ message: received === "undefined"
597
+ ? "Required"
598
+ : `Expected ${expected}, received ${received}`,
599
+ });
600
+ // The captured fetch_media validator reports its required query first and
601
+ // stops there, even when an unrelated tool-shaped object was supplied.
602
+ if (toolName === "fetch_media" && key === "query")
603
+ break;
604
+ }
605
+ return issues;
606
+ }
607
+ async function responseBytes(response, limit = 4 * 1024 * 1024) {
608
+ const bytes = Buffer.from(await response.arrayBuffer());
609
+ if (bytes.length > limit) {
610
+ throw new Error(`download exceeds ${limit} bytes`);
611
+ }
612
+ return bytes;
613
+ }
614
+ const defaultMediaCacheTtlMs = 24 * 60 * 60 * 1000;
615
+ const mediaRequestInflight = new Map();
616
+ let pexelsQuota;
617
+ class MediaProviderHttpError extends Error {
618
+ provider;
619
+ status;
620
+ constructor(provider, status, statusText) {
621
+ super(`${provider} failed (${status} ${statusText})`);
622
+ this.provider = provider;
623
+ this.status = status;
624
+ }
625
+ }
626
+ function mediaCacheTtlMs() {
627
+ const configured = Number(process.env.CODE_HARNESS_MEDIA_CACHE_TTL_MS);
628
+ return Number.isFinite(configured) && configured >= 0
629
+ ? configured
630
+ : defaultMediaCacheTtlMs;
631
+ }
632
+ function mediaCacheDirectory() {
633
+ return (process.env.CODE_HARNESS_MEDIA_CACHE_DIR ||
634
+ path.join(tmpdir(), "code-harness", "fetch-media-cache-v1"));
635
+ }
636
+ async function readMediaCache(filePath, ttlMs) {
637
+ if (ttlMs <= 0)
638
+ return undefined;
639
+ try {
640
+ const cached = record(JSON.parse(await readFile(filePath, "utf8")));
641
+ const cachedAt = Number(cached.cachedAt);
642
+ const payload = record(cached.payload);
643
+ if (Number.isFinite(cachedAt) &&
644
+ Date.now() - cachedAt < ttlMs &&
645
+ Object.keys(payload).length > 0) {
646
+ return payload;
647
+ }
648
+ }
649
+ catch {
650
+ // Missing, expired, or partially-written cache entries are normal misses.
651
+ }
652
+ return undefined;
653
+ }
654
+ async function cachedMediaRequest(options) {
655
+ const ttlMs = mediaCacheTtlMs();
656
+ const digest = createHash("sha256")
657
+ .update(`${options.provider}\n${options.cacheIdentity || options.url.href}`)
658
+ .digest("hex");
659
+ const cacheDirectory = mediaCacheDirectory();
660
+ const cacheFile = path.join(cacheDirectory, `${digest}.json`);
661
+ const cached = await readMediaCache(cacheFile, ttlMs);
662
+ if (cached)
663
+ return cached;
664
+ const existing = ttlMs > 0 ? mediaRequestInflight.get(cacheFile) : undefined;
665
+ if (existing)
666
+ return existing;
667
+ const request = (async () => {
668
+ options.beforeNetwork?.();
669
+ const response = await fetch(options.url, options.init);
670
+ options.onResponse?.(response);
671
+ if (!response.ok) {
672
+ throw new MediaProviderHttpError(options.provider, response.status, response.statusText);
673
+ }
674
+ const payload = record(await response.json());
675
+ if (Object.keys(payload).length === 0) {
676
+ throw new Error(`${options.provider} returned an empty response`);
677
+ }
678
+ if (ttlMs > 0) {
679
+ try {
680
+ mkdirSync(cacheDirectory, { recursive: true });
681
+ await writeFile(cacheFile, JSON.stringify({ cachedAt: Date.now(), payload }), "utf8");
682
+ }
683
+ catch {
684
+ // A cache write failure must not turn a successful provider call into
685
+ // a tool failure.
686
+ }
687
+ }
688
+ return payload;
689
+ })();
690
+ if (ttlMs > 0)
691
+ mediaRequestInflight.set(cacheFile, request);
692
+ try {
693
+ return await request;
694
+ }
695
+ finally {
696
+ if (ttlMs > 0)
697
+ mediaRequestInflight.delete(cacheFile);
698
+ }
699
+ }
700
+ function interleaveMedia(left, right) {
701
+ return Array.from({ length: Math.max(left.length, right.length) }, (_, index) => [left[index], right[index]])
702
+ .flat()
703
+ .filter((item) => item !== undefined);
704
+ }
705
+ function normalizeMediaQuery(value) {
706
+ return value.trim().replace(/\s+/g, " ").toLowerCase();
707
+ }
708
+ export class CodeToolExecutor {
709
+ config;
710
+ workspace;
711
+ workspaceReal;
712
+ validators = new Map();
713
+ definitions;
714
+ seedFileRank;
715
+ pendingVisualContent = new Map();
716
+ askUserHandler;
717
+ signal;
718
+ workspaceContext;
719
+ pageReader;
720
+ ajv = new Ajv({ allErrors: true, strict: false });
721
+ // Node's readdir order differs across the host filesystems used by local
722
+ // runs and Docker. Keep non-seed files stable in the order in which this
723
+ // executor first writes or observes them.
724
+ listedFileOrder = new Map();
725
+ nextListedFileOrder = 0;
726
+ generatedImageCount = 0;
727
+ // ask_user requires a handler whenever a mode exposes it. Arena mode can use
728
+ // either an interactive front-end or a configured remote User Agent.
729
+ askUserEnabled;
730
+ constructor(config, options = {}) {
731
+ this.config = config;
732
+ this.workspace = path.resolve(config.workspace);
733
+ mkdirSync(this.workspace, { recursive: true });
734
+ this.workspaceReal = realpathSync(this.workspace);
735
+ this.askUserHandler = options.askUserHandler;
736
+ this.askUserEnabled = Boolean(options.askUserHandler);
737
+ this.signal = options.signal;
738
+ this.workspaceContext = options.workspaceContext;
739
+ this.pageReader = new FetchPageReader({ signal: options.signal });
740
+ const disallowed = new Set(config.disallowedTools || []);
741
+ this.definitions = toolDefinitions(this.workspaceContext, config.mode).filter((definition) => !disallowed.has(definition.function.name));
742
+ this.seedFileRank = capturedSeedFileRank(config.mode);
743
+ addFormats(this.ajv);
744
+ for (const definition of this.definitions) {
745
+ this.validators.set(definition.function.name, this.ajv.compile(definition.function.parameters));
746
+ }
747
+ }
748
+ // The tool schemas offered to the model for this session. Identical to the
749
+ // canonical trace set, adjusted for the selected workspace runtime.
750
+ availableTools() {
751
+ return structuredClone(this.definitions);
752
+ }
753
+ unavailableToolMessage(name) {
754
+ const names = this.definitions.map((tool) => tool.function.name);
755
+ return `Model tried to call unavailable tool '${name}'. Available tools: ${names.join(", ")}.`;
756
+ }
757
+ async execute(call, _context) {
758
+ const callId = String(call.id || "");
759
+ const name = String(call.function?.name || "");
760
+ this.pendingVisualContent.delete(callId);
761
+ try {
762
+ // 监听器不会补发已发生的 abort:已取消的 signal 必须在产生任何副作用
763
+ // 之前短路,否则命令会照常 spawn 直到自然结束。
764
+ this.signal?.throwIfAborted();
765
+ const argumentsValue = JSON.parse(typeof call.function?.arguments === "string"
766
+ ? call.function.arguments
767
+ : "{}");
768
+ // The captured provider advertises these fields as required, but its
769
+ // runtime still applies the documented defaults when a model omits
770
+ // them. Preserve the byte-identical outgoing schema while matching the
771
+ // observed execution behavior.
772
+ if (name === "read_file") {
773
+ argumentsValue.offset ??= 1;
774
+ argumentsValue.limit ??= 2000;
775
+ }
776
+ else if (name === "grep_files") {
777
+ argumentsValue.path ??= "";
778
+ argumentsValue.glob ??= "";
779
+ argumentsValue.output_mode ??= "content";
780
+ argumentsValue["-i"] ??= false;
781
+ argumentsValue["-B"] ??= 0;
782
+ argumentsValue["-A"] ??= 0;
783
+ argumentsValue["-C"] ??= 0;
784
+ argumentsValue.context ??= 0;
785
+ }
786
+ const validate = this.validators.get(name);
787
+ if (!validate) {
788
+ return {
789
+ role: "tool",
790
+ tool_call_id: callId,
791
+ content: this.unavailableToolMessage(name),
792
+ };
793
+ }
794
+ if (!validate(argumentsValue)) {
795
+ const issues = validationIssues(name, argumentsValue, validate.errors || [], this.definitions);
796
+ return {
797
+ role: "tool",
798
+ tool_call_id: callId,
799
+ content: `Invalid input for tool ${name}: Type validation failed: ` +
800
+ `Value: ${JSON.stringify(argumentsValue)}.\n` +
801
+ `Error message: ${JSON.stringify(issues, null, 2)}`,
802
+ };
803
+ }
804
+ const result = await this.dispatch(name, argumentsValue, callId);
805
+ return {
806
+ role: "tool",
807
+ tool_call_id: callId,
808
+ content: typeof result === "string" ? result : json(result),
809
+ };
810
+ }
811
+ catch (error) {
812
+ return {
813
+ role: "tool",
814
+ tool_call_id: callId,
815
+ content: json({
816
+ status: "error",
817
+ message: errorMessage(error),
818
+ }),
819
+ };
820
+ }
821
+ }
822
+ takeVisualMessage(toolCallIds) {
823
+ // 生产契约:同一组 tool call 的全部图片汇入一条 user message,
824
+ // 由一个 arena-system-message 文本 part 领头。
825
+ const images = [];
826
+ for (const toolCallId of toolCallIds) {
827
+ const parts = this.pendingVisualContent.get(toolCallId);
828
+ this.pendingVisualContent.delete(toolCallId);
829
+ if (parts)
830
+ images.push(...parts);
831
+ }
832
+ if (!images.length)
833
+ return undefined;
834
+ return {
835
+ role: "user",
836
+ content: [{ text: visualIntroText }, ...images],
837
+ };
838
+ }
839
+ async dispatch(name, values, _toolCallId) {
840
+ switch (name) {
841
+ case "read_file":
842
+ return this.readCodeFile(String(values.path), Number(values.offset), Number(values.limit));
843
+ case "create_file":
844
+ return this.createCodeFile(String(values.path), String(values.content));
845
+ case "edit_file":
846
+ return this.editCodeFile(String(values.path), String(values.context), String(values.replacement));
847
+ case "delete_file":
848
+ return this.deleteCodeFile(String(values.path));
849
+ case "list_files":
850
+ return this.listCodeFiles(String(values.path));
851
+ case "grep_files":
852
+ return this.grepCodeFiles(values);
853
+ case "glob_files":
854
+ return this.globCodeFiles(String(values.pattern), String(values.path));
855
+ case "install_npm_packages":
856
+ return this.installNpmPackages(values.packages);
857
+ case "build_project":
858
+ return this.buildProject();
859
+ case "bash":
860
+ return this.fullStackBash(String(values.command), String(values.workdir));
861
+ case "build_and_start":
862
+ return this.buildAndStart();
863
+ case "web_search":
864
+ return this.webSearch(String(values.query), String(values.depth));
865
+ case "web_fetch":
866
+ return this.webFetch(String(values.url), String(values.format));
867
+ case "fetch_media":
868
+ return this.fetchMedia(String(values.query), String(values.media_type), Number(values.count), String(values.orientation), String(values.size), String(values.locale));
869
+ case "generate_image":
870
+ return this.generateImage(String(values.file_path), String(values.prompt));
871
+ default:
872
+ return this.unavailableToolMessage(name);
873
+ }
874
+ }
875
+ lexicalPath(raw) {
876
+ if (!raw)
877
+ throw new Error("path must not be empty");
878
+ let candidate;
879
+ if (!this.workspaceContext && (raw === "/home/user" || raw === "~")) {
880
+ candidate = this.workspace;
881
+ }
882
+ else if (!this.workspaceContext && raw.startsWith("/home/user/")) {
883
+ candidate = path.join(this.workspace, raw.slice("/home/user/".length));
884
+ }
885
+ else if (!this.workspaceContext && raw.startsWith("~/")) {
886
+ candidate = path.join(this.workspace, raw.slice(2));
887
+ }
888
+ else {
889
+ const defaultCwd = this.workspaceContext
890
+ ? path.resolve(this.workspaceContext.cwd)
891
+ : this.workspace;
892
+ candidate = path.isAbsolute(raw) ? raw : path.join(defaultCwd, raw);
893
+ }
894
+ const absolute = path.resolve(candidate);
895
+ const lexicalRelative = path.relative(this.workspace, absolute);
896
+ if (lexicalRelative.startsWith("..") || path.isAbsolute(lexicalRelative)) {
897
+ throw new Error(`path is outside the workspace: ${raw}`);
898
+ }
899
+ return absolute;
900
+ }
901
+ async confinedPath(raw) {
902
+ const absolute = this.lexicalPath(raw);
903
+ let ancestor = absolute;
904
+ while (!existsSync(ancestor)) {
905
+ const parent = path.dirname(ancestor);
906
+ if (parent === ancestor)
907
+ break;
908
+ ancestor = parent;
909
+ }
910
+ const ancestorReal = await realpath(ancestor);
911
+ const resolved = path.resolve(ancestorReal, path.relative(ancestor, absolute));
912
+ const realRelative = path.relative(this.workspaceReal, resolved);
913
+ if (realRelative.startsWith("..") || path.isAbsolute(realRelative)) {
914
+ throw new Error(`path is outside the workspace: ${raw}`);
915
+ }
916
+ return resolved;
917
+ }
918
+ relativePath(target) {
919
+ return path.relative(this.workspaceReal, target).split(path.sep).join("/");
920
+ }
921
+ rememberListedFile(filePath) {
922
+ if (this.seedFileRank.has(filePath) ||
923
+ this.listedFileOrder.has(filePath)) {
924
+ return;
925
+ }
926
+ this.listedFileOrder.set(filePath, this.nextListedFileOrder);
927
+ this.nextListedFileOrder += 1;
928
+ }
929
+ forgetListedPath(filePath) {
930
+ const prefix = `${filePath}/`;
931
+ for (const knownPath of this.listedFileOrder.keys()) {
932
+ if (knownPath === filePath || knownPath.startsWith(prefix)) {
933
+ this.listedFileOrder.delete(knownPath);
934
+ }
935
+ }
936
+ }
937
+ orderListedFiles(files) {
938
+ const relativePaths = files.map((filePath) => this.relativePath(filePath));
939
+ // Files produced outside create_file (for example by npm or a build) have
940
+ // no observable insertion event. Establish their first-seen order
941
+ // deterministically, then retain it for the rest of the session.
942
+ const firstSeen = relativePaths
943
+ .filter((filePath) => !this.seedFileRank.has(filePath) &&
944
+ !this.listedFileOrder.has(filePath))
945
+ .sort((left, right) => left.localeCompare(right));
946
+ for (const filePath of firstSeen)
947
+ this.rememberListedFile(filePath);
948
+ return relativePaths.sort((left, right) => {
949
+ const leftSeedRank = this.seedFileRank.get(left);
950
+ const rightSeedRank = this.seedFileRank.get(right);
951
+ if (leftSeedRank !== undefined && rightSeedRank !== undefined) {
952
+ return leftSeedRank - rightSeedRank;
953
+ }
954
+ if (leftSeedRank !== undefined)
955
+ return -1;
956
+ if (rightSeedRank !== undefined)
957
+ return 1;
958
+ return (this.listedFileOrder.get(left) - this.listedFileOrder.get(right));
959
+ });
960
+ }
961
+ async existsWithExactCase(rawPath) {
962
+ const target = this.lexicalPath(rawPath);
963
+ const relative = path.relative(this.workspace, target);
964
+ if (!relative)
965
+ return true;
966
+ let current = this.workspaceReal;
967
+ for (const component of relative.split(path.sep)) {
968
+ let entries;
969
+ try {
970
+ entries = await readdir(current);
971
+ }
972
+ catch {
973
+ return false;
974
+ }
975
+ if (!entries.includes(component))
976
+ return false;
977
+ current = path.join(current, component);
978
+ }
979
+ return true;
980
+ }
981
+ async walkFiles(searchRoot) {
982
+ const rootInfo = await stat(searchRoot);
983
+ if (rootInfo.isFile())
984
+ return [searchRoot];
985
+ if (!rootInfo.isDirectory())
986
+ return [];
987
+ const files = [];
988
+ const visit = async (directory) => {
989
+ let entries;
990
+ try {
991
+ entries = await readdir(directory, { withFileTypes: true });
992
+ }
993
+ catch {
994
+ return;
995
+ }
996
+ for (const entry of entries) {
997
+ if (directory === this.workspaceReal &&
998
+ entry.isDirectory() &&
999
+ ignoredProjectDirectories.has(entry.name)) {
1000
+ continue;
1001
+ }
1002
+ const target = path.join(directory, entry.name);
1003
+ if (entry.isDirectory()) {
1004
+ await visit(target);
1005
+ }
1006
+ else if (entry.isFile()) {
1007
+ files.push(target);
1008
+ }
1009
+ }
1010
+ };
1011
+ await visit(searchRoot);
1012
+ return files;
1013
+ }
1014
+ async readCodeFile(rawPath, offset, limit) {
1015
+ const target = await this.confinedPath(rawPath);
1016
+ let data;
1017
+ try {
1018
+ if (!(await this.existsWithExactCase(rawPath)))
1019
+ throw new Error("missing");
1020
+ data = await readFile(target);
1021
+ }
1022
+ catch {
1023
+ return {
1024
+ status: "error",
1025
+ message: `File "${rawPath}" does not exist`,
1026
+ };
1027
+ }
1028
+ if (workspaceFileKind(target, data.subarray(0, 4096)) !== "text") {
1029
+ return {
1030
+ status: "error",
1031
+ message: `File "${rawPath}" is not a supported text file`,
1032
+ };
1033
+ }
1034
+ const lines = decodeText(data).split("\n");
1035
+ const start = Math.max(1, Math.trunc(offset));
1036
+ const end = Math.min(lines.length, start + Math.max(1, Math.trunc(limit)) - 1);
1037
+ const numbered = start > lines.length
1038
+ ? ""
1039
+ : lines
1040
+ .slice(start - 1, end)
1041
+ .map((line, index) => `${start + index}: ${line}`)
1042
+ .join("\n");
1043
+ const footer = start === 1 && end === lines.length
1044
+ ? `(End of file - ${lines.length} lines)`
1045
+ : `(Showing lines ${start}-${end} of ${lines.length}. Use offset=${end + 1} to continue.)`;
1046
+ return {
1047
+ status: "success",
1048
+ file: {
1049
+ path: this.relativePath(target),
1050
+ content: `${numbered}\n\n${footer}`,
1051
+ contentType: contentTypeForFile(target),
1052
+ },
1053
+ totalLines: lines.length,
1054
+ };
1055
+ }
1056
+ async typeErrors() {
1057
+ if (!existsSync(path.join(this.workspaceReal, "tsconfig.json")))
1058
+ return "";
1059
+ const executable = path.join(this.workspaceReal, "node_modules", ".bin", process.platform === "win32" ? "tsc.cmd" : "tsc");
1060
+ const localCompiler = existsSync(executable);
1061
+ const npmCli = process.env.CODE_HARNESS_NPM_CLI_JS;
1062
+ const result = localCompiler
1063
+ ? await this.runProcess(executable, ["--noEmit", "--pretty", "false"], 30_000)
1064
+ : await this.runProcess(npmCli ? process.execPath : "npx", [
1065
+ ...(npmCli ? [npmCli, "exec", "--"] : []),
1066
+ "tsc",
1067
+ "--noEmit",
1068
+ "--pretty",
1069
+ "false",
1070
+ ], 30_000, { npm_config_cache: path.join(this.workspaceReal, ".npm") });
1071
+ if (result.exitCode === 0)
1072
+ return "";
1073
+ const output = [result.stdout.trim(), result.stderr.trim()]
1074
+ .filter(Boolean)
1075
+ .join("\n");
1076
+ const virtualized = this.virtualizeCodeOutput(output);
1077
+ return virtualized.length <= lintOutputLimit
1078
+ ? virtualized
1079
+ : `${virtualized.slice(0, lintOutputLimit)}\n...truncated`;
1080
+ }
1081
+ async runNpmProcess(args, timeoutMs) {
1082
+ const npmCli = process.env.CODE_HARNESS_NPM_CLI_JS;
1083
+ return npmCli
1084
+ ? this.runProcess(process.execPath, [npmCli, ...args], timeoutMs)
1085
+ : this.runProcess("npm", args, timeoutMs);
1086
+ }
1087
+ virtualizeCodeOutput(value) {
1088
+ if (this.workspaceContext || this.config.mode === "full-stack")
1089
+ return value;
1090
+ let virtualized = value;
1091
+ const workspaceRoots = [...new Set([this.workspace, this.workspaceReal])]
1092
+ .filter((root) => root !== path.parse(root).root)
1093
+ .sort((left, right) => right.length - left.length);
1094
+ for (const root of workspaceRoots) {
1095
+ virtualized = virtualized.replaceAll(root, "/home/user/sandbox");
1096
+ }
1097
+ return virtualized;
1098
+ }
1099
+ async mutationResult(verb, rawPath) {
1100
+ const lintError = await this.typeErrors();
1101
+ if (lintError) {
1102
+ return {
1103
+ status: "success",
1104
+ message: `${verb} ${rawPath}. Type errors found.`,
1105
+ lint_error: lintError,
1106
+ };
1107
+ }
1108
+ return { status: "success", message: `${verb} ${rawPath}.` };
1109
+ }
1110
+ async createCodeFile(rawPath, content) {
1111
+ const target = await this.confinedPath(rawPath);
1112
+ const existed = existsSync(target);
1113
+ mkdirSync(path.dirname(target), { recursive: true });
1114
+ await writeFile(target, content, "utf8");
1115
+ if (!existed)
1116
+ this.rememberListedFile(this.relativePath(target));
1117
+ return this.mutationResult("Created", rawPath);
1118
+ }
1119
+ async editCodeFile(rawPath, context, replacement) {
1120
+ const target = await this.confinedPath(rawPath);
1121
+ let source;
1122
+ try {
1123
+ if (!(await this.existsWithExactCase(rawPath)))
1124
+ throw new Error("missing");
1125
+ source = await readFile(target, "utf8");
1126
+ }
1127
+ catch {
1128
+ return {
1129
+ status: "error",
1130
+ message: `File "${rawPath}" does not exist`,
1131
+ };
1132
+ }
1133
+ if (!source.includes(context)) {
1134
+ return {
1135
+ status: "error",
1136
+ message: "Context not found. Read the file to verify the text exists.",
1137
+ };
1138
+ }
1139
+ await writeFile(target, source.replace(context, replacement), "utf8");
1140
+ return this.mutationResult("Edited", rawPath);
1141
+ }
1142
+ async deleteCodeFile(rawPath) {
1143
+ const target = await this.confinedPath(rawPath);
1144
+ await rm(target, { force: true, recursive: true });
1145
+ this.forgetListedPath(this.relativePath(target));
1146
+ return { status: "success" };
1147
+ }
1148
+ async listCodeFiles(rawPath) {
1149
+ const target = await this.confinedPath(rawPath || ".");
1150
+ if (!(await this.existsWithExactCase(rawPath || ".")) ||
1151
+ !existsSync(target)) {
1152
+ return { status: "success", files: [] };
1153
+ }
1154
+ const files = this.orderListedFiles(await this.walkFiles(target));
1155
+ return {
1156
+ status: "success",
1157
+ files: files.map((filePath) => ({ path: filePath })),
1158
+ };
1159
+ }
1160
+ async grepCodeFiles(values) {
1161
+ const target = await this.confinedPath(String(values.path || "."));
1162
+ const targetInfo = await stat(target);
1163
+ const files = await this.walkFiles(target);
1164
+ const glob = String(values.glob || "");
1165
+ const flags = Boolean(values["-i"]) ? "i" : "";
1166
+ let expression;
1167
+ try {
1168
+ expression = new RegExp(String(values.pattern), flags);
1169
+ }
1170
+ catch (error) {
1171
+ throw new Error(`Invalid regular expression: ${errorMessage(error)}`);
1172
+ }
1173
+ const symmetric = Math.max(Number(values.context || 0), Number(values["-C"] || 0));
1174
+ const before = symmetric || Number(values["-B"] || 0);
1175
+ const after = symmetric || Number(values["-A"] || 0);
1176
+ const matches = [];
1177
+ const matchedFiles = [];
1178
+ const counts = [];
1179
+ let totalMatches = 0;
1180
+ for (const filePath of files) {
1181
+ const relative = this.relativePath(filePath);
1182
+ const relativeToSearch = targetInfo.isFile()
1183
+ ? path.basename(filePath)
1184
+ : path.relative(target, filePath).split(path.sep).join("/");
1185
+ if (glob && !matchesGlob(relativeToSearch, glob))
1186
+ continue;
1187
+ let content;
1188
+ try {
1189
+ const data = await readFile(filePath);
1190
+ if (workspaceFileKind(filePath, data.subarray(0, 4096)) !== "text") {
1191
+ continue;
1192
+ }
1193
+ content = decodeText(data);
1194
+ }
1195
+ catch {
1196
+ continue;
1197
+ }
1198
+ const lines = content.split("\n");
1199
+ let fileCount = 0;
1200
+ for (let index = 0; index < lines.length; index += 1) {
1201
+ if (!expression.test(lines[index]))
1202
+ continue;
1203
+ fileCount += 1;
1204
+ totalMatches += 1;
1205
+ if (matches.length < maximumSearchResults) {
1206
+ const match = {
1207
+ path: relative,
1208
+ lineNumber: index + 1,
1209
+ lineContent: lines[index],
1210
+ };
1211
+ if (before > 0) {
1212
+ match.contextBefore = lines.slice(Math.max(0, index - before), index);
1213
+ }
1214
+ if (after > 0) {
1215
+ match.contextAfter = lines.slice(index + 1, index + 1 + after);
1216
+ }
1217
+ matches.push(match);
1218
+ }
1219
+ }
1220
+ if (fileCount > 0) {
1221
+ if (matchedFiles.length < maximumSearchResults)
1222
+ matchedFiles.push(relative);
1223
+ if (counts.length < maximumSearchResults) {
1224
+ counts.push({ path: relative, count: fileCount });
1225
+ }
1226
+ }
1227
+ }
1228
+ const mode = String(values.output_mode || "content");
1229
+ if (mode === "files_with_matches") {
1230
+ return {
1231
+ status: "success",
1232
+ mode,
1233
+ files: matchedFiles,
1234
+ truncated: counts.length >= maximumSearchResults,
1235
+ };
1236
+ }
1237
+ if (mode === "count") {
1238
+ return {
1239
+ status: "success",
1240
+ mode,
1241
+ counts,
1242
+ totalMatches,
1243
+ truncated: counts.length >= maximumSearchResults,
1244
+ };
1245
+ }
1246
+ return {
1247
+ status: "success",
1248
+ mode: "content",
1249
+ matches,
1250
+ truncated: totalMatches > maximumSearchResults,
1251
+ };
1252
+ }
1253
+ async globCodeFiles(pattern, rawPath) {
1254
+ const target = await this.confinedPath(rawPath || ".");
1255
+ const files = await this.walkFiles(target);
1256
+ const allPaths = files
1257
+ .filter((filePath) => matchesGlob(path.relative(target, filePath).split(path.sep).join("/"), pattern))
1258
+ .map((filePath) => this.relativePath(filePath));
1259
+ return {
1260
+ status: "success",
1261
+ paths: allPaths.slice(0, maximumSearchResults),
1262
+ truncated: allPaths.length > maximumSearchResults,
1263
+ };
1264
+ }
1265
+ async runProcess(command, args, timeoutMs, envOverrides = {}) {
1266
+ return new Promise((resolve, reject) => {
1267
+ const child = spawn(command, args, {
1268
+ cwd: this.workspaceReal,
1269
+ env: { ...process.env, ...envOverrides },
1270
+ stdio: ["ignore", "pipe", "pipe"],
1271
+ detached: process.platform !== "win32",
1272
+ });
1273
+ let stdout = "";
1274
+ let stderr = "";
1275
+ let settled = false;
1276
+ const kill = () => {
1277
+ try {
1278
+ if (process.platform !== "win32" && child.pid) {
1279
+ process.kill(-child.pid, "SIGKILL");
1280
+ }
1281
+ else {
1282
+ child.kill("SIGKILL");
1283
+ }
1284
+ }
1285
+ catch (error) {
1286
+ if (error.code !== "ESRCH")
1287
+ throw error;
1288
+ }
1289
+ };
1290
+ const finish = (exitCode) => {
1291
+ if (settled)
1292
+ return;
1293
+ settled = true;
1294
+ clearTimeout(timeout);
1295
+ this.signal?.removeEventListener("abort", abort);
1296
+ if (this.signal?.aborted) {
1297
+ reject(this.signal.reason || new Error("operation cancelled"));
1298
+ return;
1299
+ }
1300
+ resolve({ exitCode, stdout, stderr });
1301
+ };
1302
+ const abort = () => {
1303
+ kill();
1304
+ finish(null);
1305
+ };
1306
+ const timeout = setTimeout(() => {
1307
+ kill();
1308
+ stderr += `${stderr ? "\n" : ""}Process timed out`;
1309
+ finish(null);
1310
+ }, timeoutMs);
1311
+ this.signal?.addEventListener("abort", abort, { once: true });
1312
+ child.stdout.on("data", (chunk) => {
1313
+ stdout += chunk.toString("utf8");
1314
+ });
1315
+ child.stderr.on("data", (chunk) => {
1316
+ stderr += chunk.toString("utf8");
1317
+ });
1318
+ child.once("error", (error) => {
1319
+ if (settled)
1320
+ return;
1321
+ settled = true;
1322
+ clearTimeout(timeout);
1323
+ this.signal?.removeEventListener("abort", abort);
1324
+ reject(error);
1325
+ });
1326
+ child.once("close", finish);
1327
+ });
1328
+ }
1329
+ async installNpmPackages(packages) {
1330
+ const invalid = packages.find((packageName) => !packageNameIsSafe(packageName));
1331
+ if (invalid) {
1332
+ return {
1333
+ status: "error",
1334
+ message: `Invalid package name: ${invalid}`,
1335
+ };
1336
+ }
1337
+ const args = ["install", "-S"];
1338
+ if (this.config.mode === "react")
1339
+ args.push("--package-lock=false");
1340
+ args.push(...packages);
1341
+ const result = await this.runNpmProcess(args, 5 * 60_000);
1342
+ if (result.exitCode !== 0) {
1343
+ return {
1344
+ status: "error",
1345
+ message: "Failed to install npm packages",
1346
+ stdout: this.virtualizeCodeOutput(result.stdout),
1347
+ stderr: this.virtualizeCodeOutput(result.stderr),
1348
+ };
1349
+ }
1350
+ return {
1351
+ status: "success",
1352
+ stdout: this.virtualizeCodeOutput(result.stdout.trim()),
1353
+ stderr: this.virtualizeCodeOutput(result.stderr.trim()),
1354
+ };
1355
+ }
1356
+ async buildProject() {
1357
+ const result = await this.runNpmProcess(["run", "build"], 5 * 60_000);
1358
+ if (result.exitCode !== 0) {
1359
+ return {
1360
+ status: "error",
1361
+ message: "Failed to build project",
1362
+ stdout: this.virtualizeCodeOutput(result.stdout),
1363
+ stderr: this.virtualizeCodeOutput(result.stderr),
1364
+ };
1365
+ }
1366
+ return {
1367
+ status: "success",
1368
+ stdout: this.virtualizeCodeOutput(result.stdout),
1369
+ stderr: this.virtualizeCodeOutput(result.stderr),
1370
+ };
1371
+ }
1372
+ virtualizeBashOutput(value) {
1373
+ if (this.workspaceContext || this.config.mode === "full-stack")
1374
+ return value;
1375
+ let virtualized = value;
1376
+ const workspaceRoots = [...new Set([this.workspace, this.workspaceReal])]
1377
+ .filter((root) => root !== path.parse(root).root)
1378
+ .sort((left, right) => right.length - left.length);
1379
+ for (const root of workspaceRoots) {
1380
+ virtualized = virtualized.replaceAll(root, "/home/user");
1381
+ }
1382
+ return virtualized;
1383
+ }
1384
+ async fullStackBash(command, workdir) {
1385
+ // The captured Full Stack sandbox exposes PostgreSQL and psql from the
1386
+ // beginning of the session, not only after build_and_start. Materialize
1387
+ // that baseline before every bash call so early drizzle/psql commands see
1388
+ // the same environment. ensureFullStackDatabase is a cheap port check
1389
+ // after the first call.
1390
+ const databaseError = await this.ensureFullStackDatabase();
1391
+ if (databaseError) {
1392
+ return {
1393
+ status: "error",
1394
+ message: `Fullstack sandbox setup failed: ${databaseError}`,
1395
+ stdout: "",
1396
+ stderr: databaseError,
1397
+ };
1398
+ }
1399
+ await this.preparePostgresCliShim();
1400
+ const configuredTimeout = Number(process.env.CODE_HARNESS_BASH_TIMEOUT_SECONDS || 300);
1401
+ const timeoutSeconds = Number.isFinite(configuredTimeout) && configuredTimeout > 0
1402
+ ? configuredTimeout
1403
+ : 300;
1404
+ const result = await this.bash(command, workdir.trim() || ".", timeoutSeconds);
1405
+ const exitCode = result.exit_code;
1406
+ const stdout = String(result.stdout || "");
1407
+ const stderr = String(result.stderr || "");
1408
+ if (exitCode !== 0) {
1409
+ return {
1410
+ status: "error",
1411
+ message: `Command exited with code ${exitCode ?? -1}`,
1412
+ stdout,
1413
+ stderr,
1414
+ };
1415
+ }
1416
+ return { status: "success", stdout, stderr };
1417
+ }
1418
+ async portIsOpen(hostname, port, timeoutMs = 500) {
1419
+ return new Promise((resolve) => {
1420
+ const socket = createConnection({ host: hostname, port });
1421
+ const finish = (open) => {
1422
+ socket.removeAllListeners();
1423
+ socket.destroy();
1424
+ resolve(open);
1425
+ };
1426
+ socket.setTimeout(timeoutMs);
1427
+ socket.once("connect", () => finish(true));
1428
+ socket.once("error", () => finish(false));
1429
+ socket.once("timeout", () => finish(false));
1430
+ });
1431
+ }
1432
+ async fullStackDatabaseUrl() {
1433
+ const envPath = path.join(this.workspaceReal, ".env");
1434
+ let configured = process.env.DATABASE_URL || "";
1435
+ try {
1436
+ const source = await readFile(envPath, "utf8");
1437
+ const match = source.match(/^\s*DATABASE_URL\s*=\s*([^\r\n]+)\s*$/m);
1438
+ if (match?.[1])
1439
+ configured = match[1].replace(/^['"]|['"]$/g, "");
1440
+ }
1441
+ catch (error) {
1442
+ if (error.code !== "ENOENT")
1443
+ throw error;
1444
+ }
1445
+ return new URL(configured || "postgresql://postgres:postgres@127.0.0.1:5432/app_db");
1446
+ }
1447
+ async ensureFullStackDatabase() {
1448
+ const databaseUrl = await this.fullStackDatabaseUrl();
1449
+ const hostname = databaseUrl.hostname;
1450
+ const port = Number(databaseUrl.port || 5432);
1451
+ const username = decodeURIComponent(databaseUrl.username || "postgres");
1452
+ const password = decodeURIComponent(databaseUrl.password || "postgres");
1453
+ const database = databaseUrl.pathname.replace(/^\//, "") || "app_db";
1454
+ if (await this.portIsOpen(hostname, port))
1455
+ return undefined;
1456
+ if (!["127.0.0.1", "localhost", "::1"].includes(hostname)) {
1457
+ return `PostgreSQL is not reachable at ${hostname}:${port}`;
1458
+ }
1459
+ let dockerInfo;
1460
+ try {
1461
+ dockerInfo = await this.runProcess("docker", ["info"], 15_000);
1462
+ }
1463
+ catch (error) {
1464
+ return `PostgreSQL is not running and Docker is unavailable: ${errorMessage(error)}`;
1465
+ }
1466
+ if (dockerInfo.exitCode !== 0) {
1467
+ return (dockerInfo.stderr.trim() ||
1468
+ "PostgreSQL is not running and Docker is unavailable");
1469
+ }
1470
+ const containerName = this.postgresContainerName();
1471
+ const inspection = await this.runProcess("docker", ["inspect", "-f", "{{.State.Running}}", containerName], 15_000);
1472
+ let started;
1473
+ if (inspection.exitCode === 0) {
1474
+ started = inspection.stdout.trim() === "true"
1475
+ ? { exitCode: 0, stdout: "", stderr: "" }
1476
+ : await this.runProcess("docker", ["start", containerName], 60_000);
1477
+ }
1478
+ else {
1479
+ started = await this.runProcess("docker", [
1480
+ "run",
1481
+ "-d",
1482
+ "--name",
1483
+ containerName,
1484
+ "-e",
1485
+ `POSTGRES_USER=${username}`,
1486
+ "-e",
1487
+ `POSTGRES_PASSWORD=${password}`,
1488
+ "-e",
1489
+ `POSTGRES_DB=${database}`,
1490
+ "-p",
1491
+ `127.0.0.1:${port}:5432`,
1492
+ process.env.CODE_HARNESS_POSTGRES_IMAGE || "postgres:16-alpine",
1493
+ ], 2 * 60_000);
1494
+ }
1495
+ if (started.exitCode !== 0) {
1496
+ return started.stderr.trim() || "Failed to start PostgreSQL container";
1497
+ }
1498
+ for (let attempt = 0; attempt < 60; attempt += 1) {
1499
+ if (await this.portIsOpen(hostname, port, 1000)) {
1500
+ const ready = await this.runProcess("docker", [
1501
+ "exec",
1502
+ containerName,
1503
+ "pg_isready",
1504
+ "-U",
1505
+ username,
1506
+ "-d",
1507
+ database,
1508
+ ], 5_000);
1509
+ if (ready.exitCode === 0)
1510
+ return undefined;
1511
+ }
1512
+ await delay(500);
1513
+ }
1514
+ return `PostgreSQL did not become ready at ${hostname}:${port}`;
1515
+ }
1516
+ postgresContainerName() {
1517
+ const digest = createHash("sha256")
1518
+ .update(this.workspaceReal)
1519
+ .digest("hex")
1520
+ .slice(0, 12);
1521
+ return `code-harness-postgres-${digest}`;
1522
+ }
1523
+ async preparePostgresCliShim() {
1524
+ const containerName = this.postgresContainerName();
1525
+ let inspection;
1526
+ try {
1527
+ inspection = await this.runProcess("docker", ["inspect", "-f", "{{.State.Running}}", containerName], 15_000);
1528
+ }
1529
+ catch {
1530
+ return;
1531
+ }
1532
+ if (inspection.exitCode !== 0 || inspection.stdout.trim() !== "true")
1533
+ return;
1534
+ const binDirectory = path.join(this.workspaceReal, ".code-harness", "bin");
1535
+ const wrapperPath = path.join(binDirectory, "psql");
1536
+ await mkdir(binDirectory, { recursive: true });
1537
+ await writeFile(wrapperPath, `#!/bin/sh\nexec docker exec -i ${containerName} psql "$@"\n`, "utf8");
1538
+ await chmod(wrapperPath, 0o755);
1539
+ }
1540
+ buildAndStartFailure(stage, message, logs) {
1541
+ return {
1542
+ status: "error",
1543
+ message,
1544
+ stage,
1545
+ logTail: logs.trim().slice(-12_000),
1546
+ };
1547
+ }
1548
+ async stopPreviousPreview(pidPath) {
1549
+ try {
1550
+ const pid = Number((await readFile(pidPath, "utf8")).trim());
1551
+ if (!Number.isInteger(pid) || pid <= 1)
1552
+ return;
1553
+ try {
1554
+ if (process.platform === "win32")
1555
+ process.kill(pid, "SIGTERM");
1556
+ else
1557
+ process.kill(-pid, "SIGTERM");
1558
+ }
1559
+ catch (error) {
1560
+ if (error.code !== "ESRCH")
1561
+ throw error;
1562
+ }
1563
+ await delay(250);
1564
+ }
1565
+ catch (error) {
1566
+ if (error.code !== "ENOENT")
1567
+ throw error;
1568
+ }
1569
+ }
1570
+ async buildAndStart() {
1571
+ const timeoutMs = Number(process.env.CODE_HARNESS_FULLSTACK_BUILD_TIMEOUT_MS || 300_000);
1572
+ const databaseError = await this.ensureFullStackDatabase();
1573
+ if (databaseError) {
1574
+ return this.buildAndStartFailure("database", `Fullstack sandbox setup failed: ${databaseError}`, databaseError);
1575
+ }
1576
+ await this.preparePostgresCliShim();
1577
+ const nextBinary = path.join(this.workspaceReal, "node_modules", ".bin", "next");
1578
+ if (!existsSync(nextBinary)) {
1579
+ const installed = await this.runNpmProcess(["install"], timeoutMs);
1580
+ if (installed.exitCode !== 0) {
1581
+ const logs = `${installed.stdout}${installed.stderr}`;
1582
+ return this.buildAndStartFailure("install", `Fullstack sandbox setup failed: Dependency installation failed (exit ${installed.exitCode ?? -1}) after ${timeoutMs}ms. See startup diagnostics for log output.`, logs);
1583
+ }
1584
+ }
1585
+ const buildStartedAt = Date.now();
1586
+ const built = await this.runNpmProcess(["run", "build"], timeoutMs);
1587
+ const buildLatencyMs = Date.now() - buildStartedAt;
1588
+ if (built.exitCode !== 0) {
1589
+ const logs = `${built.stdout}${built.stderr}`;
1590
+ return this.buildAndStartFailure("build", `Fullstack sandbox setup failed: Production build failed (exit ${built.exitCode ?? -1}) after ${timeoutMs}ms. See startup diagnostics for log output.`, logs);
1591
+ }
1592
+ const runtimeDirectory = path.join(this.workspaceReal, ".code-harness");
1593
+ const pidPath = path.join(runtimeDirectory, "preview.pid");
1594
+ const logPath = path.join(runtimeDirectory, "preview.log");
1595
+ await mkdir(runtimeDirectory, { recursive: true });
1596
+ await this.stopPreviousPreview(pidPath);
1597
+ const portValue = Number(process.env.CODE_HARNESS_PREVIEW_PORT || 3000);
1598
+ const port = Number.isInteger(portValue) && portValue > 0 ? portValue : 3000;
1599
+ const npmCli = process.env.CODE_HARNESS_NPM_CLI_JS;
1600
+ const command = npmCli ? process.execPath : "npm";
1601
+ const args = [
1602
+ ...(npmCli ? [npmCli] : []),
1603
+ "run",
1604
+ "start",
1605
+ "--",
1606
+ "--hostname",
1607
+ "127.0.0.1",
1608
+ "--port",
1609
+ String(port),
1610
+ ];
1611
+ const logFd = openSync(logPath, "w");
1612
+ const child = spawn(command, args, {
1613
+ cwd: this.workspaceReal,
1614
+ env: { ...process.env, PORT: String(port) },
1615
+ detached: process.platform !== "win32",
1616
+ stdio: ["ignore", logFd, logFd],
1617
+ });
1618
+ closeSync(logFd);
1619
+ if (!child.pid) {
1620
+ return this.buildAndStartFailure("start", "Fullstack sandbox setup failed: Production server did not start.", "Production server did not return a process id");
1621
+ }
1622
+ child.unref();
1623
+ await writeFile(pidPath, `${child.pid}\n`, "utf8");
1624
+ const localUrl = `http://127.0.0.1:${port}`;
1625
+ for (let attempt = 0; attempt < 60; attempt += 1) {
1626
+ try {
1627
+ const response = await fetch(`${localUrl}/api/health`, {
1628
+ signal: AbortSignal.timeout(1000),
1629
+ });
1630
+ if (response.ok) {
1631
+ return {
1632
+ status: "success",
1633
+ previewUrl: process.env.CODE_HARNESS_PREVIEW_URL || localUrl,
1634
+ buildLatencyMs,
1635
+ };
1636
+ }
1637
+ }
1638
+ catch {
1639
+ // The production server is still starting.
1640
+ }
1641
+ await delay(500);
1642
+ }
1643
+ try {
1644
+ if (process.platform === "win32")
1645
+ process.kill(child.pid, "SIGTERM");
1646
+ else
1647
+ process.kill(-child.pid, "SIGTERM");
1648
+ }
1649
+ catch {
1650
+ // The failed preview may already have exited.
1651
+ }
1652
+ let logs = "";
1653
+ try {
1654
+ logs = await readFile(logPath, "utf8");
1655
+ }
1656
+ catch {
1657
+ logs = "Production server exited without logs";
1658
+ }
1659
+ return this.buildAndStartFailure("healthcheck", "Fullstack sandbox setup failed: Production server failed the /api/health check.", logs);
1660
+ }
1661
+ async bash(command, cwd, timeoutSeconds) {
1662
+ const workdir = await this.confinedPath(cwd);
1663
+ if (!(await stat(workdir)).isDirectory()) {
1664
+ throw new Error(`cwd is not a directory: ${cwd}`);
1665
+ }
1666
+ // Runtime-aware callers execute commands exactly as written and report
1667
+ // real local paths. The legacy /home/user translation is limited to the
1668
+ // older non-Full-Stack replay contract.
1669
+ const translatedCommand = this.workspaceContext || this.config.mode === "full-stack"
1670
+ ? command
1671
+ : command.replaceAll("/home/user", '"$ARENA_WORKSPACE"');
1672
+ return new Promise((resolve, reject) => {
1673
+ const env = {
1674
+ ...process.env,
1675
+ HOME: this.workspace,
1676
+ ARENA_WORKSPACE: this.workspace,
1677
+ CODE_HARNESS_WORKSPACE: this.workspace,
1678
+ PATH: `${path.join(this.workspaceReal, ".code-harness", "bin")}${path.delimiter}${process.env.PATH || ""}`,
1679
+ // 生产 sandbox 是 C collation(参考 find|sort 为逐字节序),宿主
1680
+ // 常见 en_US.UTF-8 会翻转大小写次序;统一成 C.UTF-8 对齐生产。
1681
+ LANG: "C.UTF-8",
1682
+ LC_ALL: "C.UTF-8",
1683
+ };
1684
+ // 交互终端(node --test、npm 等)会向子进程注入强制着色变量,
1685
+ // 管道输出被 ANSI 色码污染;生产轨迹无色码,剥离之。
1686
+ delete env.FORCE_COLOR;
1687
+ delete env.CLICOLOR_FORCE;
1688
+ // detached:管道成员(npm、tail…)与 bash 同进程组。只杀 bash 会留下
1689
+ // 孤儿持有 stdout/stderr 管道,close 永不触发,整个 turn 挂死;
1690
+ // 必须杀整组,并在宽限期后强制结算兜底。
1691
+ const child = spawn("/bin/bash", ["-c", translatedCommand], {
1692
+ cwd: workdir,
1693
+ env,
1694
+ stdio: ["ignore", "pipe", "pipe"],
1695
+ detached: process.platform !== "win32",
1696
+ });
1697
+ const startedAt = Date.now();
1698
+ let stdout = "";
1699
+ let stderr = "";
1700
+ let timedOut = false;
1701
+ let settled = false;
1702
+ let killRequested = false;
1703
+ let forceSettle;
1704
+ const killTree = (signal) => {
1705
+ try {
1706
+ if (process.platform !== "win32" && child.pid) {
1707
+ process.kill(-child.pid, signal);
1708
+ }
1709
+ else {
1710
+ child.kill(signal);
1711
+ }
1712
+ }
1713
+ catch (error) {
1714
+ // ESRCH:组已消失。EPERM:Darwin 在组正被回收时对重复 kill 的
1715
+ // 返回值,同样视作已处理。
1716
+ const code = error.code;
1717
+ if (code !== "ESRCH" && code !== "EPERM")
1718
+ throw error;
1719
+ }
1720
+ };
1721
+ const finish = (code) => {
1722
+ if (settled)
1723
+ return;
1724
+ settled = true;
1725
+ clearTimeout(timeout);
1726
+ if (forceSettle)
1727
+ clearTimeout(forceSettle);
1728
+ // 组长退出会立刻触发 close 并取消宽限定时器;取消/超时一旦请求过
1729
+ // 击杀,就必须在结算时补一发组级 SIGKILL,否则忽略 TERM 且关闭了
1730
+ // 继承管道的后台子进程会活过取消。正常退出不清场——模型可能有意
1731
+ // 留下后台服务供后续调用使用。
1732
+ if (killRequested)
1733
+ killTree("SIGKILL");
1734
+ this.signal?.removeEventListener("abort", abort);
1735
+ if (this.signal?.aborted) {
1736
+ reject(this.signal.reason || new Error("operation cancelled"));
1737
+ return;
1738
+ }
1739
+ // 生产契约:status 恒为 "completed"(包括非零退出码),超时无参考样本,
1740
+ // 按最小猜测同样返回 completed + exit_code null,stderr 附超时说明。
1741
+ if (timedOut && this.config.mode !== "full-stack") {
1742
+ stderr +=
1743
+ (stderr ? "\n" : "") + `command exceeded ${timeoutSeconds}s`;
1744
+ }
1745
+ const out = capBashOutput(this.virtualizeBashOutput(stdout));
1746
+ const err = capBashOutput(this.virtualizeBashOutput(stderr));
1747
+ resolve({
1748
+ status: "completed",
1749
+ stderr: err.text,
1750
+ stdout: out.text,
1751
+ exit_code: timedOut ? null : code,
1752
+ duration_ms: Date.now() - startedAt,
1753
+ stderr_truncated: err.truncated,
1754
+ stdout_truncated: out.truncated,
1755
+ });
1756
+ };
1757
+ // 宽限期结束时升级为组级 SIGKILL 再结算:abort 只发 SIGTERM,
1758
+ // trap TERM 的进程会活过 Promise 拒绝并继续改写工作区。
1759
+ const armForceSettle = () => {
1760
+ forceSettle ??= setTimeout(() => {
1761
+ killTree("SIGKILL");
1762
+ finish(null);
1763
+ }, 2000);
1764
+ };
1765
+ const timeout = setTimeout(() => {
1766
+ timedOut = true;
1767
+ killRequested = true;
1768
+ killTree("SIGKILL");
1769
+ armForceSettle();
1770
+ }, timeoutSeconds * 1000);
1771
+ const abort = () => {
1772
+ killRequested = true;
1773
+ killTree("SIGTERM");
1774
+ armForceSettle();
1775
+ };
1776
+ this.signal?.addEventListener("abort", abort, { once: true });
1777
+ child.stdout.on("data", (chunk) => {
1778
+ stdout += chunk.toString("utf8");
1779
+ });
1780
+ child.stderr.on("data", (chunk) => {
1781
+ stderr += chunk.toString("utf8");
1782
+ });
1783
+ child.once("error", (error) => {
1784
+ if (settled)
1785
+ return;
1786
+ settled = true;
1787
+ clearTimeout(timeout);
1788
+ if (forceSettle)
1789
+ clearTimeout(forceSettle);
1790
+ this.signal?.removeEventListener("abort", abort);
1791
+ reject(error);
1792
+ });
1793
+ child.once("close", (code) => finish(code));
1794
+ });
1795
+ }
1796
+ async webSearch(query, depth) {
1797
+ const model = process.env.CODE_HARNESS_WEB_SEARCH_MODEL ||
1798
+ process.env.ARENA_WEB_SEARCH_MODEL ||
1799
+ "qwen-plus";
1800
+ const searchStrategy = depth === "1" ? "turbo" : "max";
1801
+ const payload = await dashScopeJson("api", "/services/aigc/text-generation/generation", {
1802
+ method: "POST",
1803
+ headers: { "content-type": "application/json" },
1804
+ body: JSON.stringify({
1805
+ model,
1806
+ input: {
1807
+ messages: [{ role: "user", content: query }],
1808
+ },
1809
+ parameters: {
1810
+ enable_search: true,
1811
+ result_format: "message",
1812
+ search_options: {
1813
+ forced_search: true,
1814
+ enable_source: true,
1815
+ enable_citation: true,
1816
+ citation_format: "[<number>]",
1817
+ search_strategy: searchStrategy,
1818
+ },
1819
+ },
1820
+ }),
1821
+ signal: this.signal,
1822
+ }, "web_search");
1823
+ const output = record(payload.output);
1824
+ const searchInfo = record(output.search_info);
1825
+ const searchResults = Array.isArray(searchInfo.search_results)
1826
+ ? searchInfo.search_results
1827
+ : [];
1828
+ const limit = { "1": 3, "2": 5, "3": 8 }[depth] || 5;
1829
+ // 参考轨迹条目:{id, url, title, description[, pageAge]};
1830
+ // DashScope 无 pageAge,按可选键省略。
1831
+ return {
1832
+ status: "success",
1833
+ results: searchResults.slice(0, limit).map((value, index) => {
1834
+ const item = record(value);
1835
+ return {
1836
+ id: Number(item.index) || index + 1,
1837
+ url: stringValue(item.url),
1838
+ title: stringValue(item.title),
1839
+ description: stringValue(item.content) ||
1840
+ stringValue(item.snippet) ||
1841
+ stringValue(item.summary),
1842
+ };
1843
+ }),
1844
+ };
1845
+ }
1846
+ async webFetch(url, format) {
1847
+ const result = await this.pageReader.readFormatted(url, format);
1848
+ return {
1849
+ status: "success",
1850
+ title: String(result.title || "No Title Found"),
1851
+ content: String(result.content || ""),
1852
+ };
1853
+ }
1854
+ async pexelsRequest(pathname, parameters) {
1855
+ const apiKey = process.env.PEXELS_API_KEY;
1856
+ if (!apiKey)
1857
+ throw new Error("fetch_media requires PEXELS_API_KEY");
1858
+ const url = new URL(pathname, "https://api.pexels.com");
1859
+ for (const [name, value] of Object.entries(parameters)) {
1860
+ if (value && value !== "any")
1861
+ url.searchParams.set(name, value);
1862
+ }
1863
+ return cachedMediaRequest({
1864
+ provider: "fetch_media",
1865
+ url,
1866
+ init: {
1867
+ headers: { authorization: apiKey },
1868
+ signal: this.signal,
1869
+ },
1870
+ beforeNetwork: () => {
1871
+ if (pexelsQuota &&
1872
+ pexelsQuota.remaining <= 0 &&
1873
+ Date.now() < pexelsQuota.resetAtMs) {
1874
+ throw new MediaProviderHttpError("fetch_media", 429, "Pexels quota exhausted");
1875
+ }
1876
+ },
1877
+ onResponse: (response) => {
1878
+ if (!response.ok)
1879
+ return;
1880
+ const remaining = Number(response.headers.get("x-ratelimit-remaining"));
1881
+ const resetAtSeconds = Number(response.headers.get("x-ratelimit-reset"));
1882
+ if (Number.isFinite(remaining) && Number.isFinite(resetAtSeconds)) {
1883
+ pexelsQuota = {
1884
+ remaining,
1885
+ resetAtMs: resetAtSeconds * 1000,
1886
+ };
1887
+ }
1888
+ },
1889
+ });
1890
+ }
1891
+ mapPexelsPhotos(payload) {
1892
+ const photos = Array.isArray(payload.photos) ? payload.photos : [];
1893
+ return photos.map((value) => {
1894
+ const photo = record(value);
1895
+ const source = record(photo.src);
1896
+ return {
1897
+ type: "image",
1898
+ id: Number(photo.id),
1899
+ pexelsUrl: stringValue(photo.url),
1900
+ recommendedUrl: stringValue(source.large2x) ||
1901
+ stringValue(source.large) ||
1902
+ stringValue(source.original),
1903
+ thumbnailUrl: stringValue(source.medium) ||
1904
+ stringValue(source.small) ||
1905
+ stringValue(source.tiny),
1906
+ width: Number(photo.width),
1907
+ height: Number(photo.height),
1908
+ creatorName: stringValue(photo.photographer),
1909
+ creatorUrl: stringValue(photo.photographer_url),
1910
+ alt: stringValue(photo.alt),
1911
+ };
1912
+ });
1913
+ }
1914
+ mapPexelsVideos(payload) {
1915
+ const videos = Array.isArray(payload.videos) ? payload.videos : [];
1916
+ return videos.map((value) => {
1917
+ const video = record(value);
1918
+ const user = record(video.user);
1919
+ const pictures = Array.isArray(video.video_pictures)
1920
+ ? video.video_pictures.map(record)
1921
+ : [];
1922
+ const files = Array.isArray(video.video_files)
1923
+ ? video.video_files.map(record)
1924
+ : [];
1925
+ const preferred = files.find((item) => stringValue(item.quality) === "hd") ||
1926
+ files[0] ||
1927
+ {};
1928
+ return {
1929
+ type: "video",
1930
+ id: Number(video.id),
1931
+ pexelsUrl: stringValue(video.url),
1932
+ recommendedUrl: stringValue(preferred.link),
1933
+ thumbnailUrl: stringValue(pictures[0]?.picture),
1934
+ width: Number(video.width),
1935
+ height: Number(video.height),
1936
+ duration: Number(video.duration),
1937
+ creatorName: stringValue(user.name),
1938
+ creatorUrl: stringValue(user.url),
1939
+ };
1940
+ });
1941
+ }
1942
+ async fetchMediaFromPexels(query, mediaType, requestedCount, orientation, size, locale) {
1943
+ const common = {
1944
+ query: normalizeMediaQuery(query),
1945
+ per_page: "10",
1946
+ orientation,
1947
+ size,
1948
+ locale,
1949
+ };
1950
+ let totalResults = 0;
1951
+ let photoResults = [];
1952
+ let videoResults = [];
1953
+ if (mediaType === "image" || mediaType === "both") {
1954
+ const photos = await this.pexelsRequest("/v1/search", common);
1955
+ totalResults += Number(photos.total_results || 0);
1956
+ photoResults = this.mapPexelsPhotos(photos);
1957
+ }
1958
+ if (mediaType === "video" || mediaType === "both") {
1959
+ const videos = await this.pexelsRequest("/v1/videos/search", common);
1960
+ totalResults += Number(videos.total_results || 0);
1961
+ videoResults = this.mapPexelsVideos(videos);
1962
+ }
1963
+ const results = mediaType === "both"
1964
+ ? interleaveMedia(photoResults, videoResults)
1965
+ : mediaType === "image"
1966
+ ? photoResults
1967
+ : videoResults;
1968
+ return {
1969
+ status: "success",
1970
+ query,
1971
+ mediaType,
1972
+ totalResults,
1973
+ results: results.slice(0, requestedCount),
1974
+ };
1975
+ }
1976
+ async fetchMedia(query, mediaType, count, orientation, size, locale) {
1977
+ const requestedCount = Math.min(10, Math.max(1, Math.trunc(count)));
1978
+ return this.fetchMediaFromPexels(query, mediaType, requestedCount, orientation, size, locale);
1979
+ }
1980
+ async generateImage(rawPath, prompt, images) {
1981
+ if (this.generatedImageCount >= 10) {
1982
+ return {
1983
+ status: "error",
1984
+ message: "generate_image can be used at most 10 times per session",
1985
+ };
1986
+ }
1987
+ this.generatedImageCount += 1;
1988
+ const target = await this.confinedPath(rawPath);
1989
+ if (![".jpg", ".jpeg", ".png"].includes(path.extname(target).toLowerCase())) {
1990
+ throw new Error("file_path must end in .jpg, .jpeg, or .png");
1991
+ }
1992
+ mkdirSync(path.dirname(target), { recursive: true });
1993
+ const model = process.env.CODE_HARNESS_IMAGE_MODEL ||
1994
+ process.env.ARENA_IMAGE_MODEL ||
1995
+ "qwen-image-2.0-pro";
1996
+ const isQwenImage2 = model.startsWith("qwen-image-2.0");
1997
+ const content = [];
1998
+ for (const imagePath of images || []) {
1999
+ const source = await this.confinedPath(imagePath);
2000
+ const extension = path.extname(source).toLowerCase();
2001
+ const mimeType = mimeTypes[extension];
2002
+ if (!mimeType?.startsWith("image/")) {
2003
+ throw new Error(`unsupported reference image format: ${imagePath}`);
2004
+ }
2005
+ const bytes = await readFile(source);
2006
+ if (bytes.length > 20 * 1024 * 1024) {
2007
+ throw new Error(`reference image exceeds 20 MB: ${imagePath}`);
2008
+ }
2009
+ content.push({
2010
+ image: `data:${mimeType};base64,${bytes.toString("base64")}`,
2011
+ });
2012
+ }
2013
+ content.push({ text: prompt });
2014
+ const parameters = {
2015
+ size: process.env.CODE_HARNESS_IMAGE_SIZE ||
2016
+ process.env.ARENA_IMAGE_SIZE ||
2017
+ (isQwenImage2 ? "2048*2048" : "2K"),
2018
+ watermark: false,
2019
+ };
2020
+ if (isQwenImage2) {
2021
+ parameters.prompt_extend = true;
2022
+ }
2023
+ else {
2024
+ parameters.n = 1;
2025
+ if (!images?.length)
2026
+ parameters.thinking_mode = true;
2027
+ }
2028
+ const headers = {
2029
+ "content-type": "application/json",
2030
+ };
2031
+ if (!isQwenImage2)
2032
+ headers["x-dashscope-async"] = "enable";
2033
+ const submitted = await dashScopeJson("api", isQwenImage2
2034
+ ? "/services/aigc/multimodal-generation/generation"
2035
+ : "/services/aigc/image-generation/generation", {
2036
+ method: "POST",
2037
+ headers,
2038
+ body: JSON.stringify({
2039
+ model,
2040
+ input: {
2041
+ messages: [{ role: "user", content }],
2042
+ },
2043
+ parameters,
2044
+ }),
2045
+ signal: this.signal,
2046
+ }, "generate_image");
2047
+ let completed = isQwenImage2
2048
+ ? submitted
2049
+ : undefined;
2050
+ if (!isQwenImage2) {
2051
+ const taskId = stringValue(record(submitted.output).task_id);
2052
+ if (!taskId) {
2053
+ throw new Error(`generate_image returned no task_id: ${responseMessage(submitted)}`);
2054
+ }
2055
+ const timeoutMs = Number(process.env.DASHSCOPE_TASK_TIMEOUT_MS || 5 * 60 * 1000);
2056
+ const pollIntervalMs = Number(process.env.DASHSCOPE_TASK_POLL_INTERVAL_MS || 1000);
2057
+ const deadline = Date.now() + timeoutMs;
2058
+ while (Date.now() <= deadline) {
2059
+ const task = await dashScopeJson("api", `/tasks/${encodeURIComponent(taskId)}`, { method: "GET", signal: this.signal }, "generate_image status");
2060
+ const output = record(task.output);
2061
+ const taskStatus = stringValue(output.task_status).toUpperCase();
2062
+ if (taskStatus === "SUCCEEDED") {
2063
+ completed = task;
2064
+ break;
2065
+ }
2066
+ if (["FAILED", "CANCELED", "UNKNOWN"].includes(taskStatus)) {
2067
+ throw new Error(`generate_image task ${taskStatus}: ${responseMessage(task)}`);
2068
+ }
2069
+ if (Date.now() + pollIntervalMs > deadline)
2070
+ break;
2071
+ await delay(pollIntervalMs, undefined, { signal: this.signal });
2072
+ }
2073
+ if (!completed) {
2074
+ throw new Error(`generate_image timed out waiting for task ${taskId}`);
2075
+ }
2076
+ }
2077
+ if (!completed) {
2078
+ throw new Error("generate_image returned no response");
2079
+ }
2080
+ const output = record(completed.output);
2081
+ const choices = Array.isArray(output.choices) ? output.choices : [];
2082
+ const message = record(record(choices[0]).message);
2083
+ const generatedContent = Array.isArray(message.content)
2084
+ ? message.content
2085
+ : [];
2086
+ const generated = generatedContent
2087
+ .map(record)
2088
+ .find((item) => typeof item.image === "string");
2089
+ const legacyResults = Array.isArray(output.results) ? output.results : [];
2090
+ const imageUrl = stringValue(generated?.image) ||
2091
+ stringValue(record(legacyResults[0]).url);
2092
+ if (!imageUrl) {
2093
+ throw new Error("generate_image response did not include an image URL");
2094
+ }
2095
+ const imageResponse = await fetch(imageUrl, { signal: this.signal });
2096
+ if (!imageResponse.ok) {
2097
+ throw new Error(`image download failed (${imageResponse.status})`);
2098
+ }
2099
+ await writeFile(target, await responseBytes(imageResponse, 25 * 1024 * 1024));
2100
+ return {
2101
+ status: "success",
2102
+ message: `Image saved to ${this.relativePath(target)}`,
2103
+ };
2104
+ }
2105
+ }
2106
+ //# sourceMappingURL=code-tools.js.map