@aria-cli/tools 1.0.12 → 1.0.14

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 (233) hide show
  1. package/dist/index.js +378 -70
  2. package/dist/network-runtime/index.js +8 -12
  3. package/dist-cjs/index.js +400 -435
  4. package/dist-cjs/network-runtime/index.js +8 -172
  5. package/package.json +8 -6
  6. package/dist/.tsbuildinfo +0 -1
  7. package/dist/ask-user-interaction.js +0 -22
  8. package/dist/cache/web-cache.js +0 -66
  9. package/dist/definitions/arion.js +0 -104
  10. package/dist/definitions/browser/browser.js +0 -418
  11. package/dist/definitions/browser/index.js +0 -4
  12. package/dist/definitions/browser/pw-downloads.js +0 -114
  13. package/dist/definitions/browser/pw-interactions.js +0 -199
  14. package/dist/definitions/browser/pw-responses.js +0 -76
  15. package/dist/definitions/browser/pw-session.js +0 -310
  16. package/dist/definitions/browser/pw-shared.js +0 -66
  17. package/dist/definitions/browser/pw-snapshot.js +0 -301
  18. package/dist/definitions/browser/pw-state.js +0 -62
  19. package/dist/definitions/browser/types.js +0 -4
  20. package/dist/definitions/code-intelligence.js +0 -470
  21. package/dist/definitions/core.js +0 -109
  22. package/dist/definitions/delegation.js +0 -512
  23. package/dist/definitions/deploy.js +0 -65
  24. package/dist/definitions/filesystem.js +0 -196
  25. package/dist/definitions/frg.js +0 -63
  26. package/dist/definitions/index.js +0 -20
  27. package/dist/definitions/memory.js +0 -123
  28. package/dist/definitions/messaging.js +0 -625
  29. package/dist/definitions/meta.js +0 -349
  30. package/dist/definitions/network.js +0 -159
  31. package/dist/definitions/outlook.js +0 -277
  32. package/dist/definitions/patch/apply-patch.js +0 -184
  33. package/dist/definitions/patch/fuzzy-match.js +0 -166
  34. package/dist/definitions/patch/index.js +0 -1
  35. package/dist/definitions/patch/patch-parser.js +0 -207
  36. package/dist/definitions/patch/sandbox-paths.js +0 -105
  37. package/dist/definitions/process/index.js +0 -4
  38. package/dist/definitions/process/process-registry.js +0 -213
  39. package/dist/definitions/process/process.js +0 -386
  40. package/dist/definitions/process/pty-keys.js +0 -254
  41. package/dist/definitions/process/session-slug.js +0 -142
  42. package/dist/definitions/quip.js +0 -195
  43. package/dist/definitions/search.js +0 -60
  44. package/dist/definitions/session-history.js +0 -69
  45. package/dist/definitions/shell.js +0 -181
  46. package/dist/definitions/slack.js +0 -180
  47. package/dist/definitions/web.js +0 -109
  48. package/dist/executors/apply-patch.js +0 -901
  49. package/dist/executors/arion.js +0 -119
  50. package/dist/executors/code-intelligence.js +0 -882
  51. package/dist/executors/deploy.js +0 -848
  52. package/dist/executors/filesystem.js +0 -1122
  53. package/dist/executors/frg-freshness.js +0 -576
  54. package/dist/executors/frg.js +0 -298
  55. package/dist/executors/index.js +0 -46
  56. package/dist/executors/learning-meta.js +0 -1146
  57. package/dist/executors/lsp-client.js +0 -296
  58. package/dist/executors/memory.js +0 -750
  59. package/dist/executors/meta.js +0 -220
  60. package/dist/executors/process-registry.js +0 -465
  61. package/dist/executors/pty-session-store.js +0 -30
  62. package/dist/executors/pty.js +0 -271
  63. package/dist/executors/restart.js +0 -119
  64. package/dist/executors/search-freshness.js +0 -195
  65. package/dist/executors/search-types.js +0 -52
  66. package/dist/executors/search.js +0 -66
  67. package/dist/executors/self-diagnose.js +0 -398
  68. package/dist/executors/session-history.js +0 -283
  69. package/dist/executors/shell-safety.js +0 -473
  70. package/dist/executors/shell.js +0 -954
  71. package/dist/executors/utils.js +0 -33
  72. package/dist/executors/web.js +0 -542
  73. package/dist/extraction/content-extraction.js +0 -235
  74. package/dist/extraction/index.js +0 -4
  75. package/dist/headless-control-contract.js +0 -967
  76. package/dist/local-control-http-auth.js +0 -2
  77. package/dist/mcp/client.js +0 -181
  78. package/dist/mcp/connection.js +0 -480
  79. package/dist/mcp/index.js +0 -10
  80. package/dist/mcp/jsonrpc.js +0 -144
  81. package/dist/mcp/types.js +0 -7
  82. package/dist/network-control-adapter.js +0 -72
  83. package/dist/network-runtime/address-types.js +0 -165
  84. package/dist/network-runtime/db-owner-fencing.js +0 -69
  85. package/dist/network-runtime/delivery-receipts.js +0 -267
  86. package/dist/network-runtime/direct-endpoint-authority.js +0 -25
  87. package/dist/network-runtime/local-control-contract.js +0 -627
  88. package/dist/network-runtime/node-store-contract.js +0 -34
  89. package/dist/network-runtime/pair-route-contract.js +0 -77
  90. package/dist/network-runtime/peer-capabilities.js +0 -28
  91. package/dist/network-runtime/peer-principal-ref.js +0 -12
  92. package/dist/network-runtime/peer-state-machine.js +0 -121
  93. package/dist/network-runtime/protocol-schemas.js +0 -205
  94. package/dist/network-runtime/runtime-bootstrap-contract.js +0 -60
  95. package/dist/outlook/desktop-session.js +0 -279
  96. package/dist/policy.js +0 -149
  97. package/dist/providers/brave.js +0 -62
  98. package/dist/providers/duckduckgo.js +0 -176
  99. package/dist/providers/exa.js +0 -63
  100. package/dist/providers/firecrawl.js +0 -55
  101. package/dist/providers/index.js +0 -7
  102. package/dist/providers/jina.js +0 -49
  103. package/dist/providers/router.js +0 -96
  104. package/dist/providers/search-provider.js +0 -32
  105. package/dist/providers/tavily.js +0 -54
  106. package/dist/quip/desktop-session.js +0 -317
  107. package/dist/registry/index.js +0 -1
  108. package/dist/registry/registry.js +0 -756
  109. package/dist/runtime-socket-local-control-client.js +0 -330
  110. package/dist/security/dns-normalization.js +0 -19
  111. package/dist/security/dns-pinning.js +0 -123
  112. package/dist/security/external-content.js +0 -91
  113. package/dist/security/ssrf.js +0 -181
  114. package/dist/slack/desktop-session.js +0 -324
  115. package/dist/tool-factory.js +0 -47
  116. package/dist/types.js +0 -7
  117. package/dist/utils/retry.js +0 -132
  118. package/dist/utils/safe-parse-json.js +0 -160
  119. package/dist/utils/url.js +0 -19
  120. package/dist-cjs/.tsbuildinfo +0 -1
  121. package/dist-cjs/ask-user-interaction.js +0 -27
  122. package/dist-cjs/cache/web-cache.js +0 -70
  123. package/dist-cjs/definitions/arion.js +0 -107
  124. package/dist-cjs/definitions/browser/browser.js +0 -421
  125. package/dist-cjs/definitions/browser/index.js +0 -8
  126. package/dist-cjs/definitions/browser/pw-downloads.js +0 -117
  127. package/dist-cjs/definitions/browser/pw-interactions.js +0 -213
  128. package/dist-cjs/definitions/browser/pw-responses.js +0 -84
  129. package/dist-cjs/definitions/browser/pw-session.js +0 -326
  130. package/dist-cjs/definitions/browser/pw-shared.js +0 -72
  131. package/dist-cjs/definitions/browser/pw-snapshot.js +0 -307
  132. package/dist-cjs/definitions/browser/pw-state.js +0 -70
  133. package/dist-cjs/definitions/browser/types.js +0 -5
  134. package/dist-cjs/definitions/code-intelligence.js +0 -473
  135. package/dist-cjs/definitions/core.js +0 -133
  136. package/dist-cjs/definitions/delegation.js +0 -515
  137. package/dist-cjs/definitions/deploy.js +0 -68
  138. package/dist-cjs/definitions/filesystem.js +0 -199
  139. package/dist-cjs/definitions/frg.js +0 -66
  140. package/dist-cjs/definitions/index.js +0 -43
  141. package/dist-cjs/definitions/memory.js +0 -126
  142. package/dist-cjs/definitions/messaging.js +0 -631
  143. package/dist-cjs/definitions/meta.js +0 -352
  144. package/dist-cjs/definitions/network.js +0 -162
  145. package/dist-cjs/definitions/outlook.js +0 -280
  146. package/dist-cjs/definitions/patch/apply-patch.js +0 -191
  147. package/dist-cjs/definitions/patch/fuzzy-match.js +0 -172
  148. package/dist-cjs/definitions/patch/index.js +0 -5
  149. package/dist-cjs/definitions/patch/patch-parser.js +0 -215
  150. package/dist-cjs/definitions/patch/sandbox-paths.js +0 -113
  151. package/dist-cjs/definitions/process/index.js +0 -8
  152. package/dist-cjs/definitions/process/process-registry.js +0 -231
  153. package/dist-cjs/definitions/process/process.js +0 -389
  154. package/dist-cjs/definitions/process/pty-keys.js +0 -259
  155. package/dist-cjs/definitions/process/session-slug.js +0 -145
  156. package/dist-cjs/definitions/quip.js +0 -198
  157. package/dist-cjs/definitions/search.js +0 -63
  158. package/dist-cjs/definitions/session-history.js +0 -72
  159. package/dist-cjs/definitions/shell.js +0 -184
  160. package/dist-cjs/definitions/slack.js +0 -183
  161. package/dist-cjs/definitions/web.js +0 -112
  162. package/dist-cjs/executors/apply-patch.js +0 -938
  163. package/dist-cjs/executors/arion.js +0 -125
  164. package/dist-cjs/executors/code-intelligence.js +0 -925
  165. package/dist-cjs/executors/deploy.js +0 -869
  166. package/dist-cjs/executors/filesystem.js +0 -1167
  167. package/dist-cjs/executors/frg-freshness.js +0 -627
  168. package/dist-cjs/executors/frg.js +0 -334
  169. package/dist-cjs/executors/index.js +0 -143
  170. package/dist-cjs/executors/learning-meta.js +0 -1165
  171. package/dist-cjs/executors/lsp-client.js +0 -310
  172. package/dist-cjs/executors/memory.js +0 -796
  173. package/dist-cjs/executors/meta.js +0 -226
  174. package/dist-cjs/executors/process-registry.js +0 -469
  175. package/dist-cjs/executors/pty-session-store.js +0 -34
  176. package/dist-cjs/executors/pty.js +0 -312
  177. package/dist-cjs/executors/restart.js +0 -155
  178. package/dist-cjs/executors/search-freshness.js +0 -234
  179. package/dist-cjs/executors/search-types.js +0 -56
  180. package/dist-cjs/executors/search.js +0 -102
  181. package/dist-cjs/executors/self-diagnose.js +0 -434
  182. package/dist-cjs/executors/session-history.js +0 -320
  183. package/dist-cjs/executors/shell-safety.js +0 -478
  184. package/dist-cjs/executors/shell.js +0 -1001
  185. package/dist-cjs/executors/utils.js +0 -73
  186. package/dist-cjs/executors/web.js +0 -547
  187. package/dist-cjs/extraction/content-extraction.js +0 -243
  188. package/dist-cjs/extraction/index.js +0 -8
  189. package/dist-cjs/headless-control-contract.js +0 -972
  190. package/dist-cjs/local-control-http-auth.js +0 -5
  191. package/dist-cjs/mcp/client.js +0 -185
  192. package/dist-cjs/mcp/connection.js +0 -484
  193. package/dist-cjs/mcp/index.js +0 -30
  194. package/dist-cjs/mcp/jsonrpc.js +0 -148
  195. package/dist-cjs/mcp/types.js +0 -8
  196. package/dist-cjs/network-control-adapter.js +0 -77
  197. package/dist-cjs/network-runtime/address-types.js +0 -168
  198. package/dist-cjs/network-runtime/db-owner-fencing.js +0 -76
  199. package/dist-cjs/network-runtime/delivery-receipts.js +0 -276
  200. package/dist-cjs/network-runtime/direct-endpoint-authority.js +0 -29
  201. package/dist-cjs/network-runtime/local-control-contract.js +0 -633
  202. package/dist-cjs/network-runtime/node-store-contract.js +0 -38
  203. package/dist-cjs/network-runtime/pair-route-contract.js +0 -80
  204. package/dist-cjs/network-runtime/peer-capabilities.js +0 -37
  205. package/dist-cjs/network-runtime/peer-principal-ref.js +0 -15
  206. package/dist-cjs/network-runtime/peer-state-machine.js +0 -129
  207. package/dist-cjs/network-runtime/protocol-schemas.js +0 -212
  208. package/dist-cjs/network-runtime/runtime-bootstrap-contract.js +0 -63
  209. package/dist-cjs/outlook/desktop-session.js +0 -318
  210. package/dist-cjs/policy.js +0 -155
  211. package/dist-cjs/providers/brave.js +0 -66
  212. package/dist-cjs/providers/duckduckgo.js +0 -180
  213. package/dist-cjs/providers/exa.js +0 -67
  214. package/dist-cjs/providers/firecrawl.js +0 -59
  215. package/dist-cjs/providers/index.js +0 -17
  216. package/dist-cjs/providers/jina.js +0 -53
  217. package/dist-cjs/providers/router.js +0 -100
  218. package/dist-cjs/providers/search-provider.js +0 -36
  219. package/dist-cjs/providers/tavily.js +0 -58
  220. package/dist-cjs/quip/desktop-session.js +0 -353
  221. package/dist-cjs/registry/index.js +0 -6
  222. package/dist-cjs/registry/registry.js +0 -761
  223. package/dist-cjs/runtime-socket-local-control-client.js +0 -367
  224. package/dist-cjs/security/dns-normalization.js +0 -22
  225. package/dist-cjs/security/dns-pinning.js +0 -160
  226. package/dist-cjs/security/external-content.js +0 -95
  227. package/dist-cjs/security/ssrf.js +0 -221
  228. package/dist-cjs/slack/desktop-session.js +0 -366
  229. package/dist-cjs/tool-factory.js +0 -50
  230. package/dist-cjs/types.js +0 -8
  231. package/dist-cjs/utils/retry.js +0 -169
  232. package/dist-cjs/utils/safe-parse-json.js +0 -164
  233. package/dist-cjs/utils/url.js +0 -23
@@ -1,901 +0,0 @@
1
- /**
2
- * @aria/tools - Apply Patch executor
3
- *
4
- * Parses and applies unified diff patches with:
5
- * - Path traversal protection (SECURITY-CRITICAL)
6
- * - Atomic writes (all-or-nothing application)
7
- * - Fuzzy hunk matching (offset tolerance)
8
- * - CRLF normalization
9
- */
10
- import * as crypto from "node:crypto";
11
- import * as fs from "node:fs/promises";
12
- import * as fsSync from "node:fs";
13
- import * as nodePath from "node:path";
14
- import { success, fail, getErrorMessage, isPathWithinBase } from "./utils.js";
15
- import { recordFrgMutation } from "./frg-freshness.js";
16
- import { recordSearchMutation } from "./search-freshness.js";
17
- function getErrnoCode(err) {
18
- if (typeof err !== "object" || err === null) {
19
- return undefined;
20
- }
21
- const withCode = err;
22
- return typeof withCode.code === "string" ? withCode.code : undefined;
23
- }
24
- // Maximum fuzzy offset when searching for hunk context
25
- const MAX_FUZZ_OFFSET = 3;
26
- const BEGIN_PATCH = "*** Begin Patch";
27
- const END_PATCH = "*** End Patch";
28
- const ADD_FILE = "*** Add File: ";
29
- const DELETE_FILE = "*** Delete File: ";
30
- const UPDATE_FILE = "*** Update File: ";
31
- const MOVE_TO = "*** Move to: ";
32
- const END_OF_FILE = "*** End of File";
33
- // ============================================================================
34
- // Unified Diff Parser
35
- // ============================================================================
36
- /**
37
- * Parse a unified diff string into an array of file diffs.
38
- * Handles standard unified diff and git-style diffs.
39
- */
40
- export function parseUnifiedDiff(patch) {
41
- // Normalize CRLF to LF
42
- const normalized = patch.replace(/\r\n/g, "\n");
43
- const lines = normalized.split("\n");
44
- const diffs = [];
45
- let i = 0;
46
- while (i < lines.length) {
47
- const line = lines[i];
48
- // Look for --- header
49
- if (line.startsWith("--- ")) {
50
- // Check for binary diff markers before this point
51
- // (we check later too, but early check is good)
52
- const nextLine = lines[i + 1];
53
- if (!nextLine || !nextLine.startsWith("+++ ")) {
54
- i++;
55
- continue;
56
- }
57
- const oldPath = parseDiffPath(line.slice(4));
58
- const newPath = parseDiffPath(nextLine.slice(4));
59
- i += 2;
60
- // Parse hunks for this file
61
- const hunks = [];
62
- while (i < lines.length) {
63
- const hunkLine = lines[i];
64
- if (hunkLine.startsWith("@@ ")) {
65
- const hunk = parseHunkHeader(hunkLine);
66
- if (!hunk) {
67
- throw new Error(`Invalid hunk header: ${hunkLine}`);
68
- }
69
- i++;
70
- // Collect hunk lines
71
- const hunkLines = [];
72
- let oldSeen = 0;
73
- let newSeen = 0;
74
- while (i < lines.length) {
75
- const l = lines[i];
76
- const prefix = l[0];
77
- if (prefix === " " || prefix === "+" || prefix === "-") {
78
- hunkLines.push(l);
79
- if (prefix === " " || prefix === "-")
80
- oldSeen++;
81
- if (prefix === " " || prefix === "+")
82
- newSeen++;
83
- i++;
84
- // Unified diff hunks define old/new line counts explicitly.
85
- // Stop when we consumed the declared line counts so a following
86
- // file header ("--- ...") is not misread as hunk content.
87
- if (oldSeen >= hunk.oldCount && newSeen >= hunk.newCount) {
88
- break;
89
- }
90
- }
91
- else if (l === "\") {
92
- // Skip this marker — we handle trailing newlines via content
93
- i++;
94
- }
95
- else {
96
- break;
97
- }
98
- }
99
- if (i < lines.length) {
100
- const next = lines[i];
101
- const nextPrefix = next[0];
102
- const looksLikeHunkLine = nextPrefix === " " ||
103
- (nextPrefix === "+" && !next.startsWith("+++ ")) ||
104
- (nextPrefix === "-" && !next.startsWith("--- "));
105
- if (looksLikeHunkLine) {
106
- throw new Error(`Malformed hunk: expected old/new counts ${hunk.oldCount}/${hunk.newCount} but found extra hunk lines`);
107
- }
108
- }
109
- if (oldSeen !== hunk.oldCount || newSeen !== hunk.newCount) {
110
- throw new Error(`Malformed hunk: expected old/new counts ${hunk.oldCount}/${hunk.newCount} but found ${oldSeen}/${newSeen}`);
111
- }
112
- hunks.push({ ...hunk, lines: hunkLines });
113
- }
114
- else if (hunkLine.startsWith("--- ") || hunkLine.startsWith("diff ") || hunkLine === "") {
115
- // Start of next file diff or blank line separator
116
- break;
117
- }
118
- else {
119
- // Skip git diff metadata lines (index, mode, etc.)
120
- i++;
121
- }
122
- }
123
- if (hunks.length > 0 || oldPath === null || newPath === null) {
124
- diffs.push({ oldPath, newPath, hunks });
125
- }
126
- }
127
- else if (line.startsWith("Binary files") || line.startsWith("GIT binary patch")) {
128
- throw new Error(`Binary diffs are not supported: ${line}`);
129
- }
130
- else {
131
- i++;
132
- }
133
- }
134
- return diffs;
135
- }
136
- function buildCountedHunk(lines) {
137
- let oldCount = 0;
138
- let newCount = 0;
139
- for (const line of lines) {
140
- if (line.startsWith(" ") || line.startsWith("-"))
141
- oldCount++;
142
- if (line.startsWith(" ") || line.startsWith("+"))
143
- newCount++;
144
- }
145
- return {
146
- oldStart: 1,
147
- oldCount,
148
- newStart: 1,
149
- newCount,
150
- lines,
151
- };
152
- }
153
- function parseHeaderPath(line, prefix) {
154
- const raw = line.slice(prefix.length).trim();
155
- if (!raw) {
156
- throw new Error(`Missing file path after header: ${prefix.trim()}`);
157
- }
158
- return raw;
159
- }
160
- function parseBeginPatchFormat(patch) {
161
- const lines = normalizePatchText(patch).split("\n");
162
- if ((lines[0] ?? "").trim() !== BEGIN_PATCH) {
163
- throw new Error("Invalid apply_patch envelope: missing *** Begin Patch");
164
- }
165
- const diffs = [];
166
- let i = 1;
167
- while (i < lines.length) {
168
- const line = lines[i] ?? "";
169
- if (line === END_PATCH) {
170
- return diffs;
171
- }
172
- if (line.trim() === "") {
173
- i++;
174
- continue;
175
- }
176
- if (line.startsWith(ADD_FILE)) {
177
- const newPath = parseHeaderPath(line, ADD_FILE);
178
- i++;
179
- const addLines = [];
180
- while (i < lines.length) {
181
- const current = lines[i] ?? "";
182
- if (current.startsWith("*** "))
183
- break;
184
- if (!current.startsWith("+")) {
185
- throw new Error(`Invalid add-file line (must start with '+'): ${current}`);
186
- }
187
- addLines.push(current);
188
- i++;
189
- }
190
- diffs.push({
191
- oldPath: null,
192
- newPath,
193
- hunks: [buildCountedHunk(addLines)],
194
- });
195
- continue;
196
- }
197
- if (line.startsWith(DELETE_FILE)) {
198
- const oldPath = parseHeaderPath(line, DELETE_FILE);
199
- diffs.push({ oldPath, newPath: null, hunks: [] });
200
- i++;
201
- continue;
202
- }
203
- if (line.startsWith(UPDATE_FILE)) {
204
- const oldPath = parseHeaderPath(line, UPDATE_FILE);
205
- i++;
206
- let newPath = oldPath;
207
- if ((lines[i] ?? "").startsWith(MOVE_TO)) {
208
- newPath = parseHeaderPath(lines[i], MOVE_TO);
209
- i++;
210
- }
211
- const hunks = [];
212
- let currentHunkLines = [];
213
- while (i < lines.length) {
214
- const current = lines[i] ?? "";
215
- if (current.startsWith("*** "))
216
- break;
217
- if (current.startsWith("@@")) {
218
- if (currentHunkLines.length > 0) {
219
- hunks.push(buildCountedHunk(currentHunkLines));
220
- currentHunkLines = [];
221
- }
222
- i++;
223
- continue;
224
- }
225
- if (current === END_OF_FILE) {
226
- i++;
227
- continue;
228
- }
229
- if (current.startsWith(" ") || current.startsWith("+") || current.startsWith("-")) {
230
- currentHunkLines.push(current);
231
- i++;
232
- continue;
233
- }
234
- throw new Error(`Invalid update-file line: ${current}`);
235
- }
236
- if (currentHunkLines.length > 0) {
237
- hunks.push(buildCountedHunk(currentHunkLines));
238
- }
239
- if (hunks.length === 0 && oldPath === newPath) {
240
- throw new Error(`Update section has no hunks: ${oldPath}`);
241
- }
242
- diffs.push({ oldPath, newPath, hunks });
243
- continue;
244
- }
245
- throw new Error(`Invalid apply_patch section header: ${line}`);
246
- }
247
- throw new Error("Invalid apply_patch envelope: missing *** End Patch");
248
- }
249
- function normalizePatchText(patch) {
250
- return patch.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
251
- }
252
- function parsePatchInput(patch) {
253
- const normalized = normalizePatchText(patch).trimStart();
254
- if (normalized.startsWith(BEGIN_PATCH)) {
255
- return parseBeginPatchFormat(normalized);
256
- }
257
- return parseUnifiedDiff(normalized);
258
- }
259
- /**
260
- * Parse a file path from a --- or +++ line.
261
- * Strips a/ or b/ git-style prefixes.
262
- * Returns null for /dev/null (new file or deleted file).
263
- */
264
- function parseDiffPath(raw) {
265
- // Remove trailing timestamp (e.g., "2024-01-01 00:00:00.000000000 +0000")
266
- const path = raw.replace(/\t.*$/, "").trim();
267
- if (path === "/dev/null") {
268
- return null;
269
- }
270
- // Strip git-style a/ or b/ prefix
271
- if (path.startsWith("a/") || path.startsWith("b/")) {
272
- return path.slice(2);
273
- }
274
- return path;
275
- }
276
- /**
277
- * Parse a hunk header line: @@ -oldStart,oldCount +newStart,newCount @@
278
- */
279
- function parseHunkHeader(line) {
280
- const match = line.match(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/);
281
- if (!match)
282
- return null;
283
- return {
284
- oldStart: parseInt(match[1], 10),
285
- oldCount: match[2] !== undefined ? parseInt(match[2], 10) : 1,
286
- newStart: parseInt(match[3], 10),
287
- newCount: match[4] !== undefined ? parseInt(match[4], 10) : 1,
288
- };
289
- }
290
- // ============================================================================
291
- // Path Validation (SECURITY-CRITICAL)
292
- // ============================================================================
293
- /**
294
- * Validate all paths in a parsed diff against path traversal attacks.
295
- *
296
- * SECURITY: This is the primary defense against malicious patches that
297
- * attempt to write outside the working directory.
298
- */
299
- function validatePatchPaths(diffs, cwd) {
300
- const resolved = new Map();
301
- for (const diff of diffs) {
302
- const paths = [diff.oldPath, diff.newPath].filter((p) => p !== null);
303
- for (const rawPath of paths) {
304
- if (resolved.has(rawPath))
305
- continue;
306
- // 1. Reject absolute paths
307
- if (nodePath.isAbsolute(rawPath)) {
308
- return {
309
- valid: false,
310
- error: `Absolute path not allowed in patch: ${rawPath}`,
311
- };
312
- }
313
- // 2. Reject paths with .. components
314
- const segments = rawPath.split(/[/\\]/);
315
- if (segments.includes("..")) {
316
- return {
317
- valid: false,
318
- error: `Path traversal (..) not allowed in patch: ${rawPath}`,
319
- };
320
- }
321
- // 3. Reject system paths (even though relative shouldn't reach these,
322
- // defense-in-depth against creative path construction)
323
- const systemPrefixes = ["/dev/", "/proc/", "/sys/", "/etc/"];
324
- for (const prefix of systemPrefixes) {
325
- if (rawPath.startsWith(prefix) || rawPath === prefix.slice(0, -1)) {
326
- return {
327
- valid: false,
328
- error: `System path not allowed in patch: ${rawPath}`,
329
- };
330
- }
331
- }
332
- // 4. Resolve relative to cwd
333
- const fullPath = nodePath.resolve(cwd, rawPath);
334
- // 5. Verify the resolved path is within cwd
335
- // Resolve symlinks on cwd to handle platforms where /tmp -> /private/tmp
336
- let realCwd = cwd;
337
- try {
338
- realCwd = fsSync.realpathSync(cwd);
339
- }
340
- catch {
341
- // Fall back to original cwd if it doesn't exist yet
342
- }
343
- // Resolve symlinks on the target path (walk up to nearest existing ancestor)
344
- let realPath = fullPath;
345
- try {
346
- realPath = fsSync.realpathSync(fullPath);
347
- }
348
- catch {
349
- // Path may not exist yet — walk up to find nearest existing ancestor
350
- let current = fullPath;
351
- let suffix = "";
352
- while (current !== nodePath.dirname(current)) {
353
- const parent = nodePath.dirname(current);
354
- suffix = suffix
355
- ? nodePath.join(nodePath.basename(current), suffix)
356
- : nodePath.basename(current);
357
- try {
358
- const realAncestor = fsSync.realpathSync(parent);
359
- realPath = nodePath.join(realAncestor, suffix);
360
- break;
361
- }
362
- catch {
363
- current = parent;
364
- }
365
- }
366
- }
367
- if (!isPathWithinBase(realPath, realCwd)) {
368
- return {
369
- valid: false,
370
- error: `Resolved path escapes working directory: ${rawPath} -> ${realPath} (cwd: ${realCwd})`,
371
- };
372
- }
373
- resolved.set(rawPath, realPath);
374
- }
375
- }
376
- return { valid: true, resolved };
377
- }
378
- // ============================================================================
379
- // Windows Drive Letter Detection
380
- // ============================================================================
381
- /**
382
- * Check if a path looks like a Windows drive letter (e.g., C:\, D:/)
383
- */
384
- function isWindowsDrivePath(p) {
385
- return /^[a-zA-Z]:[/\\]/.test(p);
386
- }
387
- // ============================================================================
388
- // Hunk Application
389
- // ============================================================================
390
- /**
391
- * Apply hunks to file content.
392
- * Uses fuzzy matching with a configurable offset tolerance.
393
- *
394
- * Returns the modified content or an error describing what went wrong.
395
- */
396
- function applyHunks(originalContent, hunks, filePath) {
397
- // Normalize CRLF
398
- const normalized = originalContent.replace(/\r\n/g, "\n");
399
- let lines = normalized.split("\n");
400
- // Track cumulative offset from insertions/deletions
401
- let lineOffset = 0;
402
- for (let hunkIdx = 0; hunkIdx < hunks.length; hunkIdx++) {
403
- const hunk = hunks[hunkIdx];
404
- // Extract context and removal lines (lines that must exist in original)
405
- const expectedLines = [];
406
- const newLines = [];
407
- for (const line of hunk.lines) {
408
- const prefix = line[0];
409
- const content = line.slice(1);
410
- if (prefix === " ") {
411
- expectedLines.push(content);
412
- newLines.push(content);
413
- }
414
- else if (prefix === "-") {
415
- expectedLines.push(content);
416
- }
417
- else if (prefix === "+") {
418
- newLines.push(content);
419
- }
420
- }
421
- // Find the position where context matches
422
- const expectedStart = hunk.oldStart - 1 + lineOffset; // Convert 1-based to 0-based
423
- let matchPos = -1;
424
- // Try exact position first, then fuzzy within tolerance
425
- for (let offset = 0; offset <= MAX_FUZZ_OFFSET; offset++) {
426
- for (const dir of [0, 1, -1]) {
427
- const tryPos = expectedStart + offset * (dir === 0 ? 0 : dir);
428
- if (dir === 0 && offset > 0)
429
- continue; // Skip duplicate 0-offset
430
- if (tryPos < 0 || tryPos + expectedLines.length > lines.length)
431
- continue;
432
- let matches = true;
433
- for (let j = 0; j < expectedLines.length; j++) {
434
- if (lines[tryPos + j] !== expectedLines[j]) {
435
- matches = false;
436
- break;
437
- }
438
- }
439
- if (matches) {
440
- matchPos = tryPos;
441
- break;
442
- }
443
- }
444
- if (matchPos >= 0)
445
- break;
446
- }
447
- if (matchPos < 0) {
448
- // Fallback: global scan for context (used by relaxed patch formats where line numbers may be approximate)
449
- const candidates = [];
450
- for (let pos = 0; pos + expectedLines.length <= lines.length; pos++) {
451
- let matches = true;
452
- for (let j = 0; j < expectedLines.length; j++) {
453
- if (lines[pos + j] !== expectedLines[j]) {
454
- matches = false;
455
- break;
456
- }
457
- }
458
- if (matches)
459
- candidates.push(pos);
460
- }
461
- if (candidates.length === 1) {
462
- matchPos = candidates[0];
463
- }
464
- else {
465
- // Build a helpful error message
466
- const contextPreview = expectedLines.slice(0, 3).join("\n ");
467
- const ambiguity = candidates.length > 1
468
- ? `Context matched ${candidates.length} locations; provide more surrounding lines.`
469
- : "Context not found.";
470
- return {
471
- ok: false,
472
- error: `Hunk ${hunkIdx + 1} failed to apply to ${filePath} ` +
473
- `(expected at line ${hunk.oldStart}, searched ±${MAX_FUZZ_OFFSET} lines). ` +
474
- `${ambiguity}\n ${contextPreview}`,
475
- };
476
- }
477
- }
478
- // Apply: replace the matched range with the new lines
479
- lines = [
480
- ...lines.slice(0, matchPos),
481
- ...newLines,
482
- ...lines.slice(matchPos + expectedLines.length),
483
- ];
484
- // Update offset for subsequent hunks
485
- lineOffset += newLines.length - expectedLines.length;
486
- }
487
- return { ok: true, content: lines.join("\n") };
488
- }
489
- // ============================================================================
490
- // Atomic Patch Application
491
- // ============================================================================
492
- /**
493
- * Apply all file diffs atomically.
494
- * Writes to temp files first, then renames on success.
495
- * On any failure, cleans up all temp files.
496
- */
497
- async function applyPatchAtomic(diffs, resolvedPaths) {
498
- const actions = [];
499
- const tempFiles = [];
500
- let totalAdded = 0;
501
- let totalRemoved = 0;
502
- try {
503
- // Phase 1: Compute all file actions (read + apply hunks)
504
- for (const diff of diffs) {
505
- const isNewFile = diff.oldPath === null;
506
- const isDeleteFile = diff.newPath === null;
507
- const sourcePath = diff.oldPath ? resolvedPaths.get(diff.oldPath) : undefined;
508
- const targetPath = diff.newPath ? resolvedPaths.get(diff.newPath) : undefined;
509
- if (isDeleteFile) {
510
- // Deletion: mark for removal
511
- if (!sourcePath) {
512
- return fail(`Patch references missing delete source path: ${diff.oldPath}`);
513
- }
514
- let sourceStat;
515
- try {
516
- sourceStat = await fs.stat(sourcePath);
517
- }
518
- catch (err) {
519
- return fail(`Cannot delete missing path: ${sourcePath}: ${getErrorMessage(err)}`);
520
- }
521
- if (sourceStat.isDirectory()) {
522
- return fail(`Patch delete targets a directory (unsupported): ${sourcePath}`);
523
- }
524
- actions.push({ resolvedPath: sourcePath, type: "delete" });
525
- // Count removed lines
526
- for (const hunk of diff.hunks) {
527
- for (const line of hunk.lines) {
528
- if (line.startsWith("-"))
529
- totalRemoved++;
530
- }
531
- }
532
- continue;
533
- }
534
- if (isNewFile) {
535
- if (!targetPath) {
536
- return fail(`Patch references missing create target path: ${diff.newPath}`);
537
- }
538
- // New file: build content from additions
539
- const contentLines = [];
540
- for (const hunk of diff.hunks) {
541
- for (const line of hunk.lines) {
542
- if (line.startsWith("+")) {
543
- contentLines.push(line.slice(1));
544
- totalAdded++;
545
- }
546
- }
547
- }
548
- const content = contentLines.join("\n") + "\n";
549
- actions.push({ resolvedPath: targetPath, type: "create", content });
550
- }
551
- else {
552
- if (!sourcePath || !targetPath) {
553
- return fail(`Patch references missing update source/target paths: ${diff.oldPath} -> ${diff.newPath}`);
554
- }
555
- // Modify existing file
556
- let originalContent;
557
- try {
558
- originalContent = await fs.readFile(sourcePath, "utf-8");
559
- }
560
- catch (err) {
561
- return fail(`Cannot read file for patching: ${sourcePath}: ${getErrorMessage(err)}`);
562
- }
563
- const result = applyHunks(originalContent, diff.hunks, diff.newPath);
564
- if (!result.ok) {
565
- return fail(result.error);
566
- }
567
- // Count additions and removals
568
- for (const hunk of diff.hunks) {
569
- for (const line of hunk.lines) {
570
- if (line.startsWith("+"))
571
- totalAdded++;
572
- else if (line.startsWith("-"))
573
- totalRemoved++;
574
- }
575
- }
576
- actions.push({
577
- sourcePath,
578
- resolvedPath: targetPath,
579
- type: sourcePath === targetPath ? "modify" : "move",
580
- content: result.content,
581
- });
582
- }
583
- }
584
- // Phase 2: Write all changes to temp files
585
- for (const action of actions) {
586
- if (action.type === "delete")
587
- continue;
588
- const dir = nodePath.dirname(action.resolvedPath);
589
- const base = nodePath.basename(action.resolvedPath);
590
- const suffix = crypto.randomBytes(6).toString("hex");
591
- const tempPath = nodePath.join(dir, `.${base}.patch-${suffix}`);
592
- // Ensure parent directory exists (needed for new files)
593
- await fs.mkdir(dir, { recursive: true });
594
- await fs.writeFile(tempPath, action.content, "utf-8");
595
- tempFiles.push(tempPath);
596
- }
597
- // Phase 3: Atomic rename — all temp files to final destinations
598
- // Back up targets and deletion sources for rollback on failure
599
- const targetBackups = new Map();
600
- const sourceRemovalBackups = new Map();
601
- const appliedTargets = [];
602
- const removedSources = [];
603
- const backupTargetIfExists = async (targetPath) => {
604
- if (targetBackups.has(targetPath)) {
605
- return;
606
- }
607
- const backupSuffix = crypto.randomBytes(6).toString("hex");
608
- const backupPath = targetPath + `.patch-backup-${backupSuffix}`;
609
- try {
610
- await fs.copyFile(targetPath, backupPath);
611
- targetBackups.set(targetPath, backupPath);
612
- }
613
- catch (err) {
614
- const code = getErrnoCode(err);
615
- if (code === "ENOENT") {
616
- return;
617
- }
618
- throw err;
619
- }
620
- };
621
- const backupSourceRemovalPath = async (sourcePath) => {
622
- if (sourceRemovalBackups.has(sourcePath)) {
623
- return;
624
- }
625
- const backupSuffix = crypto.randomBytes(6).toString("hex");
626
- const backupPath = sourcePath + `.patch-source-backup-${backupSuffix}`;
627
- await fs.copyFile(sourcePath, backupPath);
628
- sourceRemovalBackups.set(sourcePath, backupPath);
629
- };
630
- const unlinkSourcePath = async (sourcePath) => {
631
- try {
632
- await fs.unlink(sourcePath);
633
- removedSources.push(sourcePath);
634
- }
635
- catch (err) {
636
- const code = getErrnoCode(err);
637
- // If the path is already gone, desired end-state (removed) is satisfied.
638
- if (code === "ENOENT") {
639
- return;
640
- }
641
- throw err;
642
- }
643
- };
644
- try {
645
- // Backup all existing target files that may be overwritten.
646
- for (const action of actions) {
647
- if (action.type === "delete")
648
- continue;
649
- await backupTargetIfExists(action.resolvedPath);
650
- }
651
- // Backup all files that will be removed (delete + move source path).
652
- for (const action of actions) {
653
- if (action.type === "delete") {
654
- await backupSourceRemovalPath(action.resolvedPath);
655
- continue;
656
- }
657
- if (action.type === "move" &&
658
- action.sourcePath &&
659
- action.sourcePath !== action.resolvedPath) {
660
- await backupSourceRemovalPath(action.sourcePath);
661
- }
662
- }
663
- let tempIdx = 0;
664
- for (const action of actions) {
665
- if (action.type === "delete")
666
- continue;
667
- const tempPath = tempFiles[tempIdx];
668
- await fs.rename(tempPath, action.resolvedPath);
669
- appliedTargets.push(action.resolvedPath);
670
- tempIdx++;
671
- }
672
- // Apply delete/move source removals as part of the same transaction.
673
- for (const action of actions) {
674
- if (action.type === "delete") {
675
- await unlinkSourcePath(action.resolvedPath);
676
- }
677
- else if (action.type === "move" &&
678
- action.sourcePath &&
679
- action.sourcePath !== action.resolvedPath) {
680
- await unlinkSourcePath(action.sourcePath);
681
- }
682
- }
683
- }
684
- catch (renameErr) {
685
- // Rollback: restore backup-backed targets, remove newly-created targets
686
- for (let idx = appliedTargets.length - 1; idx >= 0; idx--) {
687
- const target = appliedTargets[idx];
688
- const backup = targetBackups.get(target);
689
- try {
690
- if (backup) {
691
- await fs.rename(backup, target);
692
- }
693
- else {
694
- await fs.unlink(target);
695
- }
696
- }
697
- catch {
698
- /* best-effort */
699
- }
700
- }
701
- // Rollback removed source paths (delete/move) in reverse order.
702
- for (let idx = removedSources.length - 1; idx >= 0; idx--) {
703
- const sourcePath = removedSources[idx];
704
- const backup = sourceRemovalBackups.get(sourcePath);
705
- if (!backup)
706
- continue;
707
- try {
708
- await fs.rename(backup, sourcePath);
709
- sourceRemovalBackups.delete(sourcePath);
710
- }
711
- catch {
712
- /* best-effort */
713
- }
714
- }
715
- // Clean up any remaining backup files
716
- for (const backup of targetBackups.values()) {
717
- try {
718
- await fs.unlink(backup);
719
- }
720
- catch {
721
- /* best-effort */
722
- }
723
- }
724
- for (const backup of sourceRemovalBackups.values()) {
725
- try {
726
- await fs.unlink(backup);
727
- }
728
- catch {
729
- /* best-effort */
730
- }
731
- }
732
- // Clean up any remaining temp files
733
- for (const tempPath of tempFiles) {
734
- try {
735
- await fs.unlink(tempPath);
736
- }
737
- catch {
738
- /* best-effort */
739
- }
740
- }
741
- throw renameErr; // Will be caught by outer try/catch
742
- }
743
- // Success — clean up backups
744
- for (const backup of targetBackups.values()) {
745
- try {
746
- await fs.unlink(backup);
747
- }
748
- catch {
749
- /* best-effort */
750
- }
751
- }
752
- for (const backup of sourceRemovalBackups.values()) {
753
- try {
754
- await fs.unlink(backup);
755
- }
756
- catch {
757
- /* best-effort */
758
- }
759
- }
760
- // Build summary
761
- const filesChanged = actions.length;
762
- const created = actions.filter((a) => a.type === "create").length;
763
- const modified = actions.filter((a) => a.type === "modify").length;
764
- const deleted = actions.filter((a) => a.type === "delete").length;
765
- const moved = actions.filter((a) => a.type === "move").length;
766
- const parts = [];
767
- if (created > 0)
768
- parts.push(`${created} created`);
769
- if (modified > 0)
770
- parts.push(`${modified} modified`);
771
- if (moved > 0)
772
- parts.push(`${moved} moved`);
773
- if (deleted > 0)
774
- parts.push(`${deleted} deleted`);
775
- const summary = `Patch applied: ${filesChanged} file${filesChanged !== 1 ? "s" : ""} ` +
776
- `(${parts.join(", ")}), +${totalAdded}/-${totalRemoved} lines`;
777
- for (const action of actions) {
778
- if (action.type === "delete") {
779
- recordFrgMutation(action.resolvedPath, "delete");
780
- recordSearchMutation(action.resolvedPath, "delete");
781
- }
782
- else {
783
- recordFrgMutation(action.resolvedPath, "write", action.content);
784
- recordSearchMutation(action.resolvedPath, "write", action.content);
785
- if (action.type === "move" &&
786
- action.sourcePath &&
787
- action.sourcePath !== action.resolvedPath) {
788
- recordFrgMutation(action.sourcePath, "delete");
789
- recordSearchMutation(action.sourcePath, "delete");
790
- }
791
- }
792
- }
793
- return success(summary, {
794
- filesChanged,
795
- created,
796
- modified,
797
- moved,
798
- deleted,
799
- linesAdded: totalAdded,
800
- linesRemoved: totalRemoved,
801
- files: actions.map((a) => ({
802
- path: a.type === "move" ? `${a.sourcePath} -> ${a.resolvedPath}` : a.resolvedPath,
803
- action: a.type,
804
- })),
805
- });
806
- }
807
- catch (err) {
808
- // Cleanup: remove all temp files on failure
809
- for (const tempPath of tempFiles) {
810
- try {
811
- await fs.unlink(tempPath);
812
- }
813
- catch {
814
- // Best-effort cleanup
815
- }
816
- }
817
- return fail(`Patch application failed: ${getErrorMessage(err)}`);
818
- }
819
- }
820
- // ============================================================================
821
- // Main Executor
822
- // ============================================================================
823
- /**
824
- * Execute the apply_patch tool.
825
- *
826
- * Parses a unified diff, validates all paths for security,
827
- * and applies changes atomically.
828
- */
829
- export async function executeApplyPatch(input, ctx) {
830
- try {
831
- if (!input.patch || input.patch.trim() === "") {
832
- return fail("Patch content is empty");
833
- }
834
- // Size guard: reject patches > 1MB to prevent memory exhaustion
835
- const MAX_PATCH_SIZE = 1_048_576; // 1MB
836
- if (input.patch.length > MAX_PATCH_SIZE) {
837
- return fail(`Patch too large: ${input.patch.length} bytes (max: ${MAX_PATCH_SIZE})`);
838
- }
839
- // Determine working directory
840
- const cwd = input.cwd ? nodePath.resolve(ctx.workingDir, input.cwd) : ctx.workingDir;
841
- // SECURITY: Validate cwd is within the working directory
842
- if (input.cwd) {
843
- let realCwd;
844
- try {
845
- realCwd = fsSync.realpathSync(cwd);
846
- }
847
- catch {
848
- return fail(`Working directory does not exist: ${cwd}`);
849
- }
850
- let realWorkingDir;
851
- try {
852
- realWorkingDir = fsSync.realpathSync(ctx.workingDir);
853
- }
854
- catch {
855
- return fail(`Base working directory does not exist: ${ctx.workingDir}`);
856
- }
857
- if (!isPathWithinBase(realCwd, realWorkingDir)) {
858
- return fail(`cwd must be within the working directory: ${input.cwd}`);
859
- }
860
- }
861
- // Verify cwd exists and is a directory
862
- try {
863
- const stat = await fs.stat(cwd);
864
- if (!stat.isDirectory()) {
865
- return fail(`Working directory is not a directory: ${cwd}`);
866
- }
867
- }
868
- catch {
869
- return fail(`Working directory does not exist: ${cwd}`);
870
- }
871
- // Step 1: Parse patch input (unified diff or apply_patch envelope)
872
- let diffs;
873
- try {
874
- diffs = parsePatchInput(input.patch);
875
- }
876
- catch (err) {
877
- return fail(`Failed to parse patch: ${getErrorMessage(err)}`);
878
- }
879
- if (diffs.length === 0) {
880
- return fail("No file diffs found in patch");
881
- }
882
- // Step 2: Validate paths (SECURITY-CRITICAL)
883
- // Check for Windows drive letters in paths (cross-platform safety)
884
- for (const diff of diffs) {
885
- for (const p of [diff.oldPath, diff.newPath]) {
886
- if (p !== null && isWindowsDrivePath(p)) {
887
- return fail(`Absolute path not allowed in patch: ${p}`);
888
- }
889
- }
890
- }
891
- const validation = validatePatchPaths(diffs, cwd);
892
- if (!validation.valid) {
893
- return fail(validation.error);
894
- }
895
- // Step 3: Apply atomically
896
- return await applyPatchAtomic(diffs, validation.resolved);
897
- }
898
- catch (err) {
899
- return fail(`apply_patch failed: ${getErrorMessage(err)}`);
900
- }
901
- }