stitchkit 0.88.0 → 0.89.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 (164) hide show
  1. package/CHANGELOG.md +126 -0
  2. package/dist/agent-runtime/child-blocking.d.ts +17 -0
  3. package/dist/agent-runtime/child-blocking.d.ts.map +1 -0
  4. package/dist/agent-runtime/child-budget-policy.d.ts +23 -0
  5. package/dist/agent-runtime/child-budget-policy.d.ts.map +1 -0
  6. package/dist/agent-runtime/children-contract.d.ts +155 -0
  7. package/dist/agent-runtime/children-contract.d.ts.map +1 -0
  8. package/dist/agent-runtime/children.d.ts +4 -99
  9. package/dist/agent-runtime/children.d.ts.map +1 -1
  10. package/dist/agent-runtime/coding-shell-process.d.ts +33 -0
  11. package/dist/agent-runtime/coding-shell-process.d.ts.map +1 -0
  12. package/dist/agent-runtime/coding-tool-contract.d.ts +18 -0
  13. package/dist/agent-runtime/coding-tool-contract.d.ts.map +1 -1
  14. package/dist/agent-runtime/coding-tool-files.d.ts.map +1 -1
  15. package/dist/agent-runtime/coding-tool-listing.d.ts.map +1 -1
  16. package/dist/agent-runtime/coding-tool-search-patch.d.ts.map +1 -1
  17. package/dist/agent-runtime/coding-tool-shell.d.ts.map +1 -1
  18. package/dist/agent-runtime/coding-tool-utf8.d.ts +28 -0
  19. package/dist/agent-runtime/coding-tool-utf8.d.ts.map +1 -0
  20. package/dist/agent-runtime/contained-files.d.ts +20 -0
  21. package/dist/agent-runtime/contained-files.d.ts.map +1 -1
  22. package/dist/agent-runtime/durability-contract.d.ts +176 -0
  23. package/dist/agent-runtime/durability-contract.d.ts.map +1 -0
  24. package/dist/agent-runtime/durability-json.d.ts +3 -0
  25. package/dist/agent-runtime/durability-json.d.ts.map +1 -0
  26. package/dist/agent-runtime/durability-ledger.d.ts +16 -0
  27. package/dist/agent-runtime/durability-ledger.d.ts.map +1 -0
  28. package/dist/agent-runtime/durability-scheduler.d.ts +29 -0
  29. package/dist/agent-runtime/durability-scheduler.d.ts.map +1 -0
  30. package/dist/agent-runtime/durability.d.ts +14 -0
  31. package/dist/agent-runtime/durability.d.ts.map +1 -0
  32. package/dist/agent-runtime/event-search.d.ts.map +1 -1
  33. package/dist/agent-runtime/harness-contract.d.ts +37 -0
  34. package/dist/agent-runtime/harness-contract.d.ts.map +1 -1
  35. package/dist/agent-runtime/harness.d.ts +8 -0
  36. package/dist/agent-runtime/harness.d.ts.map +1 -1
  37. package/dist/agent-runtime/prompt.d.ts +23 -0
  38. package/dist/agent-runtime/prompt.d.ts.map +1 -1
  39. package/dist/agent-runtime/run-execution.d.ts.map +1 -1
  40. package/dist/agent-runtime/runtime-internals.d.ts +4 -0
  41. package/dist/agent-runtime/runtime-internals.d.ts.map +1 -1
  42. package/dist/agent-runtime/runtime.d.ts +13 -0
  43. package/dist/agent-runtime/runtime.d.ts.map +1 -1
  44. package/dist/agent-runtime/sandbox-broker.d.ts +7 -0
  45. package/dist/agent-runtime/sandbox-broker.d.ts.map +1 -0
  46. package/dist/agent-runtime/sandbox-bubblewrap-args.d.ts +9 -0
  47. package/dist/agent-runtime/sandbox-bubblewrap-args.d.ts.map +1 -0
  48. package/dist/agent-runtime/sandbox-bubblewrap-storage.d.ts +22 -0
  49. package/dist/agent-runtime/sandbox-bubblewrap-storage.d.ts.map +1 -0
  50. package/dist/agent-runtime/sandbox-bubblewrap.d.ts +14 -0
  51. package/dist/agent-runtime/sandbox-bubblewrap.d.ts.map +1 -0
  52. package/dist/agent-runtime/sandbox-coding-adapter.d.ts +15 -0
  53. package/dist/agent-runtime/sandbox-coding-adapter.d.ts.map +1 -0
  54. package/dist/agent-runtime/sandbox-coding.d.ts +15 -0
  55. package/dist/agent-runtime/sandbox-coding.d.ts.map +1 -0
  56. package/dist/agent-runtime/sandbox-contract.d.ts +93 -0
  57. package/dist/agent-runtime/sandbox-contract.d.ts.map +1 -0
  58. package/dist/agent-runtime/sandbox-process-owner.d.ts +11 -0
  59. package/dist/agent-runtime/sandbox-process-owner.d.ts.map +1 -0
  60. package/dist/agent-runtime/sandbox-process.d.ts +8 -0
  61. package/dist/agent-runtime/sandbox-process.d.ts.map +1 -0
  62. package/dist/agent-runtime/sandbox-session.d.ts +5 -0
  63. package/dist/agent-runtime/sandbox-session.d.ts.map +1 -0
  64. package/dist/agent-runtime/sandbox.d.ts +19 -0
  65. package/dist/agent-runtime/sandbox.d.ts.map +1 -1
  66. package/dist/agent-runtime/sqlite-purge.d.ts.map +1 -1
  67. package/dist/agent-runtime/sqlite.d.ts.map +1 -1
  68. package/dist/agent-runtime/store-driver.d.ts +127 -0
  69. package/dist/agent-runtime/store-driver.d.ts.map +1 -1
  70. package/dist/agent-runtime/store-events.d.ts +104 -0
  71. package/dist/agent-runtime/store-events.d.ts.map +1 -1
  72. package/dist/agent-runtime/store-migrations/v2-to-v3.d.ts +14 -0
  73. package/dist/agent-runtime/store-migrations/v2-to-v3.d.ts.map +1 -0
  74. package/dist/agent-runtime/store.d.ts +115 -0
  75. package/dist/agent-runtime/store.d.ts.map +1 -1
  76. package/dist/agent-runtime-coding-tools.js +11 -1247
  77. package/dist/agent-runtime-harness.d.ts +1 -1
  78. package/dist/agent-runtime-harness.d.ts.map +1 -1
  79. package/dist/agent-runtime-harness.js +65 -15
  80. package/dist/agent-runtime-openrouter.d.ts +3 -12
  81. package/dist/agent-runtime-openrouter.d.ts.map +1 -1
  82. package/dist/agent-runtime-openrouter.js +10 -7
  83. package/dist/agent-runtime-sandbox.d.ts +5 -0
  84. package/dist/agent-runtime-sandbox.d.ts.map +1 -0
  85. package/dist/agent-runtime-sandbox.js +797 -0
  86. package/dist/agent-runtime-sqlite-bun.js +3 -3
  87. package/dist/agent-runtime-sqlite-node.js +3 -3
  88. package/dist/agent-runtime.d.ts +5 -4
  89. package/dist/agent-runtime.d.ts.map +1 -1
  90. package/dist/agent-runtime.js +259 -65
  91. package/dist/application.js +26 -26
  92. package/dist/cli.js +8 -8
  93. package/dist/{index-rcgsnrr6.js → index-31jw8fjq.js} +3 -3
  94. package/dist/{index-9hw9eq48.js → index-7601qfg1.js} +14 -14
  95. package/dist/{index-3xnq72rz.js → index-7rkhw9ec.js} +14 -1
  96. package/dist/{index-0vfdh685.js → index-8c3a7m4g.js} +1 -1
  97. package/dist/{index-t7gh5w3v.js → index-bn2cjajt.js} +27 -6
  98. package/dist/index-cp20z0n6.js +1351 -0
  99. package/dist/{index-qp3vshvc.js → index-fadnxwna.js} +87 -5
  100. package/dist/{index-v1qk37y9.js → index-hrbajay7.js} +106 -4
  101. package/dist/{index-p86mxmpp.js → index-hw2chdvy.js} +646 -43
  102. package/dist/{index-0ydchz76.js → index-mnm4qp0p.js} +5 -5
  103. package/dist/{index-1szetq6f.js → index-q3ve2d1k.js} +30 -8
  104. package/dist/{index-6krwcdcf.js → index-zwkgfq1s.js} +95 -85
  105. package/dist/{index-wn9cb82y.js → index-zyvby92n.js} +12 -9
  106. package/dist/index.js +13 -13
  107. package/dist/server/index.js +9 -9
  108. package/dist/testing/agent-store-conformance.d.ts.map +1 -1
  109. package/dist/testing.js +57 -23
  110. package/dist/tool-invoker.js +6 -6
  111. package/dist/tools/agent.d.ts +6 -0
  112. package/dist/tools/agent.d.ts.map +1 -1
  113. package/dist/tools/connections/define.d.ts +6 -0
  114. package/dist/tools/connections/define.d.ts.map +1 -0
  115. package/dist/tools/connections/errors.d.ts +50 -0
  116. package/dist/tools/connections/errors.d.ts.map +1 -0
  117. package/dist/tools/connections/http.d.ts +3 -0
  118. package/dist/tools/connections/http.d.ts.map +1 -0
  119. package/dist/tools/connections/index.d.ts +19 -0
  120. package/dist/tools/connections/index.d.ts.map +1 -0
  121. package/dist/tools/connections/index.js +1047 -0
  122. package/dist/tools/connections/instance.d.ts +3 -0
  123. package/dist/tools/connections/instance.d.ts.map +1 -0
  124. package/dist/tools/connections/json-rpc.d.ts +8 -0
  125. package/dist/tools/connections/json-rpc.d.ts.map +1 -0
  126. package/dist/tools/connections/limits.d.ts +36 -0
  127. package/dist/tools/connections/limits.d.ts.map +1 -0
  128. package/dist/tools/connections/mcp-client.d.ts +44 -0
  129. package/dist/tools/connections/mcp-client.d.ts.map +1 -0
  130. package/dist/tools/connections/mcp.d.ts +5 -0
  131. package/dist/tools/connections/mcp.d.ts.map +1 -0
  132. package/dist/tools/connections/mount.d.ts +12 -0
  133. package/dist/tools/connections/mount.d.ts.map +1 -0
  134. package/dist/tools/connections/openapi-document.d.ts +4 -0
  135. package/dist/tools/connections/openapi-document.d.ts.map +1 -0
  136. package/dist/tools/connections/openapi.d.ts +5 -0
  137. package/dist/tools/connections/openapi.d.ts.map +1 -0
  138. package/dist/tools/connections/runtime.d.ts +29 -0
  139. package/dist/tools/connections/runtime.d.ts.map +1 -0
  140. package/dist/tools/connections/schema-budget.d.ts +8 -0
  141. package/dist/tools/connections/schema-budget.d.ts.map +1 -0
  142. package/dist/tools/connections/sse-frames.d.ts +7 -0
  143. package/dist/tools/connections/sse-frames.d.ts.map +1 -0
  144. package/dist/tools/connections/sse-session.d.ts +26 -0
  145. package/dist/tools/connections/sse-session.d.ts.map +1 -0
  146. package/dist/tools/connections/ssrf.d.ts +15 -0
  147. package/dist/tools/connections/ssrf.d.ts.map +1 -0
  148. package/dist/tools/connections/types.d.ts +60 -0
  149. package/dist/tools/connections/types.d.ts.map +1 -0
  150. package/dist/tools/durability-context.d.ts +7 -0
  151. package/dist/tools/durability-context.d.ts.map +1 -0
  152. package/dist/tools/execute.d.ts +2 -0
  153. package/dist/tools/execute.d.ts.map +1 -1
  154. package/dist/tools/registry.d.ts +38 -0
  155. package/dist/tools/registry.d.ts.map +1 -0
  156. package/dist/tools/runtime-tool.d.ts +4 -0
  157. package/dist/tools/runtime-tool.d.ts.map +1 -1
  158. package/dist/tools.d.ts +1 -0
  159. package/dist/tools.d.ts.map +1 -1
  160. package/dist/tools.js +83 -26
  161. package/llms-full.txt +246 -7
  162. package/llms.txt +1 -0
  163. package/package.json +9 -1
  164. package/dist/{index-evcnmsc3.js → index-t7fkyxcr.js} +3 -3
@@ -0,0 +1,1351 @@
1
+ import {
2
+ assertContainedFileCurrent,
3
+ assertContainedParentCurrent,
4
+ containedEntryMetadata,
5
+ listContainedDirectory,
6
+ missingContainedDirectories,
7
+ openContainedFile,
8
+ openContainedParent,
9
+ openContainedParentFile,
10
+ readContainedUtf8Handle,
11
+ scanContainedFiles,
12
+ writeContainedFile
13
+ } from "./index-bn2cjajt.js";
14
+ import {
15
+ missingSandboxRestrictions,
16
+ probeAgentProcessSandbox
17
+ } from "./index-gskm1k5a.js";
18
+ import {
19
+ AppError,
20
+ conflict,
21
+ forbidden
22
+ } from "./index-0w9abg87.js";
23
+ import {
24
+ defineRuntimeTool
25
+ } from "./index-kc6h6hg0.js";
26
+
27
+ // src/agent-runtime/coding-tool-contract.ts
28
+ import { z } from "zod";
29
+ var AgentCodingToolLimitsSchema = z.object({
30
+ maxPathBytes: z.int().positive(),
31
+ maxReadBytes: z.int().positive(),
32
+ maxWriteBytes: z.int().positive(),
33
+ maxShellArguments: z.int().positive(),
34
+ maxShellArgumentBytes: z.int().positive(),
35
+ maxShellOutputBytes: z.int().positive(),
36
+ maxArtifactBytes: z.int().positive(),
37
+ maxListEntries: z.int().positive(),
38
+ maxSearchResults: z.int().positive(),
39
+ maxSearchFiles: z.int().positive(),
40
+ maxSearchDepth: z.int().nonnegative(),
41
+ shellTimeoutMs: z.int().positive(),
42
+ shellTerminationGraceMs: z.int().positive()
43
+ }).strict();
44
+ var AgentCodingToolAuthorizationSchema = z.discriminatedUnion("operation", [
45
+ z.object({ operation: z.literal("read"), path: z.string().min(1) }).strict(),
46
+ z.object({
47
+ operation: z.literal("write"),
48
+ path: z.string().min(1),
49
+ bytes: z.int().nonnegative(),
50
+ overwrite: z.boolean(),
51
+ createsDirectories: z.array(z.string().min(1))
52
+ }).strict(),
53
+ z.object({
54
+ operation: z.literal("search"),
55
+ query: z.string().min(1),
56
+ mode: z.enum(["path", "content"])
57
+ }).strict(),
58
+ z.object({
59
+ operation: z.literal("edit"),
60
+ path: z.string().min(1),
61
+ baseSha256: z.string().length(64),
62
+ resultSha256: z.string().length(64),
63
+ resultBytes: z.int().nonnegative(),
64
+ replacements: z.int().positive(),
65
+ dryRun: z.boolean()
66
+ }).strict(),
67
+ z.object({ operation: z.literal("list"), path: z.string().min(1) }).strict(),
68
+ z.object({
69
+ operation: z.literal("glob"),
70
+ pattern: z.string().min(1),
71
+ path: z.string().min(1)
72
+ }).strict(),
73
+ z.object({
74
+ operation: z.literal("artifact-read"),
75
+ reference: z.string().min(1),
76
+ offset: z.int().nonnegative(),
77
+ maxBytes: z.int().positive()
78
+ }).strict(),
79
+ z.object({
80
+ operation: z.literal("artifact-search"),
81
+ reference: z.string().min(1),
82
+ query: z.string().min(1),
83
+ maxMatches: z.int().positive()
84
+ }).strict(),
85
+ z.object({
86
+ operation: z.literal("shell"),
87
+ executable: z.string().min(1),
88
+ args: z.array(z.string()),
89
+ cwd: z.string().min(1)
90
+ }).strict()
91
+ ]);
92
+ var AgentCodingToolPathAuthorizationSchema = z.object({ path: z.string().min(1) }).strict();
93
+ var CodingDeniedEntrySchema = z.object({
94
+ path: z.string().min(1),
95
+ kind: z.enum(["file", "directory", "symlink", "other"])
96
+ }).strict();
97
+ var FileReadInputSchema = z.object({
98
+ path: z.string().min(1),
99
+ offset: z.int().nonnegative().default(0),
100
+ maxBytes: z.int().positive().optional()
101
+ }).strict();
102
+ var FileReadOutputSchema = z.object({
103
+ path: z.string().min(1),
104
+ text: z.string(),
105
+ bytes: z.int().nonnegative(),
106
+ sha256: z.string().length(64),
107
+ truncated: z.boolean(),
108
+ nextOffset: z.int().nonnegative().optional()
109
+ }).strict();
110
+ var FileWriteInputSchema = z.object({
111
+ path: z.string().min(1),
112
+ content: z.string(),
113
+ overwrite: z.boolean().default(false)
114
+ }).strict();
115
+ var FileWriteOutputSchema = z.object({
116
+ path: z.string().min(1),
117
+ bytes: z.int().nonnegative(),
118
+ createdDirectories: z.array(z.string().min(1))
119
+ }).strict();
120
+ function createShellInputSchema(executableNames) {
121
+ if (executableNames.length === 0) {
122
+ throw new Error("A command tool requires at least one executable alias");
123
+ }
124
+ return z.object({
125
+ executable: z.enum(executableNames),
126
+ args: z.array(z.string()).default([]),
127
+ cwd: z.string().min(1).default(".")
128
+ }).strict();
129
+ }
130
+ var ShellInputSchema = z.object({
131
+ executable: z.string().min(1),
132
+ args: z.array(z.string()).default([]),
133
+ cwd: z.string().min(1).default(".")
134
+ }).strict();
135
+ var ShellOutputSchema = z.object({
136
+ executable: z.string().min(1),
137
+ exitCode: z.int().nullable(),
138
+ signal: z.string().nullable(),
139
+ stdout: z.string(),
140
+ stderr: z.string(),
141
+ outcome: z.enum(["exited", "timeout", "output-limit", "cancelled"]),
142
+ artifact: z.object({
143
+ reference: z.string().min(1),
144
+ bytes: z.int().nonnegative(),
145
+ truncated: z.boolean(),
146
+ headBytes: z.int().nonnegative(),
147
+ tailBytes: z.int().nonnegative(),
148
+ omittedBytes: z.int().nonnegative()
149
+ }).strict().optional()
150
+ }).strict();
151
+ var DEFAULT_LIMITS = {
152
+ maxPathBytes: 4096,
153
+ maxReadBytes: 262144,
154
+ maxWriteBytes: 262144,
155
+ maxShellArguments: 128,
156
+ maxShellArgumentBytes: 65536,
157
+ maxShellOutputBytes: 262144,
158
+ maxArtifactBytes: 4194304,
159
+ maxListEntries: 500,
160
+ maxSearchResults: 100,
161
+ maxSearchFiles: 1e4,
162
+ maxSearchDepth: 32,
163
+ shellTimeoutMs: 30000,
164
+ shellTerminationGraceMs: 250
165
+ };
166
+ function resolveCodingToolLimits(input) {
167
+ return AgentCodingToolLimitsSchema.parse({ ...DEFAULT_LIMITS, ...input });
168
+ }
169
+
170
+ // src/agent-runtime/coding-tools.ts
171
+ import path4 from "node:path";
172
+ import { z as z4 } from "zod";
173
+
174
+ // src/agent-runtime/coding-tool-files.ts
175
+ import { createHash } from "node:crypto";
176
+ import { realpath as realpath2 } from "node:fs/promises";
177
+
178
+ // src/agent-runtime/coding-tool-paths.ts
179
+ import { lstat, realpath } from "node:fs/promises";
180
+ import path from "node:path";
181
+
182
+ // src/agent-runtime/coding-tool-refusals.ts
183
+ var REFUSAL_STATUS = {
184
+ BAD_REQUEST: 400,
185
+ FORBIDDEN: 403,
186
+ NOT_FOUND: 404,
187
+ CONFLICT: 409,
188
+ SANDBOX_UNAVAILABLE: 503,
189
+ SANDBOX_INSUFFICIENT: 503
190
+ };
191
+ function codingRefusal(code, message, options = {}) {
192
+ throw new AppError(code, message, REFUSAL_STATUS[code], { message, ...options.details }, options.hint);
193
+ }
194
+ function codingPathRefusal(code, message, path, options = {}) {
195
+ codingRefusal(code, message, {
196
+ details: { path, ...options.details },
197
+ ...options.hint && { hint: options.hint }
198
+ });
199
+ }
200
+ function refuseMissingCodingPath(error, relative) {
201
+ const code = error?.code;
202
+ if (error instanceof Error && error.message.includes("is not a regular file")) {
203
+ codingPathRefusal("BAD_REQUEST", "This path is not a regular file", relative, {
204
+ hint: "Use list_directory to see what is here."
205
+ });
206
+ }
207
+ if (code === "ENOENT") {
208
+ codingPathRefusal("NOT_FOUND", "This path does not exist", relative, {
209
+ hint: "Use list_directory or glob to find the right path."
210
+ });
211
+ }
212
+ if (code === "ENOTDIR") {
213
+ codingPathRefusal("BAD_REQUEST", "A segment of this path is not a directory", relative, {
214
+ hint: "Check the path with list_directory."
215
+ });
216
+ }
217
+ if (code === "ELOOP") {
218
+ codingPathRefusal("FORBIDDEN", "This path is a symlink, which is not followed", relative, {
219
+ hint: "Read or write the target directly, by its own workspace-relative path."
220
+ });
221
+ }
222
+ if (code === "EISDIR") {
223
+ codingPathRefusal("BAD_REQUEST", "This path is not a regular file", relative, {
224
+ hint: "Use list_directory to see what is here."
225
+ });
226
+ }
227
+ throw error;
228
+ }
229
+
230
+ // src/agent-runtime/coding-tool-paths.ts
231
+ function within(root, target) {
232
+ const relative = path.relative(root, target);
233
+ return relative === "" || !relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative);
234
+ }
235
+ function inputPath(root, requested, maxPathBytes) {
236
+ if (new TextEncoder().encode(requested).byteLength > maxPathBytes) {
237
+ codingRefusal("BAD_REQUEST", `Path exceeds the ${maxPathBytes}-byte limit`, {
238
+ details: { maxPathBytes },
239
+ hint: "Use a shorter workspace-relative path."
240
+ });
241
+ }
242
+ if (path.isAbsolute(requested)) {
243
+ const relative = path.relative(root, requested);
244
+ const recoveryPath = within(root, requested) ? relative.split(path.sep).join("/") || "." : undefined;
245
+ codingRefusal("BAD_REQUEST", "Paths must be relative to the workspace root", {
246
+ details: { path: requested, ...recoveryPath && { recoveryPath } },
247
+ hint: recoveryPath ? `Use the workspace-relative path \`${recoveryPath}\`.` : "Pass a path relative to the workspace root; absolute paths outside it have no workspace-relative equivalent."
248
+ });
249
+ }
250
+ const resolved = path.resolve(root, requested);
251
+ if (!within(root, resolved)) {
252
+ codingRefusal("FORBIDDEN", "Path escapes the workspace root", {
253
+ details: { path: requested },
254
+ hint: "Stay inside the workspace; `..` segments that leave the root are refused."
255
+ });
256
+ }
257
+ return resolved;
258
+ }
259
+ function boundedCodingRelativePath(root, requested, maxPathBytes) {
260
+ if (path.isAbsolute(requested))
261
+ inputPath(root, requested, maxPathBytes);
262
+ if (requested.includes("\\")) {
263
+ codingRefusal("FORBIDDEN", "Path separators must be `/`", {
264
+ details: { path: requested },
265
+ hint: "Use `/` between segments; a backslash is refused because the workspace walk reads it as a separator."
266
+ });
267
+ }
268
+ const segments = requested.split("/");
269
+ if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
270
+ codingRefusal("FORBIDDEN", "Path segments must name real entries under the root", {
271
+ details: { path: requested },
272
+ hint: "Pass a plain workspace-relative path — `.`, `..` and empty segments are refused."
273
+ });
274
+ }
275
+ inputPath(root, requested, maxPathBytes);
276
+ return requested;
277
+ }
278
+ async function existingCodingPath(root, requested, maxPathBytes) {
279
+ const resolved = await realpath(inputPath(root, requested, maxPathBytes));
280
+ if (!within(root, resolved))
281
+ throw new Error("Coding tool path resolves outside its root");
282
+ return { absolute: resolved, relative: path.relative(root, resolved) || "." };
283
+ }
284
+ async function authorizeCodingTool(config, request) {
285
+ const parsed = AgentCodingToolAuthorizationSchema.parse(request);
286
+ if (!await config.authorize(parsed))
287
+ forbidden("Coding tool permission denied");
288
+ }
289
+ async function isCodingPathAuthorized(config, path2) {
290
+ if (!config.authorizePath)
291
+ return true;
292
+ const parsed = AgentCodingToolPathAuthorizationSchema.parse({ path: path2 });
293
+ return await config.authorizePath(parsed);
294
+ }
295
+ async function authorizeCodingPath(config, path2) {
296
+ if (!await isCodingPathAuthorized(config, path2)) {
297
+ codingRefusal("FORBIDDEN", "Coding tool path permission denied", {
298
+ details: { path: path2 },
299
+ hint: "The host policy refused this path or a directory above it."
300
+ });
301
+ }
302
+ }
303
+ async function authorizeCodingPathChain(config, relative) {
304
+ if (!config.authorizePath)
305
+ return;
306
+ const segments = relative === "." ? ["."] : relative.split("/");
307
+ for (let depth = 1;depth <= segments.length; depth += 1) {
308
+ await authorizeCodingPath(config, segments.slice(0, depth).join("/"));
309
+ }
310
+ }
311
+ var codingPathLocks = new Map;
312
+ async function withCodingPathLock(target, work) {
313
+ const previous = codingPathLocks.get(target) ?? Promise.resolve();
314
+ let release = () => {
315
+ return;
316
+ };
317
+ const current = new Promise((resolve) => {
318
+ release = resolve;
319
+ });
320
+ const queued = previous.then(() => current);
321
+ codingPathLocks.set(target, queued);
322
+ await previous;
323
+ try {
324
+ return await work();
325
+ } finally {
326
+ release();
327
+ if (codingPathLocks.get(target) === queued)
328
+ codingPathLocks.delete(target);
329
+ }
330
+ }
331
+ function textOccurrences(source, search) {
332
+ let count = 0;
333
+ let offset = 0;
334
+ while (offset <= source.length - search.length) {
335
+ const found = source.indexOf(search, offset);
336
+ if (found < 0)
337
+ break;
338
+ count += 1;
339
+ offset = found + search.length;
340
+ }
341
+ return count;
342
+ }
343
+
344
+ // src/agent-runtime/coding-tool-utf8.ts
345
+ function sequenceLength(lead) {
346
+ if (lead < 128)
347
+ return 1;
348
+ if ((lead & 224) === 192)
349
+ return 2;
350
+ if ((lead & 240) === 224)
351
+ return 3;
352
+ if ((lead & 248) === 240)
353
+ return 4;
354
+ return;
355
+ }
356
+ function utf8AlignedEnd(data, start, limit) {
357
+ const safe = Math.max(start, Math.min(start + limit, data.byteLength));
358
+ for (let back = 1;back <= 4 && safe - back >= start; back += 1) {
359
+ const byte = data[safe - back];
360
+ if ((byte & 192) === 128)
361
+ continue;
362
+ const length = sequenceLength(byte);
363
+ if (length === undefined)
364
+ return safe;
365
+ if (back >= length)
366
+ return safe;
367
+ const sequenceEnd = safe - back + length;
368
+ return sequenceEnd <= data.byteLength ? sequenceEnd : safe - back;
369
+ }
370
+ return safe;
371
+ }
372
+ function utf8AlignedStart(data, start, end) {
373
+ let safe = Math.max(0, Math.min(start, end));
374
+ while (safe < end && (data[safe] & 192) === 128)
375
+ safe += 1;
376
+ return safe;
377
+ }
378
+
379
+ // src/agent-runtime/coding-tool-files.ts
380
+ async function digestFile(handle, size) {
381
+ const hash = createHash("sha256");
382
+ const chunk = Buffer.alloc(Math.min(64 * 1024, Math.max(1, size)));
383
+ for (let position = 0;position < size; ) {
384
+ const length = Math.min(chunk.byteLength, size - position);
385
+ const { bytesRead } = await handle.read(chunk, 0, length, position);
386
+ if (bytesRead === 0)
387
+ break;
388
+ hash.update(chunk.subarray(0, bytesRead));
389
+ position += bytesRead;
390
+ }
391
+ return hash.digest("hex");
392
+ }
393
+ function createFileCodingTools(config, limits) {
394
+ const read = defineRuntimeTool({
395
+ name: "read_file",
396
+ description: "Read a bounded UTF-8 slice from a host-authorized workspace file.",
397
+ identity: { serviceName: "coding", action: "read-file", method: "POST" },
398
+ input: FileReadInputSchema,
399
+ output: FileReadOutputSchema,
400
+ transports: ["AGENT"],
401
+ handler: async ({ input }) => {
402
+ const root = await realpath2(config.root);
403
+ const relative = boundedCodingRelativePath(root, input.path, limits.maxPathBytes);
404
+ await authorizeCodingPathChain(config, relative);
405
+ const handle = await openContainedFile(root, relative).catch((error) => refuseMissingCodingPath(error, relative));
406
+ const maximum = Math.min(input.maxBytes ?? limits.maxReadBytes, limits.maxReadBytes);
407
+ let selected;
408
+ let start = input.offset;
409
+ let size;
410
+ let sha256;
411
+ try {
412
+ await authorizeCodingTool(config, { operation: "read", path: relative });
413
+ await assertContainedFileCurrent(root, relative, handle);
414
+ const metadata = await handle.stat();
415
+ if (!metadata.isFile()) {
416
+ codingPathRefusal("BAD_REQUEST", "This path is not a regular file", relative, {
417
+ hint: "Use list_directory to see what is here."
418
+ });
419
+ }
420
+ size = metadata.size;
421
+ const length = Math.min(maximum + 4, Math.max(0, size - input.offset));
422
+ selected = Buffer.alloc(length);
423
+ const { bytesRead } = await handle.read(selected, 0, length, input.offset);
424
+ selected = selected.subarray(0, bytesRead);
425
+ if (input.offset > 0) {
426
+ const aligned = utf8AlignedStart(selected, 0, selected.byteLength);
427
+ start = input.offset + aligned;
428
+ selected = selected.subarray(aligned);
429
+ }
430
+ if (selected.byteLength > maximum) {
431
+ selected = selected.subarray(0, utf8AlignedEnd(selected, 0, maximum));
432
+ }
433
+ sha256 = await digestFile(handle, size);
434
+ } finally {
435
+ await handle.close();
436
+ }
437
+ let text;
438
+ try {
439
+ text = new TextDecoder("utf-8", { fatal: true }).decode(selected);
440
+ } catch {
441
+ codingPathRefusal("BAD_REQUEST", "The selected file bytes are not valid UTF-8", relative, {
442
+ details: { reason: "invalid_utf8" },
443
+ hint: "Use a binary-capable reader or convert the file to UTF-8."
444
+ });
445
+ }
446
+ const end = start + selected.byteLength;
447
+ const truncated = end < size;
448
+ return {
449
+ path: relative,
450
+ text,
451
+ bytes: selected.byteLength,
452
+ sha256,
453
+ truncated,
454
+ ...truncated && { nextOffset: end }
455
+ };
456
+ }
457
+ });
458
+ const write = defineRuntimeTool({
459
+ name: "write_file",
460
+ description: "Write bounded UTF-8 content to a host-authorized workspace file.",
461
+ identity: { serviceName: "coding", action: "write-file", method: "POST" },
462
+ input: FileWriteInputSchema,
463
+ output: FileWriteOutputSchema,
464
+ transports: ["AGENT"],
465
+ handler: async ({ input }) => {
466
+ const root = await realpath2(config.root);
467
+ const relative = boundedCodingRelativePath(root, input.path, limits.maxPathBytes);
468
+ await authorizeCodingPathChain(config, relative);
469
+ const bytes = Buffer.byteLength(input.content);
470
+ if (bytes > limits.maxWriteBytes) {
471
+ codingPathRefusal("BAD_REQUEST", `The content is ${bytes} bytes, over the ${limits.maxWriteBytes}-byte limit`, relative, {
472
+ details: { bytes, maxWriteBytes: limits.maxWriteBytes },
473
+ hint: "Write a smaller file, or split it."
474
+ });
475
+ }
476
+ const createsDirectories = await missingContainedDirectories(root, relative).catch((error) => {
477
+ if (error.code === "ENOTDIR") {
478
+ codingPathRefusal("CONFLICT", "A segment of this path is not a directory", relative, {
479
+ hint: "Write under a different path, or remove the file blocking it."
480
+ });
481
+ }
482
+ throw error;
483
+ });
484
+ await authorizeCodingTool(config, {
485
+ operation: "write",
486
+ path: relative,
487
+ bytes,
488
+ overwrite: input.overwrite,
489
+ createsDirectories
490
+ });
491
+ const parent = await openContainedParent(root, relative, { create: true }).catch((error) => {
492
+ const code = error.code;
493
+ if (code === "ENOTDIR" || error instanceof Error && error.message.includes("ancestor is not a directory")) {
494
+ codingPathRefusal("CONFLICT", "A segment of this path is not a directory", relative, {
495
+ details: { createsDirectories },
496
+ hint: "Write under a different path, or remove the file blocking it."
497
+ });
498
+ }
499
+ throw error;
500
+ });
501
+ try {
502
+ await assertContainedParentCurrent(root, relative, parent.handle);
503
+ const current = await containedEntryMetadata(parent);
504
+ if (input.overwrite) {
505
+ if (current?.isSymbolicLink()) {
506
+ codingPathRefusal("FORBIDDEN", "This path is a symlink", relative, {
507
+ hint: "Coding tools never write through symlinks; write to the target directly."
508
+ });
509
+ }
510
+ await writeContainedFile({
511
+ parent,
512
+ content: input.content,
513
+ replace: true,
514
+ ...current && { mode: current.mode }
515
+ });
516
+ } else {
517
+ if (current) {
518
+ codingPathRefusal("CONFLICT", "This file already exists", relative, {
519
+ hint: "Pass overwrite: true to replace it, or edit_file to change part of it."
520
+ });
521
+ }
522
+ await writeContainedFile({ parent, content: input.content, replace: false });
523
+ }
524
+ } finally {
525
+ await parent.handle.close();
526
+ }
527
+ return { path: relative, bytes, createdDirectories: createsDirectories };
528
+ }
529
+ });
530
+ return [read, write];
531
+ }
532
+
533
+ // src/agent-runtime/coding-tool-listing.ts
534
+ import { realpath as realpath3 } from "node:fs/promises";
535
+ import path2 from "node:path";
536
+ import { z as z2 } from "zod";
537
+ var DEFAULT_EXCLUDED_DIRECTORIES = [
538
+ ".git",
539
+ ".next",
540
+ ".stitchkit",
541
+ "build",
542
+ "coverage",
543
+ "dist",
544
+ "node_modules"
545
+ ];
546
+ var ListInputSchema = z2.object({ path: z2.string().min(1).default(".") }).strict();
547
+ var ListOutputSchema = z2.object({
548
+ path: z2.string().min(1),
549
+ entries: z2.array(z2.object({
550
+ name: z2.string().min(1),
551
+ kind: z2.enum(["file", "directory", "symlink", "other"]),
552
+ bytes: z2.int().nonnegative().optional(),
553
+ excluded: z2.boolean().optional()
554
+ }).strict()),
555
+ truncated: z2.boolean(),
556
+ denied: z2.array(CodingDeniedEntrySchema).optional(),
557
+ deniedTruncated: z2.boolean().optional()
558
+ }).strict();
559
+ var GlobInputSchema = z2.object({ pattern: z2.string().min(1), path: z2.string().min(1).default(".") }).strict();
560
+ var GlobOutputSchema = z2.object({
561
+ paths: z2.array(z2.string().min(1)),
562
+ truncated: z2.boolean(),
563
+ resultTruncated: z2.boolean(),
564
+ skippedDirectories: z2.int().nonnegative(),
565
+ scannedFiles: z2.int().nonnegative(),
566
+ denied: z2.array(CodingDeniedEntrySchema).optional(),
567
+ deniedTruncated: z2.boolean().optional()
568
+ }).strict();
569
+ function compileWorkspaceGlob(pattern) {
570
+ let source = "";
571
+ for (let index = 0;index < pattern.length; index += 1) {
572
+ const character = pattern[index];
573
+ if (character === "*") {
574
+ if (pattern[index + 1] === "*") {
575
+ if (pattern[index + 2] === "/") {
576
+ source += "(?:[^/]+/)*";
577
+ index += 2;
578
+ continue;
579
+ }
580
+ source += ".*";
581
+ index += 1;
582
+ continue;
583
+ }
584
+ source += "[^/]*";
585
+ continue;
586
+ }
587
+ if (character === "?") {
588
+ source += "[^/]";
589
+ continue;
590
+ }
591
+ source += character?.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") ?? "";
592
+ }
593
+ return new RegExp(`^${source}$`);
594
+ }
595
+ function createListingCodingTools(config, limits) {
596
+ const excluded = new Set(config.search?.excludeDirectories ?? DEFAULT_EXCLUDED_DIRECTORIES);
597
+ const list = defineRuntimeTool({
598
+ name: "list_directory",
599
+ description: "List the direct entries of one workspace directory.",
600
+ identity: { serviceName: "coding", action: "list-directory", method: "POST" },
601
+ input: ListInputSchema,
602
+ output: ListOutputSchema,
603
+ transports: ["AGENT"],
604
+ handler: async ({ input }) => {
605
+ const root = await realpath3(config.root);
606
+ const relative = input.path === "." ? "." : boundedCodingRelativePath(root, input.path, limits.maxPathBytes);
607
+ await authorizeCodingPathChain(config, relative);
608
+ await authorizeCodingTool(config, { operation: "list", path: relative });
609
+ const listing = await listContainedDirectory(root, relative, limits.maxListEntries, (candidate) => isCodingPathAuthorized(config, candidate)).catch((error) => {
610
+ if (error instanceof Error && error.message.includes("ancestor is not a directory")) {
611
+ codingPathRefusal("BAD_REQUEST", "This path is not a directory", relative, {
612
+ hint: "Pass a directory; use read_file for a file."
613
+ });
614
+ }
615
+ if (error.code === "ENOENT") {
616
+ codingPathRefusal("NOT_FOUND", "This directory does not exist", relative, {
617
+ hint: "List a parent directory to see what is there."
618
+ });
619
+ }
620
+ return refuseMissingCodingPath(error, relative);
621
+ });
622
+ return {
623
+ path: relative,
624
+ entries: listing.entries.map((entry) => ({
625
+ ...entry,
626
+ ...entry.kind === "directory" && excluded.has(entry.name) && { excluded: true }
627
+ })),
628
+ truncated: listing.truncated,
629
+ ...listing.denied.length > 0 && {
630
+ denied: listing.denied.map((entry) => ({
631
+ path: relative === "." ? entry.name : path2.join(relative, entry.name),
632
+ kind: entry.kind
633
+ })),
634
+ deniedTruncated: listing.deniedTruncated
635
+ }
636
+ };
637
+ }
638
+ });
639
+ const glob = defineRuntimeTool({
640
+ name: "glob",
641
+ description: "List workspace files matching a `**`/`*`/`?` pattern.",
642
+ identity: { serviceName: "coding", action: "glob", method: "POST" },
643
+ input: GlobInputSchema,
644
+ output: GlobOutputSchema,
645
+ transports: ["AGENT"],
646
+ handler: async ({ input }) => {
647
+ const root = await realpath3(config.root);
648
+ const relative = input.path === "." ? "." : boundedCodingRelativePath(root, input.path, limits.maxPathBytes);
649
+ await authorizeCodingPathChain(config, relative);
650
+ await authorizeCodingTool(config, {
651
+ operation: "glob",
652
+ pattern: input.pattern,
653
+ path: relative
654
+ });
655
+ let matcher;
656
+ try {
657
+ matcher = compileWorkspaceGlob(input.pattern);
658
+ } catch {
659
+ codingRefusal("BAD_REQUEST", "This glob pattern could not be compiled", {
660
+ details: { pattern: input.pattern },
661
+ hint: "Use only `**`, `*` and `?`."
662
+ });
663
+ }
664
+ const scan = await scanContainedFiles({
665
+ root,
666
+ maxDepth: limits.maxSearchDepth,
667
+ maxFiles: limits.maxSearchFiles,
668
+ symlinks: "skip",
669
+ excludeDirectory: (directory) => directory.split(path2.sep).some((segment) => excluded.has(segment)),
670
+ authorizePath: (candidate) => isCodingPathAuthorized(config, candidate)
671
+ });
672
+ const prefix = relative === "." ? "" : `${relative}${path2.sep}`;
673
+ const matched = scan.files.filter((file) => file.relative.startsWith(prefix)).map((file) => file.relative).filter((candidate) => matcher.test(candidate.slice(prefix.length))).sort();
674
+ const paths = matched.slice(0, limits.maxSearchResults);
675
+ const scopedDenied = scan.denied.filter((entry) => entry.relative.startsWith(prefix));
676
+ const denied = scopedDenied.slice(0, limits.maxSearchResults).map((entry) => ({
677
+ path: entry.relative,
678
+ kind: entry.kind
679
+ }));
680
+ return {
681
+ paths,
682
+ truncated: scan.truncated,
683
+ resultTruncated: matched.length > paths.length,
684
+ skippedDirectories: scan.skippedDirectories,
685
+ scannedFiles: scan.files.length,
686
+ ...denied.length > 0 && {
687
+ denied,
688
+ deniedTruncated: scan.deniedTruncated || scopedDenied.length > denied.length
689
+ }
690
+ };
691
+ }
692
+ });
693
+ return [list, glob];
694
+ }
695
+
696
+ // src/agent-runtime/coding-tool-search-patch.ts
697
+ import { createHash as createHash2 } from "node:crypto";
698
+ import { realpath as realpath4 } from "node:fs/promises";
699
+ import path3 from "node:path";
700
+ import { z as z3 } from "zod";
701
+ var SearchInputSchema = z3.object({
702
+ query: z3.string().min(1),
703
+ mode: z3.enum(["path", "content"]).default("content"),
704
+ regex: z3.boolean().default(false),
705
+ context: z3.int().nonnegative().max(10).default(0),
706
+ include: z3.string().min(1).describe("Anchored `**`/`*`/`?` pattern matched against the whole workspace-relative path. `*` does not cross `/`; use `**/` to include matching files in subdirectories.").optional()
707
+ }).strict();
708
+ var SearchOutputSchema = z3.object({
709
+ matches: z3.array(z3.object({
710
+ path: z3.string().min(1),
711
+ line: z3.int().positive().optional(),
712
+ text: z3.string().optional(),
713
+ before: z3.array(z3.string()).optional(),
714
+ after: z3.array(z3.string()).optional()
715
+ }).strict()),
716
+ truncated: z3.boolean(),
717
+ scannedFiles: z3.int().nonnegative().describe("Files admitted by include and host authorization that reached path or content comparison."),
718
+ skippedDirectories: z3.int().nonnegative(),
719
+ skippedSymlinks: z3.int().nonnegative(),
720
+ denied: z3.array(CodingDeniedEntrySchema).optional(),
721
+ deniedTruncated: z3.boolean().optional(),
722
+ hint: z3.string().min(1).optional()
723
+ }).strict();
724
+ function compileSearchRegex(pattern) {
725
+ if (/\\[1-9]/.test(pattern) || /\(\?<?[=!]/.test(pattern)) {
726
+ codingRefusal("BAD_REQUEST", "This pattern uses a construct search does not allow", {
727
+ details: { pattern },
728
+ hint: "Backreferences and lookaround are refused because they cannot be bounded. Rewrite without them."
729
+ });
730
+ }
731
+ try {
732
+ return new RegExp(pattern);
733
+ } catch (error) {
734
+ codingRefusal("BAD_REQUEST", "This pattern is not a valid regular expression", {
735
+ details: { pattern, reason: error instanceof Error ? error.message : "unknown" },
736
+ hint: "Fix the expression, or pass regex: false to search for it literally."
737
+ });
738
+ }
739
+ }
740
+ var MAX_SEARCHED_LINE_BYTES = 8192;
741
+ var EditInputSchema = z3.object({
742
+ path: z3.string().min(1),
743
+ oldText: z3.string().min(1),
744
+ newText: z3.string(),
745
+ replaceAll: z3.boolean().default(false),
746
+ expectedSha256: z3.string().length(64).optional(),
747
+ dryRun: z3.boolean().default(false)
748
+ }).strict();
749
+ var EditOutputSchema = z3.object({
750
+ path: z3.string().min(1),
751
+ baseSha256: z3.string().length(64),
752
+ sha256: z3.string().length(64),
753
+ replacements: z3.int().positive(),
754
+ bytes: z3.int().nonnegative(),
755
+ applied: z3.boolean()
756
+ }).strict();
757
+ function sha256(value) {
758
+ return createHash2("sha256").update(value).digest("hex");
759
+ }
760
+ function createSearchAndPatchCodingTools(config, limits) {
761
+ const search = defineRuntimeTool({
762
+ name: "search_files",
763
+ description: "Search bounded relative paths or UTF-8 file content under the workspace root.",
764
+ identity: { serviceName: "coding", action: "search", method: "POST" },
765
+ input: SearchInputSchema,
766
+ output: SearchOutputSchema,
767
+ transports: ["AGENT"],
768
+ handler: async ({ input }) => {
769
+ await authorizeCodingTool(config, {
770
+ operation: "search",
771
+ query: input.query,
772
+ mode: input.mode
773
+ });
774
+ const root = await realpath4(config.root);
775
+ await authorizeCodingPath(config, ".");
776
+ const excluded = new Set(config.search?.excludeDirectories ?? DEFAULT_EXCLUDED_DIRECTORIES);
777
+ const included = input.include ? compileWorkspaceGlob(input.include) : null;
778
+ let rejectedByInclude = 0;
779
+ const scan = await scanContainedFiles({
780
+ root,
781
+ maxDepth: limits.maxSearchDepth,
782
+ maxFiles: limits.maxSearchFiles,
783
+ symlinks: "skip",
784
+ ...input.mode === "content" && {
785
+ readMaxBytes: limits.maxReadBytes,
786
+ skipUnreadable: true
787
+ },
788
+ excludeDirectory: (relative) => relative.split(path3.sep).some((segment) => excluded.has(segment)),
789
+ includeFile: (candidate) => {
790
+ if (included && !included.test(candidate)) {
791
+ rejectedByInclude += 1;
792
+ return false;
793
+ }
794
+ return true;
795
+ },
796
+ authorizePath: (candidate) => isCodingPathAuthorized(config, candidate)
797
+ });
798
+ const matcher = input.regex ? compileSearchRegex(input.query) : null;
799
+ const hit = (line) => {
800
+ if (line.length > MAX_SEARCHED_LINE_BYTES)
801
+ return false;
802
+ if (matcher) {
803
+ matcher.lastIndex = 0;
804
+ return matcher.test(line);
805
+ }
806
+ return line.includes(input.query);
807
+ };
808
+ const matches = [];
809
+ let truncated = false;
810
+ for (const file of scan.files) {
811
+ if (input.mode === "path") {
812
+ if (hit(file.relative))
813
+ matches.push({ path: file.relative });
814
+ } else {
815
+ const text = file.content?.text;
816
+ if (text === undefined)
817
+ continue;
818
+ const lines = text.split(`
819
+ `);
820
+ for (let index = 0;index < lines.length; index += 1) {
821
+ const line = lines[index];
822
+ if (line === undefined || !hit(line))
823
+ continue;
824
+ matches.push({
825
+ path: file.relative,
826
+ line: index + 1,
827
+ text: line,
828
+ ...input.context > 0 && {
829
+ before: lines.slice(Math.max(0, index - input.context), index),
830
+ after: lines.slice(index + 1, index + 1 + input.context)
831
+ }
832
+ });
833
+ if (matches.length >= limits.maxSearchResults)
834
+ break;
835
+ }
836
+ }
837
+ if (matches.length >= limits.maxSearchResults) {
838
+ truncated = true;
839
+ break;
840
+ }
841
+ }
842
+ return {
843
+ matches,
844
+ truncated: truncated || scan.truncated,
845
+ scannedFiles: scan.files.length,
846
+ skippedDirectories: scan.skippedDirectories,
847
+ skippedSymlinks: scan.skippedSymlinks,
848
+ ...scan.denied.length > 0 && {
849
+ denied: scan.denied.slice(0, limits.maxSearchResults).map(({ relative, kind }) => ({ path: relative, kind })),
850
+ deniedTruncated: scan.deniedTruncated || scan.denied.length > limits.maxSearchResults
851
+ },
852
+ ...input.include && scan.files.length === 0 && rejectedByInclude > 0 && {
853
+ hint: `The anchored include pattern \`${input.include}\` rejected all ${rejectedByInclude} ${rejectedByInclude === 1 ? "file" : "files"} the scan reached. \`*\` does not cross \`/\`; use a pattern such as \`**/*.ts\` for matching files in subdirectories.`
854
+ }
855
+ };
856
+ }
857
+ });
858
+ const edit = defineRuntimeTool({
859
+ name: "edit_file",
860
+ description: "Replace an exact snippet in one workspace file.",
861
+ identity: { serviceName: "coding", action: "edit-file", method: "POST" },
862
+ input: EditInputSchema,
863
+ output: EditOutputSchema,
864
+ transports: ["AGENT"],
865
+ handler: async ({ input }) => {
866
+ const root = await realpath4(config.root);
867
+ const relative = boundedCodingRelativePath(root, input.path, limits.maxPathBytes);
868
+ await authorizeCodingPathChain(config, relative);
869
+ const parent = await openContainedParent(root, relative).catch((error) => refuseMissingCodingPath(error, relative));
870
+ try {
871
+ return await withCodingPathLock(`${root}:${relative}`, async () => {
872
+ const handle = await openContainedParentFile(parent).catch((error) => refuseMissingCodingPath(error, relative));
873
+ const read = await readContainedUtf8Handle(handle, limits.maxWriteBytes).finally(() => handle.close());
874
+ const source = read.text;
875
+ const baseSha256 = sha256(source);
876
+ if (input.expectedSha256 !== undefined && baseSha256 !== input.expectedSha256) {
877
+ conflict("The file changed since it was read", {
878
+ message: "The file changed since it was read",
879
+ path: relative,
880
+ expectedSha256: input.expectedSha256,
881
+ actualSha256: baseSha256
882
+ });
883
+ }
884
+ const count = textOccurrences(source, input.oldText);
885
+ if (count === 0) {
886
+ codingPathRefusal("NOT_FOUND", "oldText does not appear in this file", relative, {
887
+ details: { occurrences: 0 },
888
+ hint: "Read the file and copy the snippet exactly, including indentation."
889
+ });
890
+ }
891
+ if (!input.replaceAll && count !== 1) {
892
+ codingPathRefusal("CONFLICT", `oldText appears ${count} times, so the edit is ambiguous`, relative, {
893
+ details: { occurrences: count },
894
+ hint: "Lengthen the snippet until it is unique, or pass replaceAll: true."
895
+ });
896
+ }
897
+ const replacements = input.replaceAll ? count : 1;
898
+ const changed = input.replaceAll ? source.replaceAll(input.oldText, input.newText) : source.replace(input.oldText, () => input.newText);
899
+ const bytes = Buffer.byteLength(changed);
900
+ const resultSha256 = sha256(changed);
901
+ if (bytes > limits.maxWriteBytes) {
902
+ codingPathRefusal("BAD_REQUEST", `The result would be ${bytes} bytes, over the ${limits.maxWriteBytes}-byte limit`, relative, {
903
+ details: { bytes, maxWriteBytes: limits.maxWriteBytes },
904
+ hint: "Make a smaller edit, or split the file."
905
+ });
906
+ }
907
+ await authorizeCodingTool(config, {
908
+ operation: "edit",
909
+ path: relative,
910
+ baseSha256,
911
+ resultSha256,
912
+ resultBytes: bytes,
913
+ replacements,
914
+ dryRun: input.dryRun
915
+ });
916
+ await assertContainedParentCurrent(root, relative, parent.handle);
917
+ if (!input.dryRun) {
918
+ await writeContainedFile({
919
+ parent,
920
+ content: changed,
921
+ replace: true,
922
+ mode: read.mode
923
+ });
924
+ }
925
+ return {
926
+ path: relative,
927
+ baseSha256,
928
+ sha256: resultSha256,
929
+ replacements,
930
+ bytes,
931
+ applied: !input.dryRun
932
+ };
933
+ });
934
+ } finally {
935
+ await parent.handle.close();
936
+ }
937
+ }
938
+ });
939
+ return [search, edit];
940
+ }
941
+
942
+ // src/agent-runtime/coding-tool-shell.ts
943
+ import { realpath as realpath5, stat } from "node:fs/promises";
944
+
945
+ // src/agent-runtime/coding-shell-process.ts
946
+ import { spawn } from "node:child_process";
947
+ async function runCodingShell(input) {
948
+ const preview = (data, budget) => {
949
+ if (data.byteLength <= budget) {
950
+ return { data, headBytes: data.byteLength, tailBytes: 0, omittedBytes: 0 };
951
+ }
952
+ const headEnd = utf8AlignedEnd(data, 0, Math.ceil(budget / 2));
953
+ const tailStart = Math.max(headEnd, utf8AlignedStart(data, data.byteLength - Math.floor(budget / 2), data.byteLength));
954
+ const tailBytes = data.byteLength - tailStart;
955
+ return {
956
+ data: Buffer.concat([data.subarray(0, headEnd), data.subarray(tailStart)]),
957
+ headBytes: headEnd,
958
+ tailBytes,
959
+ omittedBytes: data.byteLength - headEnd - tailBytes
960
+ };
961
+ };
962
+ const retainedPreview = (data) => {
963
+ const end = utf8AlignedEnd(data, 0, data.byteLength);
964
+ return {
965
+ data: data.subarray(0, end),
966
+ headBytes: end,
967
+ tailBytes: 0,
968
+ omittedBytes: data.byteLength - end
969
+ };
970
+ };
971
+ const stdoutHeader = Buffer.from(`--- stdout ---
972
+ `);
973
+ const stderrHeader = Buffer.from(`
974
+ --- stderr ---
975
+ `);
976
+ const artifactPayloadLimit = input.maxArtifactBytes - stdoutHeader.byteLength - stderrHeader.byteLength;
977
+ if (input.artifacts && artifactPayloadLimit < 0)
978
+ throw new Error("maxArtifactBytes is smaller than the shell artifact envelope");
979
+ if (input.signal?.aborted) {
980
+ return ShellOutputSchema.parse({
981
+ executable: input.executableName,
982
+ exitCode: null,
983
+ signal: null,
984
+ stdout: "",
985
+ stderr: "",
986
+ outcome: "cancelled"
987
+ });
988
+ }
989
+ return await new Promise((resolve, reject) => {
990
+ const ownsProcessGroup = process.platform !== "win32";
991
+ const child = input.spawn ? input.spawn() : spawn(input.executable, input.args, {
992
+ cwd: input.cwd,
993
+ env: input.environment,
994
+ stdio: ["ignore", "pipe", "pipe"],
995
+ detached: ownsProcessGroup
996
+ });
997
+ const stdout = [];
998
+ const stderr = [];
999
+ const artifactStdout = [];
1000
+ const artifactStderr = [];
1001
+ let retained = 0;
1002
+ let artifactBytes = 0;
1003
+ let outcome = "exited";
1004
+ let settled = false;
1005
+ let settlementTimer;
1006
+ const cancel = () => terminate("cancelled");
1007
+ const cleanup = () => {
1008
+ clearTimeout(timer);
1009
+ if (settlementTimer)
1010
+ clearTimeout(settlementTimer);
1011
+ input.signal?.removeEventListener("abort", cancel);
1012
+ };
1013
+ const persistAndResolve = async (exitCode, signal) => {
1014
+ if (settled)
1015
+ return;
1016
+ settled = true;
1017
+ cleanup();
1018
+ try {
1019
+ const hasArtifact = input.artifacts && artifactBytes > input.maxOutputBytes;
1020
+ const completeStdout = Buffer.concat(artifactStdout);
1021
+ const completeStderr = Buffer.concat(artifactStderr);
1022
+ const artifactData = hasArtifact ? Buffer.concat([stdoutHeader, completeStdout, stderrHeader, completeStderr]) : undefined;
1023
+ const stdoutBudget = completeStderr.byteLength === 0 ? input.maxOutputBytes : Math.ceil(input.maxOutputBytes / 2);
1024
+ const stderrBudget = completeStdout.byteLength === 0 ? input.maxOutputBytes : Math.floor(input.maxOutputBytes / 2);
1025
+ const stdoutPreview = hasArtifact ? preview(completeStdout, stdoutBudget) : retainedPreview(Buffer.concat(stdout));
1026
+ const stderrPreview = hasArtifact ? preview(completeStderr, stderrBudget) : retainedPreview(Buffer.concat(stderr));
1027
+ const persisted = hasArtifact ? await input.artifacts?.write({
1028
+ mediaType: "text/plain; charset=utf-8",
1029
+ data: artifactData ?? new Uint8Array,
1030
+ ...input.authorization && { authorization: input.authorization }
1031
+ }) : undefined;
1032
+ resolve(ShellOutputSchema.parse({
1033
+ executable: input.executableName,
1034
+ exitCode,
1035
+ signal,
1036
+ stdout: stdoutPreview.data.toString("utf8"),
1037
+ stderr: stderrPreview.data.toString("utf8"),
1038
+ outcome,
1039
+ ...persisted && {
1040
+ artifact: {
1041
+ reference: persisted.reference,
1042
+ bytes: artifactData?.byteLength ?? 0,
1043
+ truncated: outcome === "output-limit",
1044
+ headBytes: stdoutPreview.headBytes + stderrPreview.headBytes,
1045
+ tailBytes: stdoutPreview.tailBytes + stderrPreview.tailBytes,
1046
+ omittedBytes: stdoutPreview.omittedBytes + stderrPreview.omittedBytes
1047
+ }
1048
+ }
1049
+ }));
1050
+ } catch (error) {
1051
+ reject(error);
1052
+ }
1053
+ };
1054
+ const killOwnedProcessGroup = () => {
1055
+ if (!ownsProcessGroup || child.pid === undefined)
1056
+ return false;
1057
+ try {
1058
+ process.kill(-child.pid, "SIGKILL");
1059
+ return true;
1060
+ } catch {
1061
+ return false;
1062
+ }
1063
+ };
1064
+ const boundStreamSettlement = () => {
1065
+ if (settlementTimer)
1066
+ return;
1067
+ settlementTimer = setTimeout(() => {
1068
+ child.stdout.destroy();
1069
+ child.stderr.destroy();
1070
+ persistAndResolve(child.exitCode, child.signalCode);
1071
+ }, input.terminationGraceMs);
1072
+ };
1073
+ const terminate = (reason) => {
1074
+ if (settled || outcome !== "exited")
1075
+ return;
1076
+ outcome = reason;
1077
+ if (!killOwnedProcessGroup())
1078
+ child.kill("SIGKILL");
1079
+ boundStreamSettlement();
1080
+ };
1081
+ const timer = setTimeout(() => terminate("timeout"), input.timeoutMs);
1082
+ timer.unref();
1083
+ const retain = (target, artifactTarget, chunk) => {
1084
+ const remaining = Math.max(0, input.maxOutputBytes - retained);
1085
+ if (remaining > 0) {
1086
+ const kept = chunk.subarray(0, remaining);
1087
+ target.push(kept);
1088
+ retained += kept.byteLength;
1089
+ }
1090
+ if (input.artifacts) {
1091
+ const artifactRemaining = Math.max(0, artifactPayloadLimit - artifactBytes);
1092
+ if (artifactRemaining > 0) {
1093
+ const kept = chunk.subarray(0, artifactRemaining);
1094
+ artifactTarget.push(kept);
1095
+ artifactBytes += kept.byteLength;
1096
+ }
1097
+ if (chunk.byteLength > artifactRemaining)
1098
+ terminate("output-limit");
1099
+ } else if (chunk.byteLength > remaining)
1100
+ terminate("output-limit");
1101
+ };
1102
+ child.stdout.on("data", (chunk) => retain(stdout, artifactStdout, Buffer.from(chunk)));
1103
+ child.stderr.on("data", (chunk) => retain(stderr, artifactStderr, Buffer.from(chunk)));
1104
+ child.on("error", (error) => {
1105
+ if (settled)
1106
+ return;
1107
+ settled = true;
1108
+ cleanup();
1109
+ reject(error);
1110
+ });
1111
+ child.on("exit", (exitCode, signal) => {
1112
+ if (outcome === "exited")
1113
+ killOwnedProcessGroup();
1114
+ boundStreamSettlement();
1115
+ if (child.stdout.destroyed && child.stderr.destroyed) {
1116
+ persistAndResolve(exitCode, signal);
1117
+ }
1118
+ });
1119
+ child.on("close", (exitCode, signal) => void persistAndResolve(exitCode, signal));
1120
+ input.signal?.addEventListener("abort", cancel, { once: true });
1121
+ if (input.signal?.aborted)
1122
+ cancel();
1123
+ });
1124
+ }
1125
+
1126
+ // src/agent-runtime/coding-tool-shell.ts
1127
+ function createShellCodingTool(config, limits) {
1128
+ const executables = config.executables ?? {};
1129
+ const executableNames = Object.keys(executables).sort();
1130
+ return defineRuntimeTool({
1131
+ name: "run_command",
1132
+ description: "Run a finite host-declared executable with bounded arguments, time and output.",
1133
+ identity: { serviceName: "coding", action: "shell", method: "POST" },
1134
+ input: createShellInputSchema(executableNames),
1135
+ output: ShellOutputSchema,
1136
+ transports: ["AGENT"],
1137
+ handler: async ({ input, signal }) => {
1138
+ if (input.args.length > limits.maxShellArguments) {
1139
+ throw new Error("Coding tool shell arguments exceed maxShellArguments");
1140
+ }
1141
+ const argumentBytes = input.args.reduce((total, argument) => total + Buffer.byteLength(argument), 0);
1142
+ if (argumentBytes > limits.maxShellArgumentBytes) {
1143
+ throw new Error("Coding tool shell arguments exceed maxShellArgumentBytes");
1144
+ }
1145
+ const executable = executables[input.executable];
1146
+ if (!executable)
1147
+ throw new Error("Coding tool executable is not declared");
1148
+ const root = await realpath5(config.root);
1149
+ const cwd = await existingCodingPath(root, input.cwd === "." ? "." : boundedCodingRelativePath(root, input.cwd, limits.maxPathBytes), limits.maxPathBytes);
1150
+ if (!(await stat(cwd.absolute)).isDirectory()) {
1151
+ throw new Error("Coding tool cwd is not a directory");
1152
+ }
1153
+ const shellAuthorization = {
1154
+ operation: "shell",
1155
+ executable: input.executable,
1156
+ args: input.args,
1157
+ cwd: cwd.relative
1158
+ };
1159
+ await authorizeCodingTool(config, shellAuthorization);
1160
+ let executablePath = executable;
1161
+ let executableArgs = input.args;
1162
+ let environment = config.environment ?? {};
1163
+ if (config.sandbox) {
1164
+ const grade = await probeAgentProcessSandbox(config.sandbox.adapter);
1165
+ if (grade.grade === "unavailable") {
1166
+ codingRefusal("SANDBOX_UNAVAILABLE", "The configured process sandbox is unavailable", {
1167
+ details: { reason: grade.reason }
1168
+ });
1169
+ }
1170
+ const missing = missingSandboxRestrictions(grade, config.sandbox.required);
1171
+ if (missing.length > 0) {
1172
+ codingRefusal("SANDBOX_INSUFFICIENT", "The process sandbox lacks required restrictions", {
1173
+ details: { missing, grade: grade.grade }
1174
+ });
1175
+ }
1176
+ try {
1177
+ const prepared = await config.sandbox.adapter.prepare({
1178
+ required: config.sandbox.required,
1179
+ executable,
1180
+ args: input.args,
1181
+ cwd: cwd.absolute,
1182
+ environment
1183
+ });
1184
+ executablePath = prepared.executable;
1185
+ executableArgs = [...prepared.args];
1186
+ environment = prepared.environment ?? environment;
1187
+ } catch (error) {
1188
+ const refreshed = await probeAgentProcessSandbox(config.sandbox.adapter, {
1189
+ refresh: true
1190
+ });
1191
+ codingRefusal("SANDBOX_UNAVAILABLE", "The process sandbox failed while preparing a command", {
1192
+ details: {
1193
+ grade: refreshed.grade,
1194
+ reason: error instanceof Error ? error.message : "unknown sandbox error"
1195
+ }
1196
+ });
1197
+ }
1198
+ }
1199
+ return runCodingShell({
1200
+ ...config.sandbox?.adapter.spawn && {
1201
+ spawn: () => {
1202
+ const adapter = config.sandbox?.adapter;
1203
+ if (!adapter?.spawn)
1204
+ throw new Error("Sandbox launcher is unavailable");
1205
+ return adapter.spawn({
1206
+ executable: executablePath,
1207
+ args: executableArgs,
1208
+ cwd: cwd.absolute,
1209
+ environment
1210
+ });
1211
+ }
1212
+ },
1213
+ executableName: input.executable,
1214
+ executable: executablePath,
1215
+ args: executableArgs,
1216
+ cwd: cwd.absolute,
1217
+ environment,
1218
+ ...signal && { signal },
1219
+ timeoutMs: limits.shellTimeoutMs,
1220
+ terminationGraceMs: limits.shellTerminationGraceMs,
1221
+ maxOutputBytes: limits.maxShellOutputBytes,
1222
+ maxArtifactBytes: limits.maxArtifactBytes,
1223
+ ...config.artifacts && { artifacts: config.artifacts },
1224
+ authorization: shellAuthorization
1225
+ });
1226
+ }
1227
+ });
1228
+ }
1229
+
1230
+ // src/agent-runtime/coding-tools.ts
1231
+ var AGENT_CODING_TOOL_NAMES = {
1232
+ editFile: "edit_file",
1233
+ glob: "glob",
1234
+ listDirectory: "list_directory",
1235
+ readFile: "read_file",
1236
+ readOutput: "read_output",
1237
+ searchOutput: "search_output",
1238
+ runCommand: "run_command",
1239
+ searchFiles: "search_files",
1240
+ writeFile: "write_file"
1241
+ };
1242
+ function createAgentCodingTools(config) {
1243
+ if (!path4.isAbsolute(config.root))
1244
+ throw new Error("Coding tool root must be absolute");
1245
+ for (const executable of Object.values(config.executables ?? {})) {
1246
+ if (!path4.isAbsolute(executable)) {
1247
+ throw new Error("Coding tool executables must be absolute paths");
1248
+ }
1249
+ }
1250
+ const limits = resolveCodingToolLimits(config.limits);
1251
+ const artifactTools = [];
1252
+ if (config.artifacts) {
1253
+ const artifacts = config.artifacts;
1254
+ artifactTools.push(defineRuntimeTool({
1255
+ name: AGENT_CODING_TOOL_NAMES.readOutput,
1256
+ description: "Read a bounded byte slice from one opaque coding output artifact.",
1257
+ identity: { serviceName: "coding", action: "read-artifact", method: "POST" },
1258
+ input: z4.object({
1259
+ reference: z4.string().min(1),
1260
+ offset: z4.int().nonnegative().default(0),
1261
+ maxBytes: z4.int().positive().optional()
1262
+ }).strict(),
1263
+ output: z4.object({
1264
+ reference: z4.string().min(1),
1265
+ offset: z4.int().nonnegative(),
1266
+ text: z4.string(),
1267
+ bytes: z4.int().nonnegative(),
1268
+ totalBytes: z4.int().nonnegative().optional()
1269
+ }).strict(),
1270
+ transports: ["AGENT"],
1271
+ handler: async ({ input }) => {
1272
+ const maxBytes = Math.min(input.maxBytes ?? limits.maxReadBytes, limits.maxReadBytes);
1273
+ await authorizeCodingTool(config, {
1274
+ operation: "artifact-read",
1275
+ reference: input.reference,
1276
+ offset: input.offset,
1277
+ maxBytes
1278
+ });
1279
+ const originAuthorization = await artifacts.authorization?.(input.reference);
1280
+ if (originAuthorization)
1281
+ await authorizeCodingTool(config, originAuthorization);
1282
+ const result = await artifacts.read({
1283
+ reference: input.reference,
1284
+ offset: input.offset,
1285
+ maxBytes
1286
+ });
1287
+ if (result.data.byteLength > maxBytes)
1288
+ throw new Error("Coding artifact store exceeded the requested byte budget");
1289
+ if (result.totalBytes !== undefined && result.totalBytes < input.offset + result.data.byteLength) {
1290
+ throw new Error("Coding artifact store returned inconsistent totalBytes");
1291
+ }
1292
+ return {
1293
+ reference: input.reference,
1294
+ offset: input.offset,
1295
+ text: new TextDecoder("utf-8", { fatal: true }).decode(result.data),
1296
+ bytes: result.data.byteLength,
1297
+ ...result.totalBytes !== undefined && { totalBytes: result.totalBytes }
1298
+ };
1299
+ }
1300
+ }));
1301
+ if (artifacts.search) {
1302
+ const search = artifacts.search;
1303
+ artifactTools.push(defineRuntimeTool({
1304
+ name: AGENT_CODING_TOOL_NAMES.searchOutput,
1305
+ description: "Search a durable spilled output without loading it into context.",
1306
+ identity: { serviceName: "coding", action: "search-artifact", method: "POST" },
1307
+ input: z4.object({
1308
+ reference: z4.string().min(1),
1309
+ query: z4.string().min(1),
1310
+ maxMatches: z4.int().positive().max(limits.maxSearchResults).default(20)
1311
+ }).strict(),
1312
+ output: z4.object({
1313
+ reference: z4.string(),
1314
+ matches: z4.array(z4.object({ line: z4.int().positive(), text: z4.string() }))
1315
+ }).strict(),
1316
+ transports: ["AGENT"],
1317
+ handler: async ({ input }) => {
1318
+ await authorizeCodingTool(config, {
1319
+ operation: "artifact-search",
1320
+ reference: input.reference,
1321
+ query: input.query,
1322
+ maxMatches: input.maxMatches
1323
+ });
1324
+ const originAuthorization = await artifacts.authorization?.(input.reference);
1325
+ if (originAuthorization)
1326
+ await authorizeCodingTool(config, originAuthorization);
1327
+ return {
1328
+ reference: input.reference,
1329
+ matches: [
1330
+ ...await search({
1331
+ reference: input.reference,
1332
+ query: input.query,
1333
+ maxMatches: input.maxMatches
1334
+ })
1335
+ ]
1336
+ };
1337
+ }
1338
+ }));
1339
+ }
1340
+ }
1341
+ const commandTools = Object.keys(config.executables ?? {}).length ? [createShellCodingTool(config, limits)] : [];
1342
+ return [
1343
+ ...createFileCodingTools(config, limits),
1344
+ ...createListingCodingTools(config, limits),
1345
+ ...createSearchAndPatchCodingTools(config, limits),
1346
+ ...commandTools,
1347
+ ...artifactTools
1348
+ ];
1349
+ }
1350
+
1351
+ export { codingRefusal, AgentCodingToolLimitsSchema, AgentCodingToolAuthorizationSchema, AgentCodingToolPathAuthorizationSchema, createAgentCodingTools };