agy-worker-mcp 0.3.1

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 (227) hide show
  1. package/CHANGELOG.md +141 -0
  2. package/LICENSE +21 -0
  3. package/README.md +309 -0
  4. package/commands/agy-ceiling.md +31 -0
  5. package/dist/broker/blockers.d.ts +86 -0
  6. package/dist/broker/blockers.d.ts.map +1 -0
  7. package/dist/broker/blockers.js +390 -0
  8. package/dist/broker/blockers.js.map +1 -0
  9. package/dist/broker/outcome.d.ts +85 -0
  10. package/dist/broker/outcome.d.ts.map +1 -0
  11. package/dist/broker/outcome.js +147 -0
  12. package/dist/broker/outcome.js.map +1 -0
  13. package/dist/broker/reconcile.d.ts +74 -0
  14. package/dist/broker/reconcile.d.ts.map +1 -0
  15. package/dist/broker/reconcile.js +415 -0
  16. package/dist/broker/reconcile.js.map +1 -0
  17. package/dist/broker/result.d.ts +88 -0
  18. package/dist/broker/result.d.ts.map +1 -0
  19. package/dist/broker/result.js +234 -0
  20. package/dist/broker/result.js.map +1 -0
  21. package/dist/broker/verify.d.ts +52 -0
  22. package/dist/broker/verify.d.ts.map +1 -0
  23. package/dist/broker/verify.js +174 -0
  24. package/dist/broker/verify.js.map +1 -0
  25. package/dist/contract/errors.d.ts +119 -0
  26. package/dist/contract/errors.d.ts.map +1 -0
  27. package/dist/contract/errors.js +99 -0
  28. package/dist/contract/errors.js.map +1 -0
  29. package/dist/contract/index.d.ts +10 -0
  30. package/dist/contract/index.d.ts.map +1 -0
  31. package/dist/contract/index.js +10 -0
  32. package/dist/contract/index.js.map +1 -0
  33. package/dist/contract/paths.d.ts +139 -0
  34. package/dist/contract/paths.d.ts.map +1 -0
  35. package/dist/contract/paths.js +359 -0
  36. package/dist/contract/paths.js.map +1 -0
  37. package/dist/contract/schema.sql +64 -0
  38. package/dist/contract/types.d.ts +1113 -0
  39. package/dist/contract/types.d.ts.map +1 -0
  40. package/dist/contract/types.js +114 -0
  41. package/dist/contract/types.js.map +1 -0
  42. package/dist/events/cursor.d.ts +65 -0
  43. package/dist/events/cursor.d.ts.map +1 -0
  44. package/dist/events/cursor.js +286 -0
  45. package/dist/events/cursor.js.map +1 -0
  46. package/dist/events/detect.d.ts +58 -0
  47. package/dist/events/detect.d.ts.map +1 -0
  48. package/dist/events/detect.js +183 -0
  49. package/dist/events/detect.js.map +1 -0
  50. package/dist/events/normalize.d.ts +30 -0
  51. package/dist/events/normalize.d.ts.map +1 -0
  52. package/dist/events/normalize.js +158 -0
  53. package/dist/events/normalize.js.map +1 -0
  54. package/dist/events/parse.d.ts +41 -0
  55. package/dist/events/parse.d.ts.map +1 -0
  56. package/dist/events/parse.js +89 -0
  57. package/dist/events/parse.js.map +1 -0
  58. package/dist/gate/bind.d.ts +28 -0
  59. package/dist/gate/bind.d.ts.map +1 -0
  60. package/dist/gate/bind.js +154 -0
  61. package/dist/gate/bind.js.map +1 -0
  62. package/dist/gate/gate.d.ts +139 -0
  63. package/dist/gate/gate.d.ts.map +1 -0
  64. package/dist/gate/gate.js +488 -0
  65. package/dist/gate/gate.js.map +1 -0
  66. package/dist/gate/hooks-file.d.ts +55 -0
  67. package/dist/gate/hooks-file.d.ts.map +1 -0
  68. package/dist/gate/hooks-file.js +104 -0
  69. package/dist/gate/hooks-file.js.map +1 -0
  70. package/dist/gate.d.ts +3 -0
  71. package/dist/gate.d.ts.map +1 -0
  72. package/dist/gate.js +20 -0
  73. package/dist/gate.js.map +1 -0
  74. package/dist/policy/ceiling-review.d.ts +22 -0
  75. package/dist/policy/ceiling-review.d.ts.map +1 -0
  76. package/dist/policy/ceiling-review.js +170 -0
  77. package/dist/policy/ceiling-review.js.map +1 -0
  78. package/dist/policy/ceiling.d.ts +128 -0
  79. package/dist/policy/ceiling.d.ts.map +1 -0
  80. package/dist/policy/ceiling.js +273 -0
  81. package/dist/policy/ceiling.js.map +1 -0
  82. package/dist/policy/containment.d.ts +100 -0
  83. package/dist/policy/containment.d.ts.map +1 -0
  84. package/dist/policy/containment.js +1071 -0
  85. package/dist/policy/containment.js.map +1 -0
  86. package/dist/policy/hard-deny.d.ts +28 -0
  87. package/dist/policy/hard-deny.d.ts.map +1 -0
  88. package/dist/policy/hard-deny.js +78 -0
  89. package/dist/policy/hard-deny.js.map +1 -0
  90. package/dist/policy/profiles.d.ts +82 -0
  91. package/dist/policy/profiles.d.ts.map +1 -0
  92. package/dist/policy/profiles.js +317 -0
  93. package/dist/policy/profiles.js.map +1 -0
  94. package/dist/policy/rules.d.ts +138 -0
  95. package/dist/policy/rules.d.ts.map +1 -0
  96. package/dist/policy/rules.js +1180 -0
  97. package/dist/policy/rules.js.map +1 -0
  98. package/dist/policy/seatbelt.d.ts +32 -0
  99. package/dist/policy/seatbelt.d.ts.map +1 -0
  100. package/dist/policy/seatbelt.js +59 -0
  101. package/dist/policy/seatbelt.js.map +1 -0
  102. package/dist/policy/tools.d.ts +36 -0
  103. package/dist/policy/tools.d.ts.map +1 -0
  104. package/dist/policy/tools.js +94 -0
  105. package/dist/policy/tools.js.map +1 -0
  106. package/dist/runner/gate-watchdog.d.ts +93 -0
  107. package/dist/runner/gate-watchdog.d.ts.map +1 -0
  108. package/dist/runner/gate-watchdog.js +153 -0
  109. package/dist/runner/gate-watchdog.js.map +1 -0
  110. package/dist/runner/idle.d.ts +51 -0
  111. package/dist/runner/idle.d.ts.map +1 -0
  112. package/dist/runner/idle.js +81 -0
  113. package/dist/runner/idle.js.map +1 -0
  114. package/dist/runner/inbox.d.ts +56 -0
  115. package/dist/runner/inbox.d.ts.map +1 -0
  116. package/dist/runner/inbox.js +134 -0
  117. package/dist/runner/inbox.js.map +1 -0
  118. package/dist/runner/reap.d.ts +65 -0
  119. package/dist/runner/reap.d.ts.map +1 -0
  120. package/dist/runner/reap.js +168 -0
  121. package/dist/runner/reap.js.map +1 -0
  122. package/dist/runner/runner.d.ts +51 -0
  123. package/dist/runner/runner.d.ts.map +1 -0
  124. package/dist/runner/runner.js +289 -0
  125. package/dist/runner/runner.js.map +1 -0
  126. package/dist/runner/spawn.d.ts +98 -0
  127. package/dist/runner/spawn.d.ts.map +1 -0
  128. package/dist/runner/spawn.js +230 -0
  129. package/dist/runner/spawn.js.map +1 -0
  130. package/dist/runner/verify.d.ts +24 -0
  131. package/dist/runner/verify.d.ts.map +1 -0
  132. package/dist/runner/verify.js +108 -0
  133. package/dist/runner/verify.js.map +1 -0
  134. package/dist/runner.d.ts +3 -0
  135. package/dist/runner.d.ts.map +1 -0
  136. package/dist/runner.js +15 -0
  137. package/dist/runner.js.map +1 -0
  138. package/dist/server/context.d.ts +41 -0
  139. package/dist/server/context.d.ts.map +1 -0
  140. package/dist/server/context.js +59 -0
  141. package/dist/server/context.js.map +1 -0
  142. package/dist/server/instructions.d.ts +13 -0
  143. package/dist/server/instructions.d.ts.map +1 -0
  144. package/dist/server/instructions.js +121 -0
  145. package/dist/server/instructions.js.map +1 -0
  146. package/dist/server/server.d.ts +20 -0
  147. package/dist/server/server.d.ts.map +1 -0
  148. package/dist/server/server.js +55 -0
  149. package/dist/server/server.js.map +1 -0
  150. package/dist/server/tools/cancel.d.ts +24 -0
  151. package/dist/server/tools/cancel.d.ts.map +1 -0
  152. package/dist/server/tools/cancel.js +59 -0
  153. package/dist/server/tools/cancel.js.map +1 -0
  154. package/dist/server/tools/capabilities.d.ts +36 -0
  155. package/dist/server/tools/capabilities.d.ts.map +1 -0
  156. package/dist/server/tools/capabilities.js +116 -0
  157. package/dist/server/tools/capabilities.js.map +1 -0
  158. package/dist/server/tools/ceiling.d.ts +31 -0
  159. package/dist/server/tools/ceiling.d.ts.map +1 -0
  160. package/dist/server/tools/ceiling.js +121 -0
  161. package/dist/server/tools/ceiling.js.map +1 -0
  162. package/dist/server/tools/index.d.ts +38 -0
  163. package/dist/server/tools/index.d.ts.map +1 -0
  164. package/dist/server/tools/index.js +115 -0
  165. package/dist/server/tools/index.js.map +1 -0
  166. package/dist/server/tools/listJobs.d.ts +30 -0
  167. package/dist/server/tools/listJobs.d.ts.map +1 -0
  168. package/dist/server/tools/listJobs.js +37 -0
  169. package/dist/server/tools/listJobs.js.map +1 -0
  170. package/dist/server/tools/logs.d.ts +31 -0
  171. package/dist/server/tools/logs.d.ts.map +1 -0
  172. package/dist/server/tools/logs.js +96 -0
  173. package/dist/server/tools/logs.js.map +1 -0
  174. package/dist/server/tools/result.d.ts +32 -0
  175. package/dist/server/tools/result.d.ts.map +1 -0
  176. package/dist/server/tools/result.js +95 -0
  177. package/dist/server/tools/result.js.map +1 -0
  178. package/dist/server/tools/send.d.ts +37 -0
  179. package/dist/server/tools/send.d.ts.map +1 -0
  180. package/dist/server/tools/send.js +71 -0
  181. package/dist/server/tools/send.js.map +1 -0
  182. package/dist/server/tools/sessions.d.ts +28 -0
  183. package/dist/server/tools/sessions.d.ts.map +1 -0
  184. package/dist/server/tools/sessions.js +54 -0
  185. package/dist/server/tools/sessions.js.map +1 -0
  186. package/dist/server/tools/start.d.ts +107 -0
  187. package/dist/server/tools/start.d.ts.map +1 -0
  188. package/dist/server/tools/start.js +482 -0
  189. package/dist/server/tools/start.js.map +1 -0
  190. package/dist/server/tools/wait.d.ts +29 -0
  191. package/dist/server/tools/wait.d.ts.map +1 -0
  192. package/dist/server/tools/wait.js +120 -0
  193. package/dist/server/tools/wait.js.map +1 -0
  194. package/dist/server.d.ts +3 -0
  195. package/dist/server.d.ts.map +1 -0
  196. package/dist/server.js +17 -0
  197. package/dist/server.js.map +1 -0
  198. package/dist/setup/install.d.ts +52 -0
  199. package/dist/setup/install.d.ts.map +1 -0
  200. package/dist/setup/install.js +108 -0
  201. package/dist/setup/install.js.map +1 -0
  202. package/dist/setup.d.ts +3 -0
  203. package/dist/setup.d.ts.map +1 -0
  204. package/dist/setup.js +24 -0
  205. package/dist/setup.js.map +1 -0
  206. package/dist/store/db.d.ts +49 -0
  207. package/dist/store/db.d.ts.map +1 -0
  208. package/dist/store/db.js +101 -0
  209. package/dist/store/db.js.map +1 -0
  210. package/dist/store/jobs.d.ts +45 -0
  211. package/dist/store/jobs.d.ts.map +1 -0
  212. package/dist/store/jobs.js +150 -0
  213. package/dist/store/jobs.js.map +1 -0
  214. package/dist/store/locks.d.ts +60 -0
  215. package/dist/store/locks.d.ts.map +1 -0
  216. package/dist/store/locks.js +172 -0
  217. package/dist/store/locks.js.map +1 -0
  218. package/dist/store/sessions.d.ts +37 -0
  219. package/dist/store/sessions.d.ts.map +1 -0
  220. package/dist/store/sessions.js +117 -0
  221. package/dist/store/sessions.js.map +1 -0
  222. package/dist/trace/digest.d.ts +77 -0
  223. package/dist/trace/digest.d.ts.map +1 -0
  224. package/dist/trace/digest.js +481 -0
  225. package/dist/trace/digest.js.map +1 -0
  226. package/package.json +68 -0
  227. package/skills/agy-ceiling/SKILL.md +72 -0
@@ -0,0 +1,1071 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { isAbsolute, join, parse as parsePath, resolve } from 'node:path';
4
+ import { assertContained, canonicalize, containedOrNull, isWithin, stateHome } from '../contract/paths.js';
5
+ import { ValidationError } from '../contract/errors.js';
6
+ import { isDeniedEnvVar, splitChainSegments, stripEnvPrefixTokens, tokenizeCommand } from './rules.js';
7
+ /**
8
+ * Roots for a job. Write is the workspace alone. Read may include a small set of
9
+ * additional canonical directories, but never credential paths.
10
+ */
11
+ export function buildRoots(workspace, extraRead) {
12
+ const canonicalWorkspace = canonicalize(workspace);
13
+ const read = [canonicalWorkspace, ...(extraRead ?? []).map(canonicalize)];
14
+ return { read, write: [canonicalWorkspace] };
15
+ }
16
+ /** @throws {import('../contract/errors.js').PathEscapeError} */
17
+ export function checkRead(path, roots) {
18
+ return assertContained(path, roots.read, 'read');
19
+ }
20
+ /** @throws {import('../contract/errors.js').PathEscapeError} */
21
+ export function checkWrite(path, roots) {
22
+ return assertContained(path, roots.write, 'write');
23
+ }
24
+ /** A bare, unquoted, absolute-looking token or NAME=/abs/path assignment inside a shell command line. */
25
+ export const ABS_PATH_TOKEN = /^(?:[A-Za-z_][A-Za-z0-9_]*=)?["']?(\/[^\s'"]*)["']?$/;
26
+ /**
27
+ * Allowed non-workspace read targets (special pseudo-devices).
28
+ * Kept strictly to the standard dev sinks/sources documented in gate policy:
29
+ * /dev/null, /dev/stdin, /dev/stdout, /dev/stderr.
30
+ * All other paths outside workspace/read_roots are denied.
31
+ */
32
+ export const ALLOWED_DEV_READS = new Set([
33
+ '/dev/null',
34
+ '/dev/stdin',
35
+ '/dev/stdout',
36
+ '/dev/stderr',
37
+ ]);
38
+ export function getCanonicalHome() {
39
+ try {
40
+ return canonicalize(homedir());
41
+ }
42
+ catch {
43
+ return homedir();
44
+ }
45
+ }
46
+ /**
47
+ * Normalises a path token before containment:
48
+ * - a leading `~/` or bare `~` is expanded to the real home directory;
49
+ * - a glob (`*`, `?`, `[`) is cut back to its literal directory prefix
50
+ * (`src/*.ts` → `src`, `*.ts` → `.`, `dist/**​/x` → `dist`), because a shell
51
+ * glob can only ever expand inside the directory named before the first
52
+ * glob character — containing that directory contains every match.
53
+ * Anything else (`$VAR`, backticks, a non-leading `~`) is left as is for
54
+ * {@link hasUnexpandedChars} to reject.
55
+ */
56
+ export function expandLeadingTilde(token) {
57
+ const home = getCanonicalHome();
58
+ let t = token;
59
+ if (t === '~')
60
+ t = home;
61
+ else if (t.startsWith('~/'))
62
+ t = join(home, t.slice(2));
63
+ const glob = t.search(/[*?[]/);
64
+ if (glob !== -1) {
65
+ const prefix = t.slice(0, glob);
66
+ const slash = prefix.lastIndexOf('/');
67
+ t = slash === -1 ? '.' : slash === 0 ? '/' : prefix.slice(0, slash);
68
+ }
69
+ return t;
70
+ }
71
+ /**
72
+ * True when a token still carries a shell expansion the gate cannot resolve:
73
+ * `$VAR`/`$HOME`/`$(…)`, backticks, or a `~` that is not the leading one
74
+ * (checked after {@link expandLeadingTilde}). Globs are not listed — they are
75
+ * cut back to their literal directory by `expandLeadingTilde` instead.
76
+ */
77
+ export function hasUnexpandedChars(token) {
78
+ return /[$`~]/.test(token);
79
+ }
80
+ /**
81
+ * Tests whether a token looks like a path argument:
82
+ * - absolute (starts with / or isAbsolute)
83
+ * - starts with ~/ or is ~
84
+ * - starts with ./ or ../
85
+ * - contains /
86
+ * - matches an existing file relative to the workspace
87
+ */
88
+ export function isPathLike(token, workspace) {
89
+ if (!token || token === '-' || token === '--')
90
+ return false;
91
+ if (token.startsWith('-'))
92
+ return false;
93
+ if (token === '~' || token.startsWith('~/'))
94
+ return true;
95
+ if (isAbsolute(token) || token.startsWith('/'))
96
+ return true;
97
+ if (token.startsWith('./') || token.startsWith('../'))
98
+ return true;
99
+ if (token.includes('/'))
100
+ return true;
101
+ if (workspace) {
102
+ try {
103
+ if (existsSync(resolve(workspace, token)))
104
+ return true;
105
+ }
106
+ catch {
107
+ // ignore
108
+ }
109
+ }
110
+ return false;
111
+ }
112
+ export const MUTATION_COMMANDS = new Set([
113
+ 'rm',
114
+ 'mv',
115
+ 'cp',
116
+ 'touch',
117
+ 'mkdir',
118
+ 'install',
119
+ 'ln',
120
+ 'truncate',
121
+ 'rsync',
122
+ 'tee',
123
+ ]);
124
+ export const READ_UTILITIES = new Set([
125
+ 'cat',
126
+ 'head',
127
+ 'tail',
128
+ 'less',
129
+ 'more',
130
+ 'grep',
131
+ 'egrep',
132
+ 'fgrep',
133
+ 'rg',
134
+ 'sed',
135
+ 'awk',
136
+ 'wc',
137
+ 'sort',
138
+ 'uniq',
139
+ 'cut',
140
+ 'diff',
141
+ 'cmp',
142
+ 'find',
143
+ 'od',
144
+ 'xxd',
145
+ 'hexdump',
146
+ 'strings',
147
+ ]);
148
+ function extractBasename(token) {
149
+ const idx = token.lastIndexOf('/');
150
+ return idx === -1 ? token : token.slice(idx + 1);
151
+ }
152
+ function extractSedWriteTargets(tokens) {
153
+ const hasInPlace = tokens.some((t) => t === '-i' || t.startsWith('-i') || t === '--in-place' || t.startsWith('--in-place='));
154
+ if (!hasInPlace)
155
+ return [];
156
+ const targets = [];
157
+ let i = 1;
158
+ let hasScript = false;
159
+ while (i < tokens.length) {
160
+ const token = tokens[i];
161
+ if (token === '--') {
162
+ i++;
163
+ while (i < tokens.length)
164
+ targets.push(tokens[i++]);
165
+ break;
166
+ }
167
+ if (token === '-i') {
168
+ i++;
169
+ if (i < tokens.length) {
170
+ const next = tokens[i];
171
+ if (next === '' || /^\.[A-Za-z0-9_-]+$/.test(next)) {
172
+ i++;
173
+ }
174
+ }
175
+ continue;
176
+ }
177
+ if (token.startsWith('-i') || token.startsWith('--in-place')) {
178
+ i++;
179
+ continue;
180
+ }
181
+ if (token === '-e' || token === '--expression') {
182
+ i += 2;
183
+ hasScript = true;
184
+ continue;
185
+ }
186
+ if (token.startsWith('-e') || token.startsWith('--expression=')) {
187
+ i++;
188
+ hasScript = true;
189
+ continue;
190
+ }
191
+ if (token === '-f' || token === '--file') {
192
+ i += 2;
193
+ hasScript = true;
194
+ continue;
195
+ }
196
+ if (token.startsWith('-f') || token.startsWith('--file=')) {
197
+ i++;
198
+ hasScript = true;
199
+ continue;
200
+ }
201
+ if (token.startsWith('-')) {
202
+ i++;
203
+ continue;
204
+ }
205
+ if (!hasScript) {
206
+ hasScript = true;
207
+ i++;
208
+ continue;
209
+ }
210
+ targets.push(token);
211
+ i++;
212
+ }
213
+ return targets;
214
+ }
215
+ function extractSortWriteTargets(tokens) {
216
+ const targets = [];
217
+ for (let i = 1; i < tokens.length; i++) {
218
+ const token = tokens[i];
219
+ if (token === '-o' && i + 1 < tokens.length) {
220
+ targets.push(tokens[++i]);
221
+ }
222
+ else if (token.startsWith('-o') && token.length > 2) {
223
+ targets.push(token.slice(2));
224
+ }
225
+ else if (token === '--output' && i + 1 < tokens.length) {
226
+ targets.push(tokens[++i]);
227
+ }
228
+ else if (token.startsWith('--output=')) {
229
+ targets.push(token.slice('--output='.length));
230
+ }
231
+ }
232
+ return targets;
233
+ }
234
+ function extractTeeWriteTargets(tokens) {
235
+ const targets = [];
236
+ let afterDoubleDash = false;
237
+ for (let i = 1; i < tokens.length; i++) {
238
+ const token = tokens[i];
239
+ if (!afterDoubleDash) {
240
+ if (token === '--') {
241
+ afterDoubleDash = true;
242
+ continue;
243
+ }
244
+ if (token.startsWith('-'))
245
+ continue;
246
+ }
247
+ targets.push(token);
248
+ }
249
+ return targets;
250
+ }
251
+ function extractGitWriteTargets(tokens, cwd) {
252
+ const targets = [];
253
+ let subCmdIdx = 1;
254
+ while (subCmdIdx < tokens.length && tokens[subCmdIdx].startsWith('-')) {
255
+ subCmdIdx++;
256
+ }
257
+ if (subCmdIdx >= tokens.length)
258
+ return [];
259
+ const subCmd = tokens[subCmdIdx];
260
+ const rest = tokens.slice(subCmdIdx + 1);
261
+ if (subCmd === 'archive') {
262
+ for (let i = 0; i < rest.length; i++) {
263
+ const t = rest[i];
264
+ if ((t === '-o' || t === '--output') && i + 1 < rest.length) {
265
+ targets.push(rest[++i]);
266
+ }
267
+ else if (t.startsWith('--output=')) {
268
+ targets.push(t.slice('--output='.length));
269
+ }
270
+ else if (t.startsWith('-o') && t.length > 2) {
271
+ targets.push(t.slice(2));
272
+ }
273
+ }
274
+ }
275
+ else if (subCmd === 'clone') {
276
+ const positionals = rest.filter((t) => !t.startsWith('-'));
277
+ if (positionals.length >= 2) {
278
+ targets.push(positionals[1]);
279
+ }
280
+ else if (positionals.length === 1) {
281
+ const repoName = extractBasename(positionals[0]).replace(/\.git$/, '');
282
+ if (repoName)
283
+ targets.push(repoName);
284
+ }
285
+ }
286
+ else if (subCmd === 'init') {
287
+ const positionals = rest.filter((t) => !t.startsWith('-'));
288
+ if (positionals.length >= 1) {
289
+ targets.push(positionals[0]);
290
+ }
291
+ else if (cwd) {
292
+ targets.push(cwd);
293
+ }
294
+ }
295
+ else if (subCmd === 'worktree') {
296
+ if (rest[0] === 'add') {
297
+ const positionals = rest.slice(1).filter((t) => !t.startsWith('-'));
298
+ if (positionals.length >= 1) {
299
+ targets.push(positionals[0]);
300
+ }
301
+ }
302
+ }
303
+ return targets;
304
+ }
305
+ function extractDdWriteTargets(tokens) {
306
+ const targets = [];
307
+ for (let i = 1; i < tokens.length; i++) {
308
+ const t = tokens[i];
309
+ if (t.startsWith('of=')) {
310
+ targets.push(t.slice(3));
311
+ }
312
+ }
313
+ return targets;
314
+ }
315
+ function extractTarWriteTargets(tokens) {
316
+ const targets = [];
317
+ const isExtract = tokens.some((t) => t.includes('x'));
318
+ const isCreate = tokens.some((t) => t.includes('c'));
319
+ if (isExtract) {
320
+ for (let i = 1; i < tokens.length; i++) {
321
+ const t = tokens[i];
322
+ if ((t === '-C' || t === '--directory') && i + 1 < tokens.length) {
323
+ targets.push(tokens[++i]);
324
+ }
325
+ else if (t.startsWith('--directory=')) {
326
+ targets.push(t.slice('--directory='.length));
327
+ }
328
+ }
329
+ }
330
+ else if (isCreate) {
331
+ for (let i = 1; i < tokens.length; i++) {
332
+ const t = tokens[i];
333
+ if ((t === '-f' || t === '--file') && i + 1 < tokens.length) {
334
+ targets.push(tokens[++i]);
335
+ }
336
+ else if (t.startsWith('--file=')) {
337
+ targets.push(t.slice('--file='.length));
338
+ }
339
+ else if (t.startsWith('-') && t.includes('f') && i + 1 < tokens.length) {
340
+ targets.push(tokens[++i]);
341
+ }
342
+ }
343
+ }
344
+ return targets;
345
+ }
346
+ function extractUnzipWriteTargets(tokens) {
347
+ const targets = [];
348
+ for (let i = 1; i < tokens.length; i++) {
349
+ const t = tokens[i];
350
+ if (t === '-d' && i + 1 < tokens.length) {
351
+ targets.push(tokens[++i]);
352
+ }
353
+ }
354
+ return targets;
355
+ }
356
+ function extractJavacWriteTargets(tokens) {
357
+ const targets = [];
358
+ for (let i = 1; i < tokens.length; i++) {
359
+ const t = tokens[i];
360
+ if (t === '-d' && i + 1 < tokens.length) {
361
+ targets.push(tokens[++i]);
362
+ }
363
+ }
364
+ return targets;
365
+ }
366
+ function extractInstallWriteTargets(tokens) {
367
+ const positionals = tokens.slice(1).filter((t) => !t.startsWith('-'));
368
+ const hasD = tokens.some((t) => t === '-d' || t === '--directory');
369
+ if (hasD)
370
+ return positionals;
371
+ const targetDirIdx = tokens.findIndex((t) => t === '-t' || t === '--target-directory');
372
+ if (targetDirIdx !== -1 && targetDirIdx + 1 < tokens.length) {
373
+ return [tokens[targetDirIdx + 1]];
374
+ }
375
+ if (positionals.length >= 1) {
376
+ return [positionals[positionals.length - 1]];
377
+ }
378
+ return [];
379
+ }
380
+ function extractLnWriteTargets(tokens) {
381
+ const positionals = tokens.slice(1).filter((t) => !t.startsWith('-'));
382
+ const targetDirIdx = tokens.findIndex((t) => t === '-t' || t === '--target-directory');
383
+ if (targetDirIdx !== -1 && targetDirIdx + 1 < tokens.length) {
384
+ return [tokens[targetDirIdx + 1]];
385
+ }
386
+ if (positionals.length >= 1) {
387
+ return [positionals[positionals.length - 1]];
388
+ }
389
+ return [];
390
+ }
391
+ function extractTruncateWriteTargets(tokens) {
392
+ const targets = [];
393
+ let i = 1;
394
+ while (i < tokens.length) {
395
+ const t = tokens[i];
396
+ if (t === '-s' || t === '--size') {
397
+ i += 2;
398
+ continue;
399
+ }
400
+ if (t.startsWith('-')) {
401
+ i++;
402
+ continue;
403
+ }
404
+ targets.push(t);
405
+ i++;
406
+ }
407
+ return targets;
408
+ }
409
+ function extractRsyncWriteTargets(tokens) {
410
+ const positionals = tokens.slice(1).filter((t) => !t.startsWith('-'));
411
+ if (positionals.length >= 2) {
412
+ return [positionals[positionals.length - 1]];
413
+ }
414
+ return [];
415
+ }
416
+ /**
417
+ * Extract target write paths for mutating commands.
418
+ * Includes rm, mv, cp, touch, mkdir, sed -i, sort -o, tee, git archive/clone/init/worktree,
419
+ * install, ln, truncate, dd of=, tar -C/-f, unzip -d, rsync, javac -d.
420
+ * Recurses into bash -c / sh -c.
421
+ */
422
+ export function commandMutationWritePaths(commandLine, cwd) {
423
+ const segments = splitChainSegments(commandLine) ?? [commandLine];
424
+ const writePaths = [];
425
+ for (const segment of segments) {
426
+ const tokens = tokenizeCommand(segment);
427
+ if (tokens.length === 0)
428
+ continue;
429
+ const stripped = stripEnvPrefixTokens(tokens);
430
+ if (stripped.remainingTokens.length === 0)
431
+ continue;
432
+ const head = extractBasename(stripped.remainingTokens[0]);
433
+ if (['bash', 'sh', 'zsh'].includes(head)) {
434
+ const cIdx = stripped.remainingTokens.indexOf('-c');
435
+ if (cIdx !== -1 && cIdx + 1 < stripped.remainingTokens.length) {
436
+ writePaths.push(...commandMutationWritePaths(stripped.remainingTokens[cIdx + 1], cwd));
437
+ }
438
+ continue;
439
+ }
440
+ let rawTargets = [];
441
+ if (head === 'sed') {
442
+ rawTargets = extractSedWriteTargets(stripped.remainingTokens);
443
+ }
444
+ else if (head === 'sort') {
445
+ rawTargets = extractSortWriteTargets(stripped.remainingTokens);
446
+ }
447
+ else if (head === 'tee') {
448
+ rawTargets = extractTeeWriteTargets(stripped.remainingTokens);
449
+ }
450
+ else if (head === 'git') {
451
+ rawTargets = extractGitWriteTargets(stripped.remainingTokens, cwd);
452
+ }
453
+ else if (head === 'dd') {
454
+ rawTargets = extractDdWriteTargets(stripped.remainingTokens);
455
+ }
456
+ else if (head === 'tar') {
457
+ rawTargets = extractTarWriteTargets(stripped.remainingTokens);
458
+ }
459
+ else if (head === 'unzip') {
460
+ rawTargets = extractUnzipWriteTargets(stripped.remainingTokens);
461
+ }
462
+ else if (head === 'javac') {
463
+ rawTargets = extractJavacWriteTargets(stripped.remainingTokens);
464
+ }
465
+ else if (head === 'install') {
466
+ rawTargets = extractInstallWriteTargets(stripped.remainingTokens);
467
+ }
468
+ else if (head === 'ln') {
469
+ rawTargets = extractLnWriteTargets(stripped.remainingTokens);
470
+ }
471
+ else if (head === 'truncate') {
472
+ rawTargets = extractTruncateWriteTargets(stripped.remainingTokens);
473
+ }
474
+ else if (head === 'rsync') {
475
+ rawTargets = extractRsyncWriteTargets(stripped.remainingTokens);
476
+ }
477
+ else if (head === 'cp') {
478
+ const positionals = stripped.remainingTokens.slice(1).filter((t) => !t.startsWith('-'));
479
+ if (positionals.length >= 1) {
480
+ rawTargets = [positionals[positionals.length - 1]];
481
+ }
482
+ }
483
+ else if (head === 'rm' || head === 'touch' || head === 'mkdir' || head === 'mv') {
484
+ let afterDoubleDash = false;
485
+ for (let i = 1; i < stripped.remainingTokens.length; i++) {
486
+ const token = stripped.remainingTokens[i];
487
+ if (!token)
488
+ continue;
489
+ if (!afterDoubleDash) {
490
+ if (token === '--') {
491
+ afterDoubleDash = true;
492
+ continue;
493
+ }
494
+ if (token.startsWith('-')) {
495
+ continue;
496
+ }
497
+ }
498
+ rawTargets.push(token);
499
+ }
500
+ }
501
+ for (const raw of rawTargets) {
502
+ if (hasUnexpandedChars(expandLeadingTilde(raw))) {
503
+ writePaths.push(raw);
504
+ }
505
+ else {
506
+ const exp = expandLeadingTilde(raw);
507
+ if (isAbsolute(exp)) {
508
+ writePaths.push(exp);
509
+ }
510
+ else if (cwd) {
511
+ writePaths.push(resolve(cwd, exp));
512
+ }
513
+ else {
514
+ writePaths.push(exp);
515
+ }
516
+ }
517
+ }
518
+ }
519
+ return writePaths;
520
+ }
521
+ function extractGrepReadPaths(tokens) {
522
+ const paths = [];
523
+ let hasPattern = false;
524
+ let i = 1;
525
+ while (i < tokens.length) {
526
+ const t = tokens[i];
527
+ if (t === '--') {
528
+ i++;
529
+ while (i < tokens.length) {
530
+ if (!hasPattern) {
531
+ hasPattern = true;
532
+ }
533
+ else {
534
+ paths.push(tokens[i]);
535
+ }
536
+ i++;
537
+ }
538
+ break;
539
+ }
540
+ if (t === '-e' || t === '--regexp') {
541
+ hasPattern = true;
542
+ i += 2;
543
+ continue;
544
+ }
545
+ if (t.startsWith('-e') || t.startsWith('--regexp=')) {
546
+ hasPattern = true;
547
+ i++;
548
+ continue;
549
+ }
550
+ if (t === '-f' || t === '--file') {
551
+ if (i + 1 < tokens.length)
552
+ paths.push(tokens[i + 1]);
553
+ hasPattern = true;
554
+ i += 2;
555
+ continue;
556
+ }
557
+ if (t.startsWith('-f') || t.startsWith('--file=')) {
558
+ paths.push(t.startsWith('-f') ? t.slice(2) : t.slice('--file='.length));
559
+ hasPattern = true;
560
+ i++;
561
+ continue;
562
+ }
563
+ if (t.startsWith('-')) {
564
+ i++;
565
+ continue;
566
+ }
567
+ if (!hasPattern) {
568
+ hasPattern = true;
569
+ i++;
570
+ continue;
571
+ }
572
+ paths.push(t);
573
+ i++;
574
+ }
575
+ return paths;
576
+ }
577
+ function extractFindReadPaths(tokens) {
578
+ const paths = [];
579
+ for (let i = 1; i < tokens.length; i++) {
580
+ const t = tokens[i];
581
+ if (t.startsWith('-') || t === '(' || t === '!' || t === ')') {
582
+ break;
583
+ }
584
+ paths.push(t);
585
+ }
586
+ return paths;
587
+ }
588
+ function extractAwkReadPaths(tokens) {
589
+ const paths = [];
590
+ let hasProgram = false;
591
+ let i = 1;
592
+ while (i < tokens.length) {
593
+ const t = tokens[i];
594
+ if (t === '-f') {
595
+ if (i + 1 < tokens.length)
596
+ paths.push(tokens[i + 1]);
597
+ hasProgram = true;
598
+ i += 2;
599
+ continue;
600
+ }
601
+ if (t.startsWith('-f')) {
602
+ paths.push(t.slice(2));
603
+ hasProgram = true;
604
+ i++;
605
+ continue;
606
+ }
607
+ if (t.startsWith('-')) {
608
+ if ((t === '-F' || t === '-v') && i + 1 < tokens.length)
609
+ i += 2;
610
+ else
611
+ i++;
612
+ continue;
613
+ }
614
+ if (!hasProgram) {
615
+ hasProgram = true;
616
+ i++;
617
+ continue;
618
+ }
619
+ paths.push(t);
620
+ i++;
621
+ }
622
+ return paths;
623
+ }
624
+ function extractSedReadPaths(tokens) {
625
+ const hasInPlace = tokens.some((t) => t === '-i' || t.startsWith('-i') || t === '--in-place' || t.startsWith('--in-place='));
626
+ if (hasInPlace)
627
+ return [];
628
+ const paths = [];
629
+ let hasScript = false;
630
+ let i = 1;
631
+ while (i < tokens.length) {
632
+ const t = tokens[i];
633
+ if (t === '--') {
634
+ i++;
635
+ while (i < tokens.length)
636
+ paths.push(tokens[i++]);
637
+ break;
638
+ }
639
+ if (t === '-e' || t === '--expression') {
640
+ hasScript = true;
641
+ i += 2;
642
+ continue;
643
+ }
644
+ if (t.startsWith('-e') || t.startsWith('--expression=')) {
645
+ hasScript = true;
646
+ i++;
647
+ continue;
648
+ }
649
+ if (t === '-f' || t === '--file') {
650
+ if (i + 1 < tokens.length)
651
+ paths.push(tokens[i + 1]);
652
+ hasScript = true;
653
+ i += 2;
654
+ continue;
655
+ }
656
+ if (t.startsWith('-f') || t.startsWith('--file=')) {
657
+ paths.push(t.startsWith('-f') ? t.slice(2) : t.slice('--file='.length));
658
+ hasScript = true;
659
+ i++;
660
+ continue;
661
+ }
662
+ if (t.startsWith('-')) {
663
+ i++;
664
+ continue;
665
+ }
666
+ if (!hasScript) {
667
+ hasScript = true;
668
+ i++;
669
+ continue;
670
+ }
671
+ paths.push(t);
672
+ i++;
673
+ }
674
+ return paths;
675
+ }
676
+ function extractGenericReadPaths(tokens) {
677
+ const paths = [];
678
+ let afterDoubleDash = false;
679
+ let i = 1;
680
+ while (i < tokens.length) {
681
+ const t = tokens[i];
682
+ if (!afterDoubleDash) {
683
+ if (t === '--') {
684
+ afterDoubleDash = true;
685
+ i++;
686
+ continue;
687
+ }
688
+ if (t === '-n' ||
689
+ t === '-C' ||
690
+ t === '-B' ||
691
+ t === '-A' ||
692
+ t === '-d' ||
693
+ t === '-f' ||
694
+ t === '-s' ||
695
+ t === '-o' ||
696
+ t === '--output') {
697
+ i += 2;
698
+ continue;
699
+ }
700
+ if (t.startsWith('-o') && t.length > 2) {
701
+ i++;
702
+ continue;
703
+ }
704
+ if (t.startsWith('--output=')) {
705
+ i++;
706
+ continue;
707
+ }
708
+ if (t.startsWith('-')) {
709
+ i++;
710
+ continue;
711
+ }
712
+ }
713
+ paths.push(t);
714
+ i++;
715
+ }
716
+ return paths;
717
+ }
718
+ function extractScriptReadPaths(head, tokens) {
719
+ if (head === 'python' || head === 'python3') {
720
+ let i = 1;
721
+ while (i < tokens.length) {
722
+ const t = tokens[i];
723
+ if (t === '-c' || t === '-m')
724
+ return [];
725
+ if (t.startsWith('-c') || t.startsWith('-m'))
726
+ return [];
727
+ if (t.startsWith('-')) {
728
+ i++;
729
+ continue;
730
+ }
731
+ return [t];
732
+ }
733
+ }
734
+ else if (head === 'node') {
735
+ let i = 1;
736
+ while (i < tokens.length) {
737
+ const t = tokens[i];
738
+ if (t === '-e' || t === '--eval' || t === '-p' || t === '--print')
739
+ return [];
740
+ if (t.startsWith('-')) {
741
+ i++;
742
+ continue;
743
+ }
744
+ return [t];
745
+ }
746
+ }
747
+ else if (head === 'java') {
748
+ const paths = [];
749
+ let i = 1;
750
+ while (i < tokens.length) {
751
+ const t = tokens[i];
752
+ if (t === '-jar' && i + 1 < tokens.length) {
753
+ paths.push(tokens[i + 1]);
754
+ return paths;
755
+ }
756
+ if ((t === '-cp' || t === '-classpath' || t === '--class-path') && i + 1 < tokens.length) {
757
+ i += 2;
758
+ continue;
759
+ }
760
+ if (t.startsWith('-')) {
761
+ i++;
762
+ continue;
763
+ }
764
+ if (t.endsWith('.java') || t.endsWith('.jar') || t.includes('/')) {
765
+ paths.push(t);
766
+ }
767
+ break;
768
+ }
769
+ return paths;
770
+ }
771
+ return [];
772
+ }
773
+ /**
774
+ * Full structural extraction of paths from a command line.
775
+ * Extracts mutation targets, redirection targets, and read targets.
776
+ * Detects unexpanded shell expansions and collects candidate paths for credential checks.
777
+ */
778
+ export function extractCommandContainment(commandLine, workspace) {
779
+ const segments = splitChainSegments(commandLine) ?? [commandLine];
780
+ const writePaths = [];
781
+ const readPaths = [];
782
+ let unexpandedPath = null;
783
+ const candidatePaths = [];
784
+ const registerCandidate = (token) => {
785
+ if (hasUnexpandedChars(expandLeadingTilde(token))) {
786
+ return;
787
+ }
788
+ const exp = expandLeadingTilde(token);
789
+ candidatePaths.push(isAbsolute(exp) ? resolve(exp) : resolve(workspace, exp));
790
+ };
791
+ for (const segment of segments) {
792
+ const tokens = tokenizeCommand(segment);
793
+ if (tokens.length === 0)
794
+ continue;
795
+ const stripped = stripEnvPrefixTokens(tokens);
796
+ if (stripped.remainingTokens.length === 0)
797
+ continue;
798
+ const head = extractBasename(stripped.remainingTokens[0]);
799
+ if (['bash', 'sh', 'zsh'].includes(head)) {
800
+ const cIdx = stripped.remainingTokens.indexOf('-c');
801
+ if (cIdx !== -1 && cIdx + 1 < stripped.remainingTokens.length) {
802
+ const inner = extractCommandContainment(stripped.remainingTokens[cIdx + 1], workspace);
803
+ writePaths.push(...inner.writePaths);
804
+ readPaths.push(...inner.readPaths);
805
+ if (!unexpandedPath && inner.unexpandedPath)
806
+ unexpandedPath = inner.unexpandedPath;
807
+ candidatePaths.push(...inner.candidatePaths);
808
+ }
809
+ continue;
810
+ }
811
+ // 1. Redirections in this segment
812
+ for (let i = 0; i < tokens.length; i++) {
813
+ const token = tokens[i];
814
+ const m = REDIRECT.exec(token);
815
+ if (!m)
816
+ continue;
817
+ const glued = token.slice(m[0].length);
818
+ const target = glued.length > 0 ? glued : (tokens[++i] ?? '');
819
+ if (target.length === 0 || NON_FILE_TARGET.test(target))
820
+ continue;
821
+ if (hasUnexpandedChars(expandLeadingTilde(target))) {
822
+ if (!unexpandedPath)
823
+ unexpandedPath = target;
824
+ }
825
+ else {
826
+ const exp = expandLeadingTilde(target);
827
+ const resolved = isAbsolute(exp) ? resolve(exp) : resolve(workspace, exp);
828
+ writePaths.push(resolved);
829
+ candidatePaths.push(resolved);
830
+ }
831
+ }
832
+ // 2. Mutation write targets
833
+ const mutTargets = commandMutationWritePaths(segment, null);
834
+ for (const raw of mutTargets) {
835
+ if (hasUnexpandedChars(expandLeadingTilde(raw))) {
836
+ if (!unexpandedPath)
837
+ unexpandedPath = raw;
838
+ }
839
+ else {
840
+ const exp = expandLeadingTilde(raw);
841
+ const resolved = isAbsolute(exp) ? resolve(exp) : resolve(workspace, exp);
842
+ writePaths.push(resolved);
843
+ candidatePaths.push(resolved);
844
+ }
845
+ }
846
+ // 3. Read utility targets
847
+ let rawReadCandidates = [];
848
+ if (READ_UTILITIES.has(head)) {
849
+ if (head === 'grep' || head === 'egrep' || head === 'fgrep' || head === 'rg') {
850
+ rawReadCandidates = extractGrepReadPaths(stripped.remainingTokens);
851
+ }
852
+ else if (head === 'find') {
853
+ rawReadCandidates = extractFindReadPaths(stripped.remainingTokens);
854
+ }
855
+ else if (head === 'sed') {
856
+ rawReadCandidates = extractSedReadPaths(stripped.remainingTokens);
857
+ }
858
+ else if (head === 'awk') {
859
+ rawReadCandidates = extractAwkReadPaths(stripped.remainingTokens);
860
+ }
861
+ else {
862
+ rawReadCandidates = extractGenericReadPaths(stripped.remainingTokens);
863
+ }
864
+ }
865
+ else if (head === 'cp' || head === 'mv') {
866
+ const positionals = stripped.remainingTokens.slice(1).filter((t) => !t.startsWith('-'));
867
+ if (positionals.length >= 2) {
868
+ rawReadCandidates = positionals.slice(0, -1);
869
+ }
870
+ }
871
+ else if (head === 'python' || head === 'python3' || head === 'node' || head === 'java') {
872
+ rawReadCandidates = extractScriptReadPaths(head, stripped.remainingTokens);
873
+ }
874
+ for (const cand of rawReadCandidates) {
875
+ if (!cand || cand === '-' || cand === '--' || cand.startsWith('-'))
876
+ continue;
877
+ if (hasUnexpandedChars(expandLeadingTilde(cand))) {
878
+ if (!unexpandedPath)
879
+ unexpandedPath = cand;
880
+ }
881
+ else if (ALLOWED_DEV_READS.has(cand)) {
882
+ readPaths.push(cand);
883
+ }
884
+ else {
885
+ const exp = expandLeadingTilde(cand);
886
+ const resolved = isAbsolute(exp) ? resolve(exp) : resolve(workspace, exp);
887
+ readPaths.push(resolved);
888
+ candidatePaths.push(resolved);
889
+ }
890
+ }
891
+ // 4. Collect any path-looking arguments across any command for credential HARD_DENY
892
+ for (let i = 1; i < stripped.remainingTokens.length; i++) {
893
+ const t = stripped.remainingTokens[i];
894
+ if (!t)
895
+ continue;
896
+ const eqIdx = t.indexOf('=');
897
+ const val = eqIdx !== -1 && !t.startsWith('-') ? t.slice(eqIdx + 1) : t;
898
+ if (isPathLike(val, workspace)) {
899
+ registerCandidate(val);
900
+ }
901
+ }
902
+ }
903
+ return {
904
+ writePaths: Array.from(new Set(writePaths)),
905
+ readPaths: Array.from(new Set(readPaths)),
906
+ unexpandedPath,
907
+ candidatePaths: Array.from(new Set(candidatePaths)),
908
+ };
909
+ }
910
+ export function pathsFromToolCall(toolName, args) {
911
+ const read = [];
912
+ const write = [];
913
+ const cwd = typeof args.Cwd === 'string' ? args.Cwd : null;
914
+ if (cwd) {
915
+ read.push(cwd);
916
+ write.push(cwd);
917
+ }
918
+ if (toolName === 'run_command' && typeof args.CommandLine === 'string') {
919
+ for (const token of args.CommandLine.split(/\s+/)) {
920
+ const m = ABS_PATH_TOKEN.exec(token);
921
+ if (m && m[1]) {
922
+ const eqIdx = token.indexOf('=');
923
+ if (eqIdx > 0) {
924
+ const varName = token.slice(0, eqIdx);
925
+ if (isDeniedEnvVar(varName)) {
926
+ continue;
927
+ }
928
+ }
929
+ const parts = m[1].split(':');
930
+ for (const p of parts) {
931
+ if (p.startsWith('/') && !NON_FILE_TARGET.test(p)) {
932
+ read.push(p);
933
+ }
934
+ }
935
+ }
936
+ }
937
+ const mutTargets = commandMutationWritePaths(args.CommandLine, cwd);
938
+ for (const p of mutTargets) {
939
+ write.push(p);
940
+ }
941
+ }
942
+ return { read, write };
943
+ }
944
+ /** `NAME=/abs/path` at the start of a command line (or after `env` / `export`). */
945
+ const ENV_ASSIGNMENT_PATH = /^([A-Za-z_][A-Za-z0-9_]*)=["']?(\/[^\s'"]*)["']?$/;
946
+ export function assignmentReadPaths(commandLine) {
947
+ const out = [];
948
+ const tokens = commandLine.split(/\s+/);
949
+ let i = 0;
950
+ while (i < tokens.length) {
951
+ const token = tokens[i];
952
+ if (token === 'env' || token === 'export' || token === '-i' || token === '--ignore-environment') {
953
+ i++;
954
+ continue;
955
+ }
956
+ const m = ENV_ASSIGNMENT_PATH.exec(token);
957
+ const plainAssignment = /^[A-Za-z_][A-Za-z0-9_]*=/.test(token);
958
+ if (!plainAssignment)
959
+ break;
960
+ i++;
961
+ if (!m || !m[1] || !m[2])
962
+ continue;
963
+ if (isDeniedEnvVar(m[1]))
964
+ continue;
965
+ for (const part of m[2].split(':')) {
966
+ if (part.startsWith('/') && !NON_FILE_TARGET.test(part))
967
+ out.push(part);
968
+ }
969
+ }
970
+ return out;
971
+ }
972
+ /** Non-throwing containment probe over both root sets. */
973
+ export function isContainedForKind(path, roots, kind) {
974
+ return containedOrNull(path, roots[kind]) !== null;
975
+ }
976
+ /**
977
+ * I6: `{workspace}/.agents/**` holds the
978
+ * gate's own `hooks.json`, so it is write-protected even though it sits
979
+ * *inside* the workspace and would otherwise pass ordinary containment.
980
+ */
981
+ export function isAgentsPath(path, workspace) {
982
+ const agentsDir = canonicalize(join(canonicalize(workspace), '.agents'));
983
+ return isWithin(canonicalize(path), agentsDir);
984
+ }
985
+ // ─────────────────────────────────────────────────────────────────────────────
986
+ // Shell redirection targets
987
+ // ─────────────────────────────────────────────────────────────────────────────
988
+ const REDIRECT = /^(?:&|\d+)?>{1,2}\|?/;
989
+ const NON_FILE_TARGET = /^(?:&\d+|&-|\/dev\/(?:null|stdout|stderr|tty|fd\/\d+))$/;
990
+ export function redirectionTargets(commandLine, workspace) {
991
+ const segments = splitChainSegments(commandLine) ?? [commandLine];
992
+ const targets = [];
993
+ for (const segment of segments) {
994
+ const tokens = tokenizeCommand(segment);
995
+ for (let i = 0; i < tokens.length; i++) {
996
+ const token = tokens[i];
997
+ const m = REDIRECT.exec(token);
998
+ if (!m)
999
+ continue;
1000
+ const glued = token.slice(m[0].length);
1001
+ const target = glued.length > 0 ? glued : (tokens[++i] ?? '');
1002
+ if (target.length === 0)
1003
+ continue;
1004
+ if (NON_FILE_TARGET.test(target))
1005
+ continue;
1006
+ const exp = expandLeadingTilde(target);
1007
+ if (hasUnexpandedChars(exp)) {
1008
+ targets.push(target);
1009
+ }
1010
+ else {
1011
+ targets.push(isAbsolute(exp) ? exp : join(workspace, exp));
1012
+ }
1013
+ }
1014
+ }
1015
+ return targets;
1016
+ }
1017
+ export function escapingRedirectTarget(commandLine, workspace) {
1018
+ const roots = [canonicalize(workspace)];
1019
+ for (const target of redirectionTargets(commandLine, workspace)) {
1020
+ if (hasUnexpandedChars(expandLeadingTilde(target)))
1021
+ return target;
1022
+ if (containedOrNull(target, roots) === null)
1023
+ return target;
1024
+ if (isAgentsPath(target, workspace))
1025
+ return target;
1026
+ }
1027
+ return null;
1028
+ }
1029
+ /**
1030
+ * Ceiling write_roots sanity.
1031
+ * If an effective write root is /, the home directory, contains ~/.agy-worker,
1032
+ * or contains the gate binary, refuse with ValidationError.
1033
+ */
1034
+ export function validateWriteRoots(writeRoots, gatePath) {
1035
+ const sHome = canonicalize(stateHome());
1036
+ const home = canonicalize(homedir());
1037
+ const gPath = gatePath ? canonicalize(gatePath) : null;
1038
+ for (const root of writeRoots) {
1039
+ const cRoot = canonicalize(root);
1040
+ const rootOf = parsePath(cRoot).root;
1041
+ if (cRoot === rootOf || cRoot === '/') {
1042
+ throw new ValidationError({
1043
+ field: 'write_roots',
1044
+ value: root,
1045
+ expected: 'effective write root must not be root directory (/)',
1046
+ });
1047
+ }
1048
+ if (cRoot === home) {
1049
+ throw new ValidationError({
1050
+ field: 'write_roots',
1051
+ value: root,
1052
+ expected: 'effective write root must not be home directory',
1053
+ });
1054
+ }
1055
+ if (cRoot === sHome || isWithin(sHome, cRoot)) {
1056
+ throw new ValidationError({
1057
+ field: 'write_roots',
1058
+ value: root,
1059
+ expected: 'effective write root must not contain state home (~/.agy-worker)',
1060
+ });
1061
+ }
1062
+ if (gPath && (cRoot === gPath || isWithin(gPath, cRoot))) {
1063
+ throw new ValidationError({
1064
+ field: 'write_roots',
1065
+ value: root,
1066
+ expected: 'effective write root must not contain gate binary',
1067
+ });
1068
+ }
1069
+ }
1070
+ }
1071
+ //# sourceMappingURL=containment.js.map