@vzn/vx 0.0.651 → 0.0.653

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 (177) hide show
  1. package/README.md +3 -4
  2. package/launcher.sh +2 -1
  3. package/package.json +5 -5
  4. package/schemas/error.json +1 -1
  5. package/schemas/show.json +15 -20
  6. package/skills/vx/SKILL.md +3 -3
  7. package/src/bin.ts +37 -3
  8. package/src/cache/archive.ts +197 -32
  9. package/src/cache/cache.ts +882 -230
  10. package/src/cache/checkpointer.ts +102 -0
  11. package/src/cache/file-hashes.ts +2 -1
  12. package/src/cache/fold-memo.ts +24 -4
  13. package/src/cache/fs-clock.ts +26 -0
  14. package/src/cache/git-inputs.ts +188 -22
  15. package/src/cache/history-stream.ts +151 -0
  16. package/src/cache/history-writer.ts +76 -0
  17. package/src/cache/index.ts +12 -1
  18. package/src/cache/inputs.ts +616 -135
  19. package/src/cache/key-fold.ts +1 -1
  20. package/src/cache/layer.ts +29 -16
  21. package/src/cache/layered-cache.ts +28 -7
  22. package/src/cache/lockfile-memo.ts +2 -1
  23. package/src/cache/output-index.ts +106 -44
  24. package/src/cache/plugin-cache.ts +2 -2
  25. package/src/cache/policy.ts +6 -2
  26. package/src/cache/remote-lane.ts +3 -1
  27. package/src/cache/run-history.ts +94 -25
  28. package/src/cache/schema.ts +46 -10
  29. package/src/cli/cache.ts +149 -36
  30. package/src/cli/completions.ts +2 -2
  31. package/src/cli/core-alias.ts +9 -2
  32. package/src/cli/docs.ts +4 -2
  33. package/src/cli/help.ts +27 -4
  34. package/src/cli/index.ts +62 -7
  35. package/src/cli/info.ts +5 -4
  36. package/src/cli/init.ts +55 -20
  37. package/src/cli/last.ts +218 -43
  38. package/src/cli/lock.ts +187 -24
  39. package/src/cli/plan-format.ts +24 -21
  40. package/src/cli/plugin-commands.ts +6 -1
  41. package/src/cli/plugin-templates.ts +36 -45
  42. package/src/cli/run.ts +68 -34
  43. package/src/cli/select.ts +160 -64
  44. package/src/cli/show.ts +70 -28
  45. package/src/cli/task-verb.ts +12 -7
  46. package/src/cli/upgrade.ts +210 -76
  47. package/src/cli/watch-filter.ts +88 -15
  48. package/src/cli/watch-fs.ts +34 -47
  49. package/src/cli/watch-judge.ts +73 -9
  50. package/src/cli/watch-set.ts +10 -8
  51. package/src/cli/watch.ts +190 -33
  52. package/src/cli/why.ts +107 -33
  53. package/src/cli/workspace-config.ts +11 -1
  54. package/src/config.ts +56 -30
  55. package/src/exec/env.ts +3 -1
  56. package/src/exec/executor.ts +116 -16
  57. package/src/exec/index.ts +1 -0
  58. package/src/exec/kill-tree.ts +7 -7
  59. package/src/exec/runner.ts +270 -108
  60. package/src/exec/sandbox-binds.ts +109 -18
  61. package/src/exec/sandbox-paths.ts +3 -1
  62. package/src/exec/sandbox-runtime.ts +446 -64
  63. package/src/exec/sandbox-violations.ts +250 -46
  64. package/src/exec/spawn-off.ts +10 -5
  65. package/src/exec/task-proxy.ts +93 -0
  66. package/src/exec/warm-shell.ts +29 -7
  67. package/src/graph/definition.ts +16 -3
  68. package/src/graph/folds.ts +89 -16
  69. package/src/graph/index.ts +8 -0
  70. package/src/graph/priorities.ts +8 -5
  71. package/src/graph/scheduler.ts +176 -39
  72. package/src/graph/stand-ins.ts +36 -0
  73. package/src/graph/task-graph.ts +347 -160
  74. package/src/graph/task-status.ts +5 -1
  75. package/src/handoff.ts +45 -10
  76. package/src/history/failure-mode.ts +39 -6
  77. package/src/history/history.ts +103 -30
  78. package/src/history/index.ts +2 -0
  79. package/src/history/metrics.ts +193 -73
  80. package/src/index.ts +6 -3
  81. package/src/orchestrator/admission.ts +14 -10
  82. package/src/orchestrator/affected-tasks.ts +35 -13
  83. package/src/orchestrator/command-windows.ts +8 -5
  84. package/src/orchestrator/deferred-outputs.ts +42 -13
  85. package/src/orchestrator/doctor.ts +21 -9
  86. package/src/orchestrator/download-policy.ts +8 -10
  87. package/src/orchestrator/excluded-keys.ts +15 -3
  88. package/src/orchestrator/execute-args.ts +9 -2
  89. package/src/orchestrator/execute-task.ts +612 -138
  90. package/src/orchestrator/graph-memo.ts +26 -4
  91. package/src/orchestrator/graph-stage.ts +27 -4
  92. package/src/orchestrator/held-servers.ts +36 -14
  93. package/src/orchestrator/hit-restore.ts +141 -93
  94. package/src/orchestrator/index.ts +4 -1
  95. package/src/orchestrator/keyed-projects.ts +53 -0
  96. package/src/orchestrator/local-floor.ts +21 -3
  97. package/src/orchestrator/local-shortcircuit.ts +2 -2
  98. package/src/orchestrator/lockfile-claim.ts +30 -4
  99. package/src/orchestrator/miss-save.ts +60 -61
  100. package/src/orchestrator/options.ts +13 -0
  101. package/src/orchestrator/output-host.ts +16 -4
  102. package/src/orchestrator/output-log.ts +1 -54
  103. package/src/orchestrator/persistent-task.ts +32 -16
  104. package/src/orchestrator/persistent.ts +152 -15
  105. package/src/orchestrator/placement.ts +82 -16
  106. package/src/orchestrator/plan.ts +39 -17
  107. package/src/orchestrator/plugin-host.ts +238 -110
  108. package/src/orchestrator/plugin.ts +59 -27
  109. package/src/orchestrator/prepare.ts +194 -38
  110. package/src/orchestrator/projects.ts +89 -10
  111. package/src/orchestrator/read-only.ts +115 -0
  112. package/src/orchestrator/remote-prefetch.ts +1 -1
  113. package/src/orchestrator/run-context.ts +6 -2
  114. package/src/orchestrator/run-failures.ts +101 -23
  115. package/src/orchestrator/run-id.ts +13 -2
  116. package/src/orchestrator/run-keys.ts +87 -28
  117. package/src/orchestrator/run-live.ts +16 -13
  118. package/src/orchestrator/run-lock.ts +27 -4
  119. package/src/orchestrator/run-records.ts +95 -74
  120. package/src/orchestrator/run-report.ts +3 -2
  121. package/src/orchestrator/run-stop.ts +17 -9
  122. package/src/orchestrator/run.ts +531 -122
  123. package/src/orchestrator/sandbox-fix.ts +40 -12
  124. package/src/orchestrator/sandbox-request.ts +314 -44
  125. package/src/orchestrator/save-lane.ts +24 -13
  126. package/src/orchestrator/shell-verdict.ts +29 -1
  127. package/src/orchestrator/signals.ts +103 -8
  128. package/src/orchestrator/stable-keys.ts +34 -27
  129. package/src/orchestrator/task-artifacts.ts +16 -3
  130. package/src/orchestrator/task-hash.ts +191 -34
  131. package/src/orchestrator/telemetry-host.ts +25 -6
  132. package/src/orchestrator/telemetry.ts +85 -13
  133. package/src/orchestrator/upstream.ts +3 -1
  134. package/src/report/events.ts +23 -6
  135. package/src/report/forecast.ts +103 -37
  136. package/src/report/framed-output.ts +179 -162
  137. package/src/report/index.ts +2 -1
  138. package/src/report/logger.ts +108 -31
  139. package/src/report/plain-output.ts +15 -9
  140. package/src/report/status-line.ts +28 -55
  141. package/src/report/summary.ts +61 -18
  142. package/src/settings.ts +50 -14
  143. package/src/ui/components.ts +36 -42
  144. package/src/ui/index.ts +3 -1
  145. package/src/ui/live-region.ts +67 -8
  146. package/src/ui/terminal.ts +12 -0
  147. package/src/ui/theme.ts +16 -1
  148. package/src/util/config-frame.ts +142 -6
  149. package/src/util/edit-distance.ts +17 -1
  150. package/src/util/errors.ts +23 -7
  151. package/src/util/foreign-cache-dir.ts +60 -0
  152. package/src/util/git-ignored.ts +59 -5
  153. package/src/util/index.ts +15 -1
  154. package/src/util/paths.ts +51 -7
  155. package/src/util/plain-text.ts +19 -0
  156. package/src/util/secret-mask.ts +57 -1
  157. package/src/util/settle.ts +18 -1
  158. package/src/util/size.ts +12 -2
  159. package/src/util/tail.ts +4 -1
  160. package/src/util/write-file.ts +86 -9
  161. package/src/workspace/affected.ts +225 -27
  162. package/src/workspace/bracket-literal.ts +2 -2
  163. package/src/workspace/config-bytecode.ts +23 -4
  164. package/src/workspace/config-eval.ts +90 -16
  165. package/src/workspace/config-imports.ts +96 -41
  166. package/src/workspace/config-schema.ts +192 -86
  167. package/src/workspace/dependencies-lock.ts +7 -14
  168. package/src/workspace/filter.ts +18 -2
  169. package/src/workspace/index.ts +3 -1
  170. package/src/workspace/json-data.ts +34 -8
  171. package/src/workspace/lockfile.ts +86 -37
  172. package/src/workspace/migration.ts +58 -31
  173. package/src/workspace/module-state.ts +62 -0
  174. package/src/workspace/nested-dirs.ts +8 -0
  175. package/src/workspace/project-loader.ts +276 -32
  176. package/src/workspace/repo-id.ts +29 -6
  177. package/src/workspace/workspace.ts +190 -30
@@ -13,7 +13,8 @@ import {
13
13
  toRealPath,
14
14
  unique,
15
15
  } from './sandbox-paths.js'
16
- import type { SandboxedRunArgs } from './sandbox-runtime.js'
16
+ import { shellQuote } from './runner.js'
17
+ import type { ResolvedSandboxConfig, SandboxedRunArgs } from './sandbox-runtime.js'
17
18
  import * as ui from '../ui/index.js'
18
19
 
19
20
  type SrtModule = typeof import('@anthropic-ai/sandbox-runtime')
@@ -40,6 +41,48 @@ export function bindableWrites(paths: readonly string[]): string[] {
40
41
  return unique(paths.filter((p) => !holdsGlobChar(p)).map(bindOf))
41
42
  }
42
43
 
44
+ /**
45
+ * Linux: each write grant spelled through a link in the project, as the
46
+ * bind that puts it where the task names it: `[source, at]`, the grant's
47
+ * own bind and its twin under the link. A file grant binds its directory,
48
+ * so its twin is the link's; a link that is the file itself sits in the
49
+ * bound directory already and needs none. SRT skips a write path that is
50
+ * a sibling link (it reads one as leaving its tree), so vx lays these
51
+ * itself (`withLinkedWrites`).
52
+ */
53
+ export function linkedWriteBinds(
54
+ linked: ResolvedSandboxConfig['linkedWrites'],
55
+ ): Array<readonly [string, string]> {
56
+ if (process.platform !== 'linux' || linked === undefined) return []
57
+ const out: Array<readonly [string, string]> = []
58
+ for (const { link, target } of linked) {
59
+ if (holdsGlobChar(target) || holdsGlobChar(link)) continue
60
+ const source = bindOf(target)
61
+ const at = source === target ? link : path.dirname(link)
62
+ if (at !== source && !out.some(([, a]) => a === at)) out.push([source, at])
63
+ }
64
+ return out
65
+ }
66
+
67
+ /**
68
+ * Linux: SRT's bwrap line with each `linkedWriteBinds` pair bound last,
69
+ * before the `--` that ends bwrap's options: after the masks and the read
70
+ * grants, so none lands over it. The link's directory is then a mount of
71
+ * its target, writable as the grant is.
72
+ */
73
+ export function withLinkedWrites(
74
+ wrapped: string,
75
+ binds: ReadonlyArray<readonly [string, string]>,
76
+ ): string {
77
+ if (binds.length === 0) return wrapped
78
+ for (const m of wrapped.matchAll(/(?:'[^']*'|"[^"]*"|[^\s'"])+/g)) {
79
+ if (m[0] !== '--') continue
80
+ const words = binds.map(([s, a]) => `--bind ${shellQuote(s)} ${shellQuote(a)} `).join('')
81
+ return `${wrapped.slice(0, m.index)}${words}${wrapped.slice(m.index)}`
82
+ }
83
+ return wrapped
84
+ }
85
+
43
86
  function bindOf(p: string): string {
44
87
  if (!isMountableLiteral(p)) return p
45
88
  try {
@@ -162,7 +205,12 @@ const warnedSymlinkPunch = new Set<string>()
162
205
  * A grant with no write path under it is returned untouched — the common
163
206
  * case costs nothing, not even a readdir.
164
207
  */
165
- export function punchWritePaths(readPath: string, writePaths: readonly string[]): string[] {
208
+ export function punchWritePaths(
209
+ readPath: string,
210
+ writePaths: readonly string[],
211
+ /** Whether a link child's real path may be bound; one it may not is dropped. */
212
+ reachable: (real: string) => boolean = () => true,
213
+ ): string[] {
166
214
  // Linux only. The shadowing is a property of bwrap MOUNTS; macOS seatbelt
167
215
  // evaluates rules by precedence, so a read grant on a directory and a
168
216
  // write grant inside it coexist. Punching there costs the directory
@@ -193,25 +241,57 @@ export function punchWritePaths(readPath: string, writePaths: readonly string[])
193
241
  // SRT's config carries no `--symlink`, so the only fix is the grant:
194
242
  // say which one, loudly.
195
243
  try {
196
- if (lstatSync(child).isSymbolicLink()) linked.push(child)
244
+ if (lstatSync(child).isSymbolicLink()) {
245
+ // Bound by its target, a link out to a denied place (`node_modules
246
+ // -> ..`, `shared -> ../b`) granted what the whole grant never did:
247
+ // bound unpunched, the link stays a link into the deny (item 1006's
248
+ // glob hit, through the punch).
249
+ if (!reachable(toRealPath(child))) continue
250
+ linked.push(child)
251
+ }
197
252
  } catch {
198
253
  // vanished between readdir and lstat: nothing to bind either way
199
254
  }
200
- out.push(...punchWritePaths(child, under))
255
+ out.push(...punchWritePaths(child, under, reachable))
201
256
  }
202
257
  if (linked.length > 0 && !warnedSymlinkPunch.has(readPath)) {
203
258
  warnedSymlinkPunch.add(readPath)
204
- ui.say(
205
- 'warn',
206
- `[vx] sandbox: a write grant under ${readPath} makes its ${linked.length} symlinked ` +
207
- `entr${linked.length === 1 ? 'y' : 'ies'} (${linked
208
- .slice(0, 3)
209
- .map((l) => path.basename(l))
210
- .join(', ')}${linked.length > 3 ? ', …' : ''}) plain directories inside the sandbox — ` +
211
- `a package resolved through one loses its siblings. Move the write grant (${under
212
- .map((w) => path.relative(readPath, w))
213
- .join(', ')}) out of it.\n`,
214
- )
259
+ // A link INTO a write grant is that grant under a second name, mounted
260
+ // read-only: the remedy is to grant the name (`linkedWriteBinds` binds
261
+ // it writable), not to move the grant.
262
+ const plain: string[] = []
263
+ for (const l of linked) {
264
+ const w = writePaths.find((w) => atOrUnder(toRealPath(l), w))
265
+ if (w === undefined) {
266
+ plain.push(l)
267
+ continue
268
+ }
269
+ let dir = false
270
+ try {
271
+ dir = statSync(l).isDirectory()
272
+ } catch {
273
+ // dangling: granted as a file
274
+ }
275
+ ui.say(
276
+ 'warn',
277
+ `[vx] sandbox: ${l} is a symlink into the write grant ${w}, and the Linux sandbox ` +
278
+ `mounts it read-only, so a write through it is refused. Grant the link itself to ` +
279
+ `write through it: ${l}${dir ? '/' : ''}\n`,
280
+ )
281
+ }
282
+ if (plain.length > 0) {
283
+ ui.say(
284
+ 'warn',
285
+ `[vx] sandbox: a write grant under ${readPath} makes its ${plain.length} symlinked ` +
286
+ `entr${plain.length === 1 ? 'y' : 'ies'} (${plain
287
+ .slice(0, 3)
288
+ .map((l) => path.basename(l))
289
+ .join(', ')}${plain.length > 3 ? ', …' : ''}) plain directories inside the sandbox — ` +
290
+ `a package resolved through one loses its siblings. Move the write grant (${under
291
+ .map((w) => path.relative(readPath, w))
292
+ .join(', ')}) out of it.\n`,
293
+ )
294
+ }
215
295
  }
216
296
  return out
217
297
  }
@@ -300,9 +380,20 @@ export function buildCustomConfig(
300
380
  const c = args.config
301
381
  const denyRead = unique([...baselines.denyRead])
302
382
  const allowWrite = bindableWrites(unique([...c.allowWrite]))
303
- const allowRead = unique(
304
- [...baselines.allowRead, ...c.allowRead].flatMap((r) => punchWritePaths(r, allowWrite)),
305
- )
383
+ // A link a write grant is spelled through is bound by vx, not SRT: a
384
+ // read grant above it is punched around it all the same.
385
+ const punched = [...allowWrite, ...linkedWriteBinds(c.linkedWrites).map(([, at]) => at)]
386
+ const reads = [...baselines.allowRead, ...c.allowRead]
387
+ // Asked only of a link a punch meets: a target under the deny anchor is
388
+ // bound only where some read grant already reaches it.
389
+ let fence: { denied: string[]; granted: string[] } | undefined
390
+ const reachable = (real: string): boolean => {
391
+ fence ??= { denied: denyRead.map(toRealPath), granted: reads.map(toRealPath) }
392
+ return (
393
+ !fence.denied.some((d) => atOrUnder(real, d)) || fence.granted.some((g) => atOrUnder(real, g))
394
+ )
395
+ }
396
+ const allowRead = unique(reads.flatMap((r) => punchWritePaths(r, punched, reachable)))
306
397
 
307
398
  const custom: Parameters<SrtModule['SandboxManager']['wrapWithSandbox']>[2] = {
308
399
  filesystem: {
@@ -102,6 +102,8 @@ export function sandboxReads(
102
102
  readonly config: {
103
103
  readonly allowRead: readonly string[]
104
104
  readonly allowWrite: readonly string[]
105
+ /** Denies the whole host, not only `baseDenyRead`: only a grant reads. */
106
+ readonly hermetic?: boolean
105
107
  }
106
108
  },
107
109
  file: string,
@@ -109,7 +111,7 @@ export function sandboxReads(
109
111
  const real = toRealPath(file)
110
112
  const under = (g: string): boolean => atOrUnder(real, toRealPath(g))
111
113
  return (
112
- !sandbox.baseDenyRead.some(under) ||
114
+ (sandbox.config.hermetic !== true && !sandbox.baseDenyRead.some(under)) ||
113
115
  [...sandbox.baseAllowRead, ...sandbox.config.allowRead, ...sandbox.config.allowWrite].some(
114
116
  under,
115
117
  )