stitchkit 0.70.6 → 0.72.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/README.md +1 -1
  2. package/dist/agent-runtime/coding-tool-contract.d.ts +11 -1
  3. package/dist/agent-runtime/coding-tool-contract.d.ts.map +1 -1
  4. package/dist/agent-runtime/coding-tool-files.d.ts.map +1 -1
  5. package/dist/agent-runtime/coding-tool-listing.d.ts +15 -0
  6. package/dist/agent-runtime/coding-tool-listing.d.ts.map +1 -0
  7. package/dist/agent-runtime/coding-tool-paths.d.ts.map +1 -1
  8. package/dist/agent-runtime/coding-tool-refusals.d.ts +59 -0
  9. package/dist/agent-runtime/coding-tool-refusals.d.ts.map +1 -0
  10. package/dist/agent-runtime/coding-tool-search-patch.d.ts.map +1 -1
  11. package/dist/agent-runtime/coding-tools.d.ts +3 -1
  12. package/dist/agent-runtime/coding-tools.d.ts.map +1 -1
  13. package/dist/agent-runtime/compaction.d.ts +35 -0
  14. package/dist/agent-runtime/compaction.d.ts.map +1 -1
  15. package/dist/agent-runtime/contained-files.d.ts +21 -2
  16. package/dist/agent-runtime/contained-files.d.ts.map +1 -1
  17. package/dist/agent-runtime/provider-failure.d.ts +66 -0
  18. package/dist/agent-runtime/provider-failure.d.ts.map +1 -0
  19. package/dist/agent-runtime/run-execution.d.ts.map +1 -1
  20. package/dist/agent-runtime/runtime.d.ts +31 -1
  21. package/dist/agent-runtime/runtime.d.ts.map +1 -1
  22. package/dist/agent-runtime/schemas.d.ts +1 -0
  23. package/dist/agent-runtime/schemas.d.ts.map +1 -1
  24. package/dist/agent-runtime-coding-tools.js +438 -124
  25. package/dist/agent-runtime-harness.js +4 -4
  26. package/dist/agent-runtime-openrouter.d.ts +19 -0
  27. package/dist/agent-runtime-openrouter.d.ts.map +1 -1
  28. package/dist/agent-runtime-openrouter.js +2 -1
  29. package/dist/agent-runtime.d.ts +3 -2
  30. package/dist/agent-runtime.d.ts.map +1 -1
  31. package/dist/agent-runtime.js +118 -14
  32. package/dist/application/admission.d.ts +22 -2
  33. package/dist/application/admission.d.ts.map +1 -1
  34. package/dist/application/channel.d.ts +28 -0
  35. package/dist/application/channel.d.ts.map +1 -1
  36. package/dist/application/diagnostic-journal-contract.d.ts +20 -0
  37. package/dist/application/diagnostic-journal-contract.d.ts.map +1 -1
  38. package/dist/application/diagnostic-journal-lock.d.ts +17 -0
  39. package/dist/application/diagnostic-journal-lock.d.ts.map +1 -0
  40. package/dist/application/diagnostic-journal-manager.d.ts +2 -1
  41. package/dist/application/diagnostic-journal-manager.d.ts.map +1 -1
  42. package/dist/application/diagnostic-journal-storage.d.ts +4 -1
  43. package/dist/application/diagnostic-journal-storage.d.ts.map +1 -1
  44. package/dist/application/diagnostic-journal.d.ts +4 -2
  45. package/dist/application/diagnostic-journal.d.ts.map +1 -1
  46. package/dist/application-opentelemetry.js +1 -1
  47. package/dist/application.d.ts +3 -3
  48. package/dist/application.d.ts.map +1 -1
  49. package/dist/application.js +134 -21
  50. package/dist/browser/resumable.d.ts +56 -0
  51. package/dist/browser/resumable.d.ts.map +1 -0
  52. package/dist/cli.js +5 -5
  53. package/dist/{index-cx84zg25.js → index-118v0z41.js} +1 -1
  54. package/dist/{index-zpyj7hsv.js → index-3cwck0rm.js} +98 -33
  55. package/dist/{index-ejv31h14.js → index-7qy2ex0m.js} +1 -1
  56. package/dist/{index-trrf7nch.js → index-ezmn6ac6.js} +3 -3
  57. package/dist/{index-ekkt4gy9.js → index-jqtsc9mj.js} +2 -2
  58. package/dist/{index-hxtm2xr2.js → index-k2zczx1g.js} +1 -1
  59. package/dist/{index-by57nwhc.js → index-m668wzyc.js} +10 -2
  60. package/dist/{index-w9m1wznn.js → index-nemjkxjp.js} +118 -1
  61. package/dist/{index-tnazr5tc.js → index-pz7ytdga.js} +3 -3
  62. package/dist/{index-1z5xxfst.js → index-rqpdar1e.js} +102 -5
  63. package/dist/{index-qm87g51s.js → index-s4c8wy8m.js} +1 -1
  64. package/dist/index.d.ts +1 -0
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +103 -14
  67. package/dist/node.js +2 -2
  68. package/dist/observability/index.js +1 -1
  69. package/dist/primitives/audit.d.ts +52 -0
  70. package/dist/primitives/audit.d.ts.map +1 -0
  71. package/dist/primitives/deadline.d.ts +39 -0
  72. package/dist/primitives/deadline.d.ts.map +1 -0
  73. package/dist/primitives/decimal.d.ts +10 -0
  74. package/dist/primitives/decimal.d.ts.map +1 -0
  75. package/dist/primitives/delivery.d.ts +96 -0
  76. package/dist/primitives/delivery.d.ts.map +1 -0
  77. package/dist/primitives/event.d.ts +42 -0
  78. package/dist/primitives/event.d.ts.map +1 -0
  79. package/dist/primitives/export-operation.d.ts +89 -0
  80. package/dist/primitives/export-operation.d.ts.map +1 -0
  81. package/dist/primitives/index.d.ts +12 -0
  82. package/dist/primitives/index.d.ts.map +1 -0
  83. package/dist/primitives/lifecycle.d.ts +90 -0
  84. package/dist/primitives/lifecycle.d.ts.map +1 -0
  85. package/dist/primitives/migration-checks.d.ts +15 -0
  86. package/dist/primitives/migration-checks.d.ts.map +1 -0
  87. package/dist/primitives/money.d.ts +43 -0
  88. package/dist/primitives/money.d.ts.map +1 -0
  89. package/dist/primitives/owner-scope.d.ts +30 -0
  90. package/dist/primitives/owner-scope.d.ts.map +1 -0
  91. package/dist/primitives/permission.d.ts +28 -0
  92. package/dist/primitives/permission.d.ts.map +1 -0
  93. package/dist/primitives/quantity.d.ts +57 -0
  94. package/dist/primitives/quantity.d.ts.map +1 -0
  95. package/dist/primitives.d.ts +3 -0
  96. package/dist/primitives.d.ts.map +1 -0
  97. package/dist/primitives.js +750 -0
  98. package/dist/server/index.d.ts +1 -1
  99. package/dist/server/index.d.ts.map +1 -1
  100. package/dist/server/index.js +4 -2
  101. package/dist/server/request.d.ts +19 -0
  102. package/dist/server/request.d.ts.map +1 -1
  103. package/dist/telegram/init-data.d.ts +134 -0
  104. package/dist/telegram/init-data.d.ts.map +1 -0
  105. package/dist/telegram/send-failure.d.ts +73 -0
  106. package/dist/telegram/send-failure.d.ts.map +1 -0
  107. package/dist/telegram.d.ts +3 -0
  108. package/dist/telegram.d.ts.map +1 -0
  109. package/dist/telegram.js +224 -0
  110. package/dist/tool-invoker.js +4 -4
  111. package/dist/tools.js +7 -7
  112. package/llms-full.txt +510 -10
  113. package/llms.txt +1 -0
  114. package/native/darwin-arm64.node +0 -0
  115. package/native/darwin-x64.node +0 -0
  116. package/package.json +11 -3
@@ -2,13 +2,15 @@ import {
2
2
  assertContainedFileCurrent,
3
3
  assertContainedParentCurrent,
4
4
  containedEntryMetadata,
5
+ listContainedDirectory,
6
+ missingContainedDirectories,
5
7
  openContainedFile,
6
8
  openContainedParent,
7
9
  openContainedParentFile,
8
10
  readContainedUtf8Handle,
9
11
  scanContainedFiles,
10
12
  writeContainedFile
11
- } from "./index-1z5xxfst.js";
13
+ } from "./index-rqpdar1e.js";
12
14
  import {
13
15
  defineRuntimeTool
14
16
  } from "./index-wwst0td5.js";
@@ -16,6 +18,7 @@ import"./index-22by16v6.js";
16
18
  import"./index-cby4ar3v.js";
17
19
  import"./index-vj3vvpaa.js";
18
20
  import {
21
+ AppError,
19
22
  conflict,
20
23
  forbidden
21
24
  } from "./index-0w9abg87.js";
@@ -23,8 +26,8 @@ import"./index-qyrqwr4c.js";
23
26
  import"./index-6k1937bx.js";
24
27
 
25
28
  // src/agent-runtime/coding-tools.ts
26
- import path3 from "node:path";
27
- import { z as z3 } from "zod";
29
+ import path4 from "node:path";
30
+ import { z as z4 } from "zod";
28
31
 
29
32
  // src/agent-runtime/coding-tool-contract.ts
30
33
  import { z } from "zod";
@@ -36,6 +39,7 @@ var AgentCodingToolLimitsSchema = z.object({
36
39
  maxShellArgumentBytes: z.int().positive(),
37
40
  maxShellOutputBytes: z.int().positive(),
38
41
  maxArtifactBytes: z.int().positive(),
42
+ maxListEntries: z.int().positive(),
39
43
  maxSearchResults: z.int().positive(),
40
44
  maxSearchFiles: z.int().positive(),
41
45
  maxSearchDepth: z.int().nonnegative(),
@@ -48,7 +52,8 @@ var AgentCodingToolAuthorizationSchema = z.discriminatedUnion("operation", [
48
52
  operation: z.literal("write"),
49
53
  path: z.string().min(1),
50
54
  bytes: z.int().nonnegative(),
51
- overwrite: z.boolean()
55
+ overwrite: z.boolean(),
56
+ createsDirectories: z.array(z.string().min(1))
52
57
  }).strict(),
53
58
  z.object({
54
59
  operation: z.literal("search"),
@@ -56,7 +61,7 @@ var AgentCodingToolAuthorizationSchema = z.discriminatedUnion("operation", [
56
61
  mode: z.enum(["path", "content"])
57
62
  }).strict(),
58
63
  z.object({
59
- operation: z.literal("patch"),
64
+ operation: z.literal("edit"),
60
65
  path: z.string().min(1),
61
66
  baseSha256: z.string().length(64),
62
67
  resultSha256: z.string().length(64),
@@ -64,6 +69,12 @@ var AgentCodingToolAuthorizationSchema = z.discriminatedUnion("operation", [
64
69
  replacements: z.int().positive(),
65
70
  dryRun: z.boolean()
66
71
  }).strict(),
72
+ z.object({ operation: z.literal("list"), path: z.string().min(1) }).strict(),
73
+ z.object({
74
+ operation: z.literal("glob"),
75
+ pattern: z.string().min(1),
76
+ path: z.string().min(1)
77
+ }).strict(),
67
78
  z.object({
68
79
  operation: z.literal("artifact-read"),
69
80
  reference: z.string().min(1),
@@ -95,7 +106,11 @@ var FileWriteInputSchema = z.object({
95
106
  content: z.string(),
96
107
  overwrite: z.boolean().default(false)
97
108
  }).strict();
98
- var FileWriteOutputSchema = z.object({ path: z.string().min(1), bytes: z.int().nonnegative() }).strict();
109
+ var FileWriteOutputSchema = z.object({
110
+ path: z.string().min(1),
111
+ bytes: z.int().nonnegative(),
112
+ createdDirectories: z.array(z.string().min(1))
113
+ }).strict();
99
114
  function createShellInputSchema(executableNames) {
100
115
  if (executableNames.length === 0) {
101
116
  throw new Error("A command tool requires at least one executable alias");
@@ -132,6 +147,7 @@ var DEFAULT_LIMITS = {
132
147
  maxShellArgumentBytes: 65536,
133
148
  maxShellOutputBytes: 262144,
134
149
  maxArtifactBytes: 4194304,
150
+ maxListEntries: 500,
135
151
  maxSearchResults: 100,
136
152
  maxSearchFiles: 1e4,
137
153
  maxSearchDepth: 32,
@@ -149,23 +165,89 @@ import { realpath as realpath2 } from "node:fs/promises";
149
165
  // src/agent-runtime/coding-tool-paths.ts
150
166
  import { lstat, realpath } from "node:fs/promises";
151
167
  import path from "node:path";
168
+
169
+ // src/agent-runtime/coding-tool-refusals.ts
170
+ var REFUSAL_STATUS = {
171
+ BAD_REQUEST: 400,
172
+ FORBIDDEN: 403,
173
+ NOT_FOUND: 404,
174
+ CONFLICT: 409
175
+ };
176
+ function codingRefusal(code, message, options = {}) {
177
+ throw new AppError(code, message, REFUSAL_STATUS[code], { message, ...options.details }, options.hint);
178
+ }
179
+ function codingPathRefusal(code, message, path, options = {}) {
180
+ codingRefusal(code, message, {
181
+ details: { path, ...options.details },
182
+ ...options.hint && { hint: options.hint }
183
+ });
184
+ }
185
+ function refuseMissingCodingPath(error, relative) {
186
+ const code = error?.code;
187
+ if (error instanceof Error && error.message.includes("is not a regular file")) {
188
+ codingPathRefusal("BAD_REQUEST", "This path is not a regular file", relative, {
189
+ hint: "Use list_directory to see what is here."
190
+ });
191
+ }
192
+ if (code === "ENOENT") {
193
+ codingPathRefusal("NOT_FOUND", "This path does not exist", relative, {
194
+ hint: "Use list_directory or glob to find the right path."
195
+ });
196
+ }
197
+ if (code === "ENOTDIR") {
198
+ codingPathRefusal("BAD_REQUEST", "A segment of this path is not a directory", relative, {
199
+ hint: "Check the path with list_directory."
200
+ });
201
+ }
202
+ if (code === "ELOOP") {
203
+ codingPathRefusal("FORBIDDEN", "This path is a symlink, which is not followed", relative, {
204
+ hint: "Read or write the target directly, by its own workspace-relative path."
205
+ });
206
+ }
207
+ if (code === "EISDIR") {
208
+ codingPathRefusal("BAD_REQUEST", "This path is not a regular file", relative, {
209
+ hint: "Use list_directory to see what is here."
210
+ });
211
+ }
212
+ throw error;
213
+ }
214
+
215
+ // src/agent-runtime/coding-tool-paths.ts
152
216
  function within(root, target) {
153
217
  const relative = path.relative(root, target);
154
218
  return relative === "" || !relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative);
155
219
  }
156
220
  function inputPath(root, requested, maxPathBytes) {
157
221
  if (new TextEncoder().encode(requested).byteLength > maxPathBytes) {
158
- throw new Error("Coding tool path exceeds maxPathBytes");
222
+ codingRefusal("BAD_REQUEST", `Path exceeds the ${maxPathBytes}-byte limit`, {
223
+ details: { maxPathBytes },
224
+ hint: "Use a shorter workspace-relative path."
225
+ });
226
+ }
227
+ if (path.isAbsolute(requested)) {
228
+ codingRefusal("BAD_REQUEST", "Paths must be relative to the workspace root", {
229
+ details: { path: requested },
230
+ hint: "Drop the leading slash and pass a path relative to the workspace root."
231
+ });
159
232
  }
160
- if (path.isAbsolute(requested))
161
- throw new Error("Coding tool paths must be relative");
162
233
  const resolved = path.resolve(root, requested);
163
- if (!within(root, resolved))
164
- throw new Error("Coding tool path escapes its root");
234
+ if (!within(root, resolved)) {
235
+ codingRefusal("FORBIDDEN", "Path escapes the workspace root", {
236
+ details: { path: requested },
237
+ hint: "Stay inside the workspace; `..` segments that leave the root are refused."
238
+ });
239
+ }
165
240
  return resolved;
166
241
  }
167
242
  function boundedCodingRelativePath(requested, maxPathBytes) {
168
243
  inputPath(path.parse(process.cwd()).root, requested, maxPathBytes);
244
+ const segments = requested.split(/[\\/]/u);
245
+ if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
246
+ codingRefusal("FORBIDDEN", "Path segments must name real entries under the root", {
247
+ details: { path: requested },
248
+ hint: "Pass a plain workspace-relative path — `.`, `..` and empty segments are refused."
249
+ });
250
+ }
169
251
  return requested;
170
252
  }
171
253
  async function existingCodingPath(root, requested, maxPathBytes) {
@@ -237,7 +319,7 @@ function createFileCodingTools(config, limits) {
237
319
  handler: async ({ input }) => {
238
320
  const root = await realpath2(config.root);
239
321
  const relative = boundedCodingRelativePath(input.path, limits.maxPathBytes);
240
- const handle = await openContainedFile(root, relative);
322
+ const handle = await openContainedFile(root, relative).catch((error) => refuseMissingCodingPath(error, relative));
241
323
  const maximum = Math.min(input.maxBytes ?? limits.maxReadBytes, limits.maxReadBytes);
242
324
  let selected;
243
325
  let size;
@@ -246,8 +328,11 @@ function createFileCodingTools(config, limits) {
246
328
  await authorizeCodingTool(config, { operation: "read", path: relative });
247
329
  await assertContainedFileCurrent(root, relative, handle);
248
330
  const metadata = await handle.stat();
249
- if (!metadata.isFile())
250
- throw new Error("Coding read path is not a regular file");
331
+ if (!metadata.isFile()) {
332
+ codingPathRefusal("BAD_REQUEST", "This path is not a regular file", relative, {
333
+ hint: "Use list_directory to see what is here."
334
+ });
335
+ }
251
336
  size = metadata.size;
252
337
  const length = Math.min(maximum, Math.max(0, size - input.offset));
253
338
  selected = Buffer.alloc(length);
@@ -281,21 +366,46 @@ function createFileCodingTools(config, limits) {
281
366
  const root = await realpath2(config.root);
282
367
  const relative = boundedCodingRelativePath(input.path, limits.maxPathBytes);
283
368
  const bytes = Buffer.byteLength(input.content);
284
- if (bytes > limits.maxWriteBytes)
285
- throw new Error("Coding tool write exceeds maxWriteBytes");
286
- const parent = await openContainedParent(root, relative);
287
- try {
288
- await authorizeCodingTool(config, {
289
- operation: "write",
290
- path: relative,
291
- bytes,
292
- overwrite: input.overwrite
369
+ if (bytes > limits.maxWriteBytes) {
370
+ codingPathRefusal("BAD_REQUEST", `The content is ${bytes} bytes, over the ${limits.maxWriteBytes}-byte limit`, relative, {
371
+ details: { bytes, maxWriteBytes: limits.maxWriteBytes },
372
+ hint: "Write a smaller file, or split it."
293
373
  });
374
+ }
375
+ const createsDirectories = await missingContainedDirectories(root, relative).catch((error) => {
376
+ if (error.code === "ENOTDIR") {
377
+ codingPathRefusal("CONFLICT", "A segment of this path is not a directory", relative, {
378
+ hint: "Write under a different path, or remove the file blocking it."
379
+ });
380
+ }
381
+ throw error;
382
+ });
383
+ await authorizeCodingTool(config, {
384
+ operation: "write",
385
+ path: relative,
386
+ bytes,
387
+ overwrite: input.overwrite,
388
+ createsDirectories
389
+ });
390
+ const parent = await openContainedParent(root, relative, { create: true }).catch((error) => {
391
+ const code = error.code;
392
+ if (code === "ENOTDIR" || error instanceof Error && error.message.includes("ancestor is not a directory")) {
393
+ codingPathRefusal("CONFLICT", "A segment of this path is not a directory", relative, {
394
+ details: { createsDirectories },
395
+ hint: "Write under a different path, or remove the file blocking it."
396
+ });
397
+ }
398
+ throw error;
399
+ });
400
+ try {
294
401
  await assertContainedParentCurrent(root, relative, parent.handle);
402
+ const current = await containedEntryMetadata(parent);
295
403
  if (input.overwrite) {
296
- const current = await containedEntryMetadata(parent);
297
- if (current?.isSymbolicLink())
298
- throw new Error("Coding tools do not overwrite symlinks");
404
+ if (current?.isSymbolicLink()) {
405
+ codingPathRefusal("FORBIDDEN", "This path is a symlink", relative, {
406
+ hint: "Coding tools never write through symlinks; write to the target directly."
407
+ });
408
+ }
299
409
  await writeContainedFile({
300
410
  parent,
301
411
  content: input.content,
@@ -303,19 +413,23 @@ function createFileCodingTools(config, limits) {
303
413
  ...current && { mode: current.mode }
304
414
  });
305
415
  } else {
416
+ if (current) {
417
+ codingPathRefusal("CONFLICT", "This file already exists", relative, {
418
+ hint: "Pass overwrite: true to replace it, or edit_file to change part of it."
419
+ });
420
+ }
306
421
  await writeContainedFile({ parent, content: input.content, replace: false });
307
422
  }
308
423
  } finally {
309
424
  await parent.handle.close();
310
425
  }
311
- return { path: relative, bytes };
426
+ return { path: relative, bytes, createdDirectories: createsDirectories };
312
427
  }
313
428
  });
314
429
  return [read, write];
315
430
  }
316
431
 
317
- // src/agent-runtime/coding-tool-search-patch.ts
318
- import { createHash as createHash2 } from "node:crypto";
432
+ // src/agent-runtime/coding-tool-listing.ts
319
433
  import { realpath as realpath3 } from "node:fs/promises";
320
434
  import path2 from "node:path";
321
435
  import { z as z2 } from "zod";
@@ -328,29 +442,190 @@ var DEFAULT_EXCLUDED_DIRECTORIES = [
328
442
  "dist",
329
443
  "node_modules"
330
444
  ];
331
- var SearchInputSchema = z2.object({ query: z2.string().min(1), mode: z2.enum(["path", "content"]).default("content") }).strict();
332
- var SearchOutputSchema = z2.object({
333
- matches: z2.array(z2.object({ path: z2.string().min(1), line: z2.int().positive().optional() }).strict()),
445
+ var ListInputSchema = z2.object({ path: z2.string().min(1).default(".") }).strict();
446
+ var ListOutputSchema = z2.object({
447
+ path: z2.string().min(1),
448
+ entries: z2.array(z2.object({
449
+ name: z2.string().min(1),
450
+ kind: z2.enum(["file", "directory", "symlink", "other"]),
451
+ bytes: z2.int().nonnegative().optional(),
452
+ excluded: z2.boolean().optional()
453
+ }).strict()),
454
+ truncated: z2.boolean()
455
+ }).strict();
456
+ var GlobInputSchema = z2.object({ pattern: z2.string().min(1), path: z2.string().min(1).default(".") }).strict();
457
+ var GlobOutputSchema = z2.object({
458
+ paths: z2.array(z2.string().min(1)),
334
459
  truncated: z2.boolean(),
335
- scannedFiles: z2.int().nonnegative(),
460
+ resultTruncated: z2.boolean(),
336
461
  skippedDirectories: z2.int().nonnegative(),
337
- skippedSymlinks: z2.int().nonnegative()
462
+ scannedFiles: z2.int().nonnegative()
338
463
  }).strict();
339
- var PatchInputSchema = z2.object({
340
- path: z2.string().min(1),
341
- baseSha256: z2.string().length(64),
342
- oldText: z2.string().min(1),
343
- newText: z2.string(),
344
- replaceAll: z2.boolean().default(false),
345
- dryRun: z2.boolean().default(true)
464
+ function compileWorkspaceGlob(pattern) {
465
+ let source = "";
466
+ for (let index = 0;index < pattern.length; index += 1) {
467
+ const character = pattern[index];
468
+ if (character === "*") {
469
+ if (pattern[index + 1] === "*") {
470
+ if (pattern[index + 2] === "/") {
471
+ source += "(?:[^/]+/)*";
472
+ index += 2;
473
+ continue;
474
+ }
475
+ source += ".*";
476
+ index += 1;
477
+ continue;
478
+ }
479
+ source += "[^/]*";
480
+ continue;
481
+ }
482
+ if (character === "?") {
483
+ source += "[^/]";
484
+ continue;
485
+ }
486
+ source += character?.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") ?? "";
487
+ }
488
+ return new RegExp(`^${source}$`);
489
+ }
490
+ function createListingCodingTools(config, limits) {
491
+ const excluded = new Set(config.search?.excludeDirectories ?? DEFAULT_EXCLUDED_DIRECTORIES);
492
+ const list = defineRuntimeTool({
493
+ name: "list_directory",
494
+ description: "List the direct entries of one workspace directory.",
495
+ identity: { serviceName: "coding", action: "list-directory", method: "POST" },
496
+ input: ListInputSchema,
497
+ output: ListOutputSchema,
498
+ transports: ["AGENT"],
499
+ handler: async ({ input }) => {
500
+ const root = await realpath3(config.root);
501
+ const relative = input.path === "." ? "." : boundedCodingRelativePath(input.path, limits.maxPathBytes);
502
+ await authorizeCodingTool(config, { operation: "list", path: relative });
503
+ const listing = await listContainedDirectory(root, relative, limits.maxListEntries).catch((error) => {
504
+ if (error instanceof Error && error.message.includes("ancestor is not a directory")) {
505
+ codingPathRefusal("BAD_REQUEST", "This path is not a directory", relative, {
506
+ hint: "Pass a directory; use read_file for a file."
507
+ });
508
+ }
509
+ if (error.code === "ENOENT") {
510
+ codingPathRefusal("NOT_FOUND", "This directory does not exist", relative, {
511
+ hint: "List a parent directory to see what is there."
512
+ });
513
+ }
514
+ return refuseMissingCodingPath(error, relative);
515
+ });
516
+ return {
517
+ path: relative,
518
+ entries: listing.entries.map((entry) => ({
519
+ ...entry,
520
+ ...entry.kind === "directory" && excluded.has(entry.name) && { excluded: true }
521
+ })),
522
+ truncated: listing.truncated
523
+ };
524
+ }
525
+ });
526
+ const glob = defineRuntimeTool({
527
+ name: "glob",
528
+ description: "List workspace files matching a `**`/`*`/`?` pattern.",
529
+ identity: { serviceName: "coding", action: "glob", method: "POST" },
530
+ input: GlobInputSchema,
531
+ output: GlobOutputSchema,
532
+ transports: ["AGENT"],
533
+ handler: async ({ input }) => {
534
+ const relative = input.path === "." ? "." : boundedCodingRelativePath(input.path, limits.maxPathBytes);
535
+ await authorizeCodingTool(config, {
536
+ operation: "glob",
537
+ pattern: input.pattern,
538
+ path: relative
539
+ });
540
+ let matcher;
541
+ try {
542
+ matcher = compileWorkspaceGlob(input.pattern);
543
+ } catch {
544
+ codingRefusal("BAD_REQUEST", "This glob pattern could not be compiled", {
545
+ details: { pattern: input.pattern },
546
+ hint: "Use only `**`, `*` and `?`."
547
+ });
548
+ }
549
+ const root = await realpath3(config.root);
550
+ const scan = await scanContainedFiles({
551
+ root,
552
+ maxDepth: limits.maxSearchDepth,
553
+ maxFiles: limits.maxSearchFiles,
554
+ symlinks: "skip",
555
+ excludeDirectory: (directory) => directory.split(path2.sep).some((segment) => excluded.has(segment))
556
+ });
557
+ const prefix = relative === "." ? "" : `${relative}${path2.sep}`;
558
+ const matched = scan.files.filter((file) => file.relative.startsWith(prefix)).map((file) => file.relative).filter((candidate) => matcher.test(candidate.slice(prefix.length))).sort();
559
+ const paths = matched.slice(0, limits.maxSearchResults);
560
+ return {
561
+ paths,
562
+ truncated: scan.truncated,
563
+ resultTruncated: matched.length > paths.length,
564
+ skippedDirectories: scan.skippedDirectories,
565
+ scannedFiles: scan.files.length
566
+ };
567
+ }
568
+ });
569
+ return [list, glob];
570
+ }
571
+
572
+ // src/agent-runtime/coding-tool-search-patch.ts
573
+ import { createHash as createHash2 } from "node:crypto";
574
+ import { realpath as realpath4 } from "node:fs/promises";
575
+ import path3 from "node:path";
576
+ import { z as z3 } from "zod";
577
+ var SearchInputSchema = z3.object({
578
+ query: z3.string().min(1),
579
+ mode: z3.enum(["path", "content"]).default("content"),
580
+ regex: z3.boolean().default(false),
581
+ context: z3.int().nonnegative().max(10).default(0),
582
+ include: z3.string().min(1).optional()
346
583
  }).strict();
347
- var PatchOutputSchema = z2.object({
348
- path: z2.string().min(1),
349
- baseSha256: z2.string().length(64),
350
- resultSha256: z2.string().length(64),
351
- replacements: z2.int().positive(),
352
- bytes: z2.int().nonnegative(),
353
- applied: z2.boolean()
584
+ var SearchOutputSchema = z3.object({
585
+ matches: z3.array(z3.object({
586
+ path: z3.string().min(1),
587
+ line: z3.int().positive().optional(),
588
+ text: z3.string().optional(),
589
+ before: z3.array(z3.string()).optional(),
590
+ after: z3.array(z3.string()).optional()
591
+ }).strict()),
592
+ truncated: z3.boolean(),
593
+ scannedFiles: z3.int().nonnegative(),
594
+ skippedDirectories: z3.int().nonnegative(),
595
+ skippedSymlinks: z3.int().nonnegative()
596
+ }).strict();
597
+ function compileSearchRegex(pattern) {
598
+ if (/\\[1-9]/.test(pattern) || /\(\?<?[=!]/.test(pattern)) {
599
+ codingRefusal("BAD_REQUEST", "This pattern uses a construct search does not allow", {
600
+ details: { pattern },
601
+ hint: "Backreferences and lookaround are refused because they cannot be bounded. Rewrite without them."
602
+ });
603
+ }
604
+ try {
605
+ return new RegExp(pattern);
606
+ } catch (error) {
607
+ codingRefusal("BAD_REQUEST", "This pattern is not a valid regular expression", {
608
+ details: { pattern, reason: error instanceof Error ? error.message : "unknown" },
609
+ hint: "Fix the expression, or pass regex: false to search for it literally."
610
+ });
611
+ }
612
+ }
613
+ var MAX_SEARCHED_LINE_BYTES = 8192;
614
+ var EditInputSchema = z3.object({
615
+ path: z3.string().min(1),
616
+ oldText: z3.string().min(1),
617
+ newText: z3.string(),
618
+ replaceAll: z3.boolean().default(false),
619
+ expectedSha256: z3.string().length(64).optional(),
620
+ dryRun: z3.boolean().default(false)
621
+ }).strict();
622
+ var EditOutputSchema = z3.object({
623
+ path: z3.string().min(1),
624
+ baseSha256: z3.string().length(64),
625
+ sha256: z3.string().length(64),
626
+ replacements: z3.int().positive(),
627
+ bytes: z3.int().nonnegative(),
628
+ applied: z3.boolean()
354
629
  }).strict();
355
630
  function sha256(value) {
356
631
  return createHash2("sha256").update(value).digest("hex");
@@ -369,7 +644,7 @@ function createSearchAndPatchCodingTools(config, limits) {
369
644
  query: input.query,
370
645
  mode: input.mode
371
646
  });
372
- const root = await realpath3(config.root);
647
+ const root = await realpath4(config.root);
373
648
  const excluded = new Set(config.search?.excludeDirectories ?? DEFAULT_EXCLUDED_DIRECTORIES);
374
649
  const scan = await scanContainedFiles({
375
650
  root,
@@ -380,13 +655,26 @@ function createSearchAndPatchCodingTools(config, limits) {
380
655
  readMaxBytes: limits.maxReadBytes,
381
656
  skipUnreadable: true
382
657
  },
383
- excludeDirectory: (relative) => relative.split(path2.sep).some((segment) => excluded.has(segment))
658
+ excludeDirectory: (relative) => relative.split(path3.sep).some((segment) => excluded.has(segment))
384
659
  });
660
+ const matcher = input.regex ? compileSearchRegex(input.query) : null;
661
+ const included = input.include ? compileWorkspaceGlob(input.include) : null;
662
+ const hit = (line) => {
663
+ if (line.length > MAX_SEARCHED_LINE_BYTES)
664
+ return false;
665
+ if (matcher) {
666
+ matcher.lastIndex = 0;
667
+ return matcher.test(line);
668
+ }
669
+ return line.includes(input.query);
670
+ };
385
671
  const matches = [];
386
672
  let truncated = false;
387
673
  for (const file of scan.files) {
674
+ if (included && !included.test(file.relative))
675
+ continue;
388
676
  if (input.mode === "path") {
389
- if (file.relative.includes(input.query))
677
+ if (hit(file.relative))
390
678
  matches.push({ path: file.relative });
391
679
  } else {
392
680
  const text = file.content?.text;
@@ -395,11 +683,20 @@ function createSearchAndPatchCodingTools(config, limits) {
395
683
  const lines = text.split(`
396
684
  `);
397
685
  for (let index = 0;index < lines.length; index += 1) {
398
- if (lines[index]?.includes(input.query)) {
399
- matches.push({ path: file.relative, line: index + 1 });
400
- if (matches.length >= limits.maxSearchResults)
401
- break;
402
- }
686
+ const line = lines[index];
687
+ if (line === undefined || !hit(line))
688
+ continue;
689
+ matches.push({
690
+ path: file.relative,
691
+ line: index + 1,
692
+ text: line,
693
+ ...input.context > 0 && {
694
+ before: lines.slice(Math.max(0, index - input.context), index),
695
+ after: lines.slice(index + 1, index + 1 + input.context)
696
+ }
697
+ });
698
+ if (matches.length >= limits.maxSearchResults)
699
+ break;
403
700
  }
404
701
  }
405
702
  if (matches.length >= limits.maxSearchResults) {
@@ -416,78 +713,92 @@ function createSearchAndPatchCodingTools(config, limits) {
416
713
  };
417
714
  }
418
715
  });
419
- const patch = defineRuntimeTool({
420
- name: "apply_patch",
421
- description: "Dry-run or atomically apply one exact guarded UTF-8 file patch.",
422
- identity: { serviceName: "coding", action: "patch-file", method: "POST" },
423
- input: PatchInputSchema,
424
- output: PatchOutputSchema,
716
+ const edit = defineRuntimeTool({
717
+ name: "edit_file",
718
+ description: "Replace an exact snippet in one workspace file.",
719
+ identity: { serviceName: "coding", action: "edit-file", method: "POST" },
720
+ input: EditInputSchema,
721
+ output: EditOutputSchema,
425
722
  transports: ["AGENT"],
426
723
  handler: async ({ input }) => {
427
- const root = await realpath3(config.root);
724
+ const root = await realpath4(config.root);
428
725
  const relative = boundedCodingRelativePath(input.path, limits.maxPathBytes);
429
- const parent = await openContainedParent(root, relative);
726
+ const parent = await openContainedParent(root, relative).catch((error) => refuseMissingCodingPath(error, relative));
430
727
  try {
431
- const initial = await openContainedParentFile(parent);
432
- const read = await readContainedUtf8Handle(initial, limits.maxWriteBytes).finally(() => initial.close());
433
- const source = read.text;
434
- const baseSha256 = sha256(source);
435
- if (baseSha256 !== input.baseSha256)
436
- conflict("Coding patch base digest is stale");
437
- const count = textOccurrences(source, input.oldText);
438
- if (count === 0)
439
- throw new Error("Coding patch text was not found");
440
- if (!input.replaceAll && count !== 1)
441
- throw new Error("Coding patch text is not unique");
442
- const replacements = input.replaceAll ? count : 1;
443
- const changed = input.replaceAll ? source.replaceAll(input.oldText, input.newText) : source.replace(input.oldText, input.newText);
444
- const bytes = Buffer.byteLength(changed);
445
- const resultSha256 = sha256(changed);
446
- if (bytes > limits.maxWriteBytes)
447
- throw new Error("Coding patch exceeds maxWriteBytes");
448
- await authorizeCodingTool(config, {
449
- operation: "patch",
450
- path: relative,
451
- baseSha256,
452
- resultSha256,
453
- resultBytes: bytes,
454
- replacements,
455
- dryRun: input.dryRun
456
- });
457
- await assertContainedParentCurrent(root, relative, parent.handle);
458
- if (!input.dryRun) {
459
- await withCodingPathLock(`${root}:${relative}`, async () => {
460
- const currentHandle = await openContainedParentFile(parent);
461
- const current = await readContainedUtf8Handle(currentHandle, limits.maxWriteBytes).finally(() => currentHandle.close());
462
- if (sha256(current.text) !== baseSha256)
463
- conflict("Coding patch base digest became stale before apply");
728
+ return await withCodingPathLock(`${root}:${relative}`, async () => {
729
+ const handle = await openContainedParentFile(parent).catch((error) => refuseMissingCodingPath(error, relative));
730
+ const read = await readContainedUtf8Handle(handle, limits.maxWriteBytes).finally(() => handle.close());
731
+ const source = read.text;
732
+ const baseSha256 = sha256(source);
733
+ if (input.expectedSha256 !== undefined && baseSha256 !== input.expectedSha256) {
734
+ conflict("The file changed since it was read", {
735
+ message: "The file changed since it was read",
736
+ path: relative,
737
+ expectedSha256: input.expectedSha256,
738
+ actualSha256: baseSha256
739
+ });
740
+ }
741
+ const count = textOccurrences(source, input.oldText);
742
+ if (count === 0) {
743
+ codingPathRefusal("NOT_FOUND", "oldText does not appear in this file", relative, {
744
+ details: { occurrences: 0 },
745
+ hint: "Read the file and copy the snippet exactly, including indentation."
746
+ });
747
+ }
748
+ if (!input.replaceAll && count !== 1) {
749
+ codingPathRefusal("CONFLICT", `oldText appears ${count} times, so the edit is ambiguous`, relative, {
750
+ details: { occurrences: count },
751
+ hint: "Lengthen the snippet until it is unique, or pass replaceAll: true."
752
+ });
753
+ }
754
+ const replacements = input.replaceAll ? count : 1;
755
+ const changed = input.replaceAll ? source.replaceAll(input.oldText, input.newText) : source.replace(input.oldText, input.newText);
756
+ const bytes = Buffer.byteLength(changed);
757
+ const resultSha256 = sha256(changed);
758
+ if (bytes > limits.maxWriteBytes) {
759
+ codingPathRefusal("BAD_REQUEST", `The result would be ${bytes} bytes, over the ${limits.maxWriteBytes}-byte limit`, relative, {
760
+ details: { bytes, maxWriteBytes: limits.maxWriteBytes },
761
+ hint: "Make a smaller edit, or split the file."
762
+ });
763
+ }
764
+ await authorizeCodingTool(config, {
765
+ operation: "edit",
766
+ path: relative,
767
+ baseSha256,
768
+ resultSha256,
769
+ resultBytes: bytes,
770
+ replacements,
771
+ dryRun: input.dryRun
772
+ });
773
+ await assertContainedParentCurrent(root, relative, parent.handle);
774
+ if (!input.dryRun) {
464
775
  await writeContainedFile({
465
776
  parent,
466
777
  content: changed,
467
778
  replace: true,
468
- mode: current.mode
779
+ mode: read.mode
469
780
  });
470
- });
471
- }
472
- return {
473
- path: relative,
474
- baseSha256,
475
- resultSha256,
476
- replacements,
477
- bytes,
478
- applied: !input.dryRun
479
- };
781
+ }
782
+ return {
783
+ path: relative,
784
+ baseSha256,
785
+ sha256: resultSha256,
786
+ replacements,
787
+ bytes,
788
+ applied: !input.dryRun
789
+ };
790
+ });
480
791
  } finally {
481
792
  await parent.handle.close();
482
793
  }
483
794
  }
484
795
  });
485
- return [search, patch];
796
+ return [search, edit];
486
797
  }
487
798
 
488
799
  // src/agent-runtime/coding-tool-shell.ts
489
800
  import { spawn } from "node:child_process";
490
- import { realpath as realpath4, stat } from "node:fs/promises";
801
+ import { realpath as realpath5, stat } from "node:fs/promises";
491
802
  async function runShell(input) {
492
803
  const stdoutHeader = Buffer.from(`--- stdout ---
493
804
  `);
@@ -654,7 +965,7 @@ function createShellCodingTool(config, limits) {
654
965
  const executable = executables[input.executable];
655
966
  if (!executable)
656
967
  throw new Error("Coding tool executable is not declared");
657
- const root = await realpath4(config.root);
968
+ const root = await realpath5(config.root);
658
969
  const cwd = await existingCodingPath(root, input.cwd, limits.maxPathBytes);
659
970
  if (!(await stat(cwd.absolute)).isDirectory()) {
660
971
  throw new Error("Coding tool cwd is not a directory");
@@ -684,7 +995,9 @@ function createShellCodingTool(config, limits) {
684
995
 
685
996
  // src/agent-runtime/coding-tools.ts
686
997
  var AGENT_CODING_TOOL_NAMES = {
687
- applyPatch: "apply_patch",
998
+ editFile: "edit_file",
999
+ glob: "glob",
1000
+ listDirectory: "list_directory",
688
1001
  readFile: "read_file",
689
1002
  readOutput: "read_output",
690
1003
  runCommand: "run_command",
@@ -692,10 +1005,10 @@ var AGENT_CODING_TOOL_NAMES = {
692
1005
  writeFile: "write_file"
693
1006
  };
694
1007
  function createAgentCodingTools(config) {
695
- if (!path3.isAbsolute(config.root))
1008
+ if (!path4.isAbsolute(config.root))
696
1009
  throw new Error("Coding tool root must be absolute");
697
1010
  for (const executable of Object.values(config.executables ?? {})) {
698
- if (!path3.isAbsolute(executable)) {
1011
+ if (!path4.isAbsolute(executable)) {
699
1012
  throw new Error("Coding tool executables must be absolute paths");
700
1013
  }
701
1014
  }
@@ -707,17 +1020,17 @@ function createAgentCodingTools(config) {
707
1020
  name: AGENT_CODING_TOOL_NAMES.readOutput,
708
1021
  description: "Read a bounded byte slice from one opaque coding output artifact.",
709
1022
  identity: { serviceName: "coding", action: "read-artifact", method: "POST" },
710
- input: z3.object({
711
- reference: z3.string().min(1),
712
- offset: z3.int().nonnegative().default(0),
713
- maxBytes: z3.int().positive().optional()
1023
+ input: z4.object({
1024
+ reference: z4.string().min(1),
1025
+ offset: z4.int().nonnegative().default(0),
1026
+ maxBytes: z4.int().positive().optional()
714
1027
  }).strict(),
715
- output: z3.object({
716
- reference: z3.string().min(1),
717
- offset: z3.int().nonnegative(),
718
- text: z3.string(),
719
- bytes: z3.int().nonnegative(),
720
- totalBytes: z3.int().nonnegative().optional()
1028
+ output: z4.object({
1029
+ reference: z4.string().min(1),
1030
+ offset: z4.int().nonnegative(),
1031
+ text: z4.string(),
1032
+ bytes: z4.int().nonnegative(),
1033
+ totalBytes: z4.int().nonnegative().optional()
721
1034
  }).strict(),
722
1035
  transports: ["AGENT"],
723
1036
  handler: async ({ input }) => {
@@ -751,6 +1064,7 @@ function createAgentCodingTools(config) {
751
1064
  const commandTools = Object.keys(config.executables ?? {}).length ? [createShellCodingTool(config, limits)] : [];
752
1065
  return [
753
1066
  ...createFileCodingTools(config, limits),
1067
+ ...createListingCodingTools(config, limits),
754
1068
  ...createSearchAndPatchCodingTools(config, limits),
755
1069
  ...commandTools,
756
1070
  ...artifactTools