@vzn/vx 0.0.18 → 0.0.22

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 (149) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +107 -70
  3. package/package.json +10 -20
  4. package/src/bin.ts +64 -5
  5. package/src/cache/archive.ts +279 -28
  6. package/src/cache/cache.ts +952 -1584
  7. package/src/cache/chained-cache.ts +14 -10
  8. package/src/cache/config-evals.ts +112 -0
  9. package/src/cache/file-hashes.ts +290 -0
  10. package/src/cache/git-inputs.ts +966 -0
  11. package/src/cache/index.ts +23 -7
  12. package/src/cache/inputs.ts +553 -739
  13. package/src/cache/key-fold.ts +232 -0
  14. package/src/cache/layer.ts +674 -0
  15. package/src/cache/layered-cache.ts +211 -49
  16. package/src/cache/output-index.ts +323 -0
  17. package/src/cache/policy.ts +84 -0
  18. package/src/cache/run-history.ts +152 -0
  19. package/src/cache/zstd.ts +183 -0
  20. package/src/cli/cache.ts +88 -30
  21. package/src/cli/completions.ts +104 -0
  22. package/src/cli/core-alias.ts +25 -0
  23. package/src/cli/format.ts +4 -15
  24. package/src/cli/help.ts +76 -38
  25. package/src/cli/index.ts +82 -17
  26. package/src/cli/info.ts +162 -91
  27. package/src/cli/init.ts +87 -0
  28. package/src/cli/last.ts +130 -23
  29. package/src/cli/lock.ts +36 -18
  30. package/src/cli/plan-format.ts +18 -7
  31. package/src/cli/plugin-commands.ts +34 -12
  32. package/src/cli/run.ts +138 -313
  33. package/src/cli/select.ts +342 -0
  34. package/src/cli/show.ts +151 -71
  35. package/src/cli/upgrade.ts +136 -15
  36. package/src/cli/watch.ts +1175 -116
  37. package/src/cli/why.ts +51 -29
  38. package/src/cli/workspace-config.ts +138 -0
  39. package/src/config.ts +183 -126
  40. package/src/exec/env.ts +11 -0
  41. package/src/exec/executor.ts +62 -31
  42. package/src/exec/index.ts +16 -2
  43. package/src/exec/kill-tree.ts +281 -0
  44. package/src/exec/local-executor.ts +41 -0
  45. package/src/exec/runner.ts +361 -77
  46. package/src/exec/sandbox-binds.ts +211 -0
  47. package/src/exec/sandbox-paths.ts +70 -0
  48. package/src/exec/sandbox-runtime.ts +1152 -415
  49. package/src/exec/sandbox-violations.ts +293 -0
  50. package/src/graph/dependency-spec.ts +14 -14
  51. package/src/graph/index.ts +6 -4
  52. package/src/graph/priorities.ts +130 -0
  53. package/src/graph/scheduler.ts +332 -329
  54. package/src/graph/task-graph.ts +552 -188
  55. package/src/index.ts +111 -102
  56. package/src/orchestrator/admission.ts +161 -0
  57. package/src/orchestrator/colors.ts +8 -3
  58. package/src/orchestrator/deferred-outputs.ts +25 -7
  59. package/src/orchestrator/doctor.ts +363 -0
  60. package/src/orchestrator/events.ts +111 -8
  61. package/src/orchestrator/excluded-keys.ts +155 -0
  62. package/src/orchestrator/execute-task.ts +450 -565
  63. package/src/orchestrator/failure-mode.ts +169 -28
  64. package/src/orchestrator/failure-recap.ts +121 -0
  65. package/src/orchestrator/fingerprint-watch.ts +89 -0
  66. package/src/orchestrator/framed-output.ts +121 -49
  67. package/src/orchestrator/history.ts +123 -104
  68. package/src/orchestrator/hit-restore.ts +285 -0
  69. package/src/orchestrator/index.ts +42 -5
  70. package/src/orchestrator/keyed-projects.ts +89 -0
  71. package/src/orchestrator/local-shortcircuit.ts +83 -23
  72. package/src/orchestrator/lockfile-claim.ts +401 -0
  73. package/src/orchestrator/logger.ts +128 -41
  74. package/src/orchestrator/metrics.ts +201 -72
  75. package/src/orchestrator/miss-save.ts +232 -0
  76. package/src/orchestrator/options.ts +63 -34
  77. package/src/orchestrator/persistent.ts +99 -0
  78. package/src/orchestrator/placement.ts +212 -0
  79. package/src/orchestrator/plan.ts +24 -11
  80. package/src/orchestrator/plugin-host.ts +200 -49
  81. package/src/orchestrator/plugin.ts +190 -14
  82. package/src/orchestrator/prepare.ts +168 -137
  83. package/src/orchestrator/projects.ts +270 -0
  84. package/src/orchestrator/remote-prefetch.ts +22 -2
  85. package/src/orchestrator/run-artifacts.ts +34 -3
  86. package/src/orchestrator/run-context.ts +20 -5
  87. package/src/orchestrator/run-lock.ts +349 -0
  88. package/src/orchestrator/run-records.ts +132 -0
  89. package/src/orchestrator/run-report.ts +6 -4
  90. package/src/orchestrator/run.ts +467 -611
  91. package/src/orchestrator/sandbox-request.ts +569 -0
  92. package/src/orchestrator/save-lane.ts +44 -0
  93. package/src/orchestrator/shell-verdict.ts +117 -0
  94. package/src/orchestrator/signals.ts +167 -0
  95. package/src/orchestrator/stable-keys.ts +259 -10
  96. package/src/orchestrator/status-line.ts +35 -2
  97. package/src/orchestrator/summary.ts +115 -25
  98. package/src/orchestrator/task-hash.ts +103 -21
  99. package/src/orchestrator/task-log-buffer.ts +15 -18
  100. package/src/orchestrator/telemetry-host.ts +27 -8
  101. package/src/orchestrator/telemetry.ts +71 -44
  102. package/src/orchestrator/upstream.ts +91 -45
  103. package/src/util/bun-version.ts +43 -0
  104. package/src/util/cgroup.ts +160 -0
  105. package/src/util/edit-distance.ts +49 -0
  106. package/src/util/errors.ts +86 -0
  107. package/src/util/hash.ts +10 -1
  108. package/src/util/index.ts +42 -5
  109. package/src/util/paths.ts +168 -5
  110. package/src/util/procfs.ts +20 -0
  111. package/src/util/settle.ts +23 -2
  112. package/src/util/size.ts +33 -0
  113. package/src/util/task-id.ts +20 -0
  114. package/src/util/timing.ts +11 -1
  115. package/src/util/verbs.ts +39 -0
  116. package/src/util/which.ts +48 -0
  117. package/src/workspace/affected.ts +368 -60
  118. package/src/workspace/config-cache.ts +271 -32
  119. package/src/workspace/config-eval.ts +60 -16
  120. package/src/workspace/config-imports.ts +131 -9
  121. package/src/workspace/config-schema.ts +1033 -0
  122. package/src/workspace/filter.ts +69 -17
  123. package/src/workspace/fingerprint.ts +59 -7
  124. package/src/workspace/index.ts +37 -3
  125. package/src/workspace/json-data.ts +90 -0
  126. package/src/workspace/load-reads.ts +32 -0
  127. package/src/workspace/lockfile.ts +41 -14
  128. package/src/workspace/migrate-scripts.ts +187 -0
  129. package/src/workspace/migration.ts +353 -0
  130. package/src/workspace/package-graph.ts +239 -31
  131. package/src/workspace/project-loader.ts +295 -815
  132. package/src/workspace/workspace.ts +266 -31
  133. package/plugins/local-cache/index.ts +0 -2
  134. package/plugins/local-executor/index.ts +0 -2
  135. package/plugins/schedule-history/index.ts +0 -2
  136. package/src/cache/cas-backend.ts +0 -115
  137. package/src/cache/digest.ts +0 -32
  138. package/src/cli/migrate-emit.ts +0 -21
  139. package/src/cli/migrate-nx.ts +0 -473
  140. package/src/cli/migrate-scripts.ts +0 -127
  141. package/src/cli/migrate-turbo.ts +0 -401
  142. package/src/cli/migrate.ts +0 -305
  143. package/src/cli/prune.ts +0 -291
  144. package/src/orchestrator/missing-plugin.ts +0 -9
  145. package/src/orchestrator/resources.ts +0 -45
  146. package/src/orchestrator/verify.ts +0 -174
  147. package/src/plugins/local-cache/index.ts +0 -11
  148. package/src/plugins/local-executor/index.ts +0 -45
  149. package/src/plugins/schedule-history/index.ts +0 -109
@@ -0,0 +1,211 @@
1
+ // Write grants as bwrap can honour them, and the SRT custom config built
2
+ // from the resolved policy: a file-shaped grant widened to its directory,
3
+ // a read grant punched around the write grants inside it, the network
4
+ // lists SRT insists on receiving in full.
5
+
6
+ import { lstatSync, readdirSync, statSync } from 'node:fs'
7
+ import path from 'node:path'
8
+ import { isMountableLiteral, localBindingOn, unique } from './sandbox-paths.js'
9
+ import type { SandboxedRunArgs } from './sandbox-runtime.js'
10
+
11
+ type SrtModule = typeof import('@anthropic-ai/sandbox-runtime')
12
+
13
+ /**
14
+ * Write grants as bwrap can actually honour them.
15
+ *
16
+ * A grant naming a FILE becomes a bwrap file bind, and you cannot rename
17
+ * onto an active mount point: any tool that writes its output by staging
18
+ * beside it and renaming — `bun build --compile`, most compilers, every
19
+ * atomic writer — dies with EBUSY. Minimal repro, 2026-09-05: under
20
+ * `bwrap --bind /w/dist/out.bin /w/dist/out.bin`, `mv /w/s /w/dist/out.bin`
21
+ * is "Device or resource busy"; binding `/w/dist` instead succeeds.
22
+ *
23
+ * So on Linux a file-shaped grant is widened to its directory. That IS a
24
+ * widening — the task may write its siblings — and it is the narrowest
25
+ * grant the mechanism can express: the alternative is a declared output
26
+ * the task cannot produce. macOS needs none of this (seatbelt matches
27
+ * paths, it does not mount), so the grant stays exact there.
28
+ */
29
+ export function bindableWrites(paths: readonly string[]): string[] {
30
+ if (process.platform !== 'linux') return [...paths]
31
+ return unique(
32
+ paths.map((p) => {
33
+ if (!isMountableLiteral(p)) return p
34
+ try {
35
+ if (statSync(p).isDirectory()) return p
36
+ } catch {
37
+ // Does not exist yet: `prepareOutputsForBind` creates a file for a
38
+ // file-shaped grant, so treat it as one.
39
+ }
40
+ return path.dirname(p)
41
+ }),
42
+ )
43
+ }
44
+
45
+ /**
46
+ * A read grant with the WALLS inside it cut out: the directories of the
47
+ * projects nested in this one, the repository and vx's own directory. A
48
+ * root project's `read: ['.']` bound every nested project, `.git` and the
49
+ * cache readable, and the key, which excludes nested projects, replayed a
50
+ * nested file's old bytes (item 1010). The cut is the punch above, a wall
51
+ * dropped where a write path is bound: its siblings are granted, the wall
52
+ * is not. A grant that IS a wall, or lies inside one, names it on purpose
53
+ * and stays. Linux only, as the punch is; a grant with no wall under it
54
+ * costs nothing.
55
+ */
56
+ export function punchWalls(readPath: string, walls: readonly string[]): string[] {
57
+ if (process.platform !== 'linux') return [readPath]
58
+ const under = walls.filter((w) => w !== readPath && w.startsWith(readPath + path.sep))
59
+ if (under.length === 0) return [readPath]
60
+ let entries: string[]
61
+ try {
62
+ entries = readdirSync(readPath)
63
+ } catch {
64
+ return [readPath]
65
+ }
66
+ const out: string[] = []
67
+ for (const entry of entries) {
68
+ const child = path.join(readPath, entry)
69
+ if (under.includes(child)) continue
70
+ out.push(...punchWalls(child, under))
71
+ }
72
+ return out
73
+ }
74
+
75
+ /** Directories already warned about below — once per process, not per spawn. */
76
+ const warnedSymlinkPunch = new Set<string>()
77
+
78
+ /**
79
+ * Expand a read grant so it is never an ANCESTOR of a write grant.
80
+ *
81
+ * bwrap builds the sandbox out of mounts, and SRT emits them write-first:
82
+ * `--bind <out>` then `--ro-bind <readPath>`. When the read path is an
83
+ * ancestor of the write path the read-only mount lands ON TOP of the
84
+ * writable one and every write fails with `Read-only file system`
85
+ * (`pushReadDenyDirMounts`, SRT 0.0.75 — its skip only covers the reverse
86
+ * nesting). Verified in a Linux container 2026-09-05:
87
+ *
88
+ * read=[proj] write=[proj/dist] → mkdir: Read-only file system
89
+ * read=[proj/src] write=[proj/dist] → ok
90
+ * read=[proj] no writes → ok
91
+ *
92
+ * So punch the write paths out: grant the ancestor's children instead,
93
+ * recursing only along the branches that actually contain one. Same probe,
94
+ * same command: `read=[src, lib, package.json] write=[dist]` → ok. macOS
95
+ * never needed this (seatbelt is precedence-based, not mounts), and it is
96
+ * harmless there, so both platforms take the same path.
97
+ *
98
+ * A grant with no write path under it is returned untouched — the common
99
+ * case costs nothing, not even a readdir.
100
+ */
101
+ export function punchWritePaths(readPath: string, writePaths: readonly string[]): string[] {
102
+ // Linux only. The shadowing is a property of bwrap MOUNTS; macOS seatbelt
103
+ // evaluates rules by precedence, so a read grant on a directory and a
104
+ // write grant inside it coexist. Punching there costs the directory
105
+ // ENTRY: granting every child is not granting the dir, so a command that
106
+ // stats its own cwd — `bun build` — is denied it and dies with
107
+ // `error: An unknown error occurred (Unexpected)` (2026-09-05).
108
+ if (process.platform !== 'linux') return [readPath]
109
+ const under = writePaths.filter((w) => w !== readPath && w.startsWith(readPath + path.sep))
110
+ if (under.length === 0) return [readPath]
111
+ let entries: string[]
112
+ try {
113
+ entries = readdirSync(readPath)
114
+ } catch {
115
+ // Unreadable or not a directory: nothing to expand, hand it over as is.
116
+ return [readPath]
117
+ }
118
+ const out: string[] = []
119
+ const linked: string[] = []
120
+ for (const entry of entries) {
121
+ const child = path.join(readPath, entry)
122
+ // A write path is already bound read-write, which is readable.
123
+ if (under.includes(child)) continue
124
+ // bwrap resolves a bind SOURCE, so a symlinked child is mounted as the
125
+ // directory it points at: inside the sandbox the link is gone. For a
126
+ // package in Bun's isolated node_modules layout that severs it from
127
+ // the `.bun/` siblings its own dependencies resolve through — astro
128
+ // could not find `yargs-parser` for four days of red CI (2026-09-09).
129
+ // SRT's config carries no `--symlink`, so the only fix is the grant:
130
+ // say which one, loudly.
131
+ try {
132
+ if (lstatSync(child).isSymbolicLink()) linked.push(child)
133
+ } catch {
134
+ // vanished between readdir and lstat: nothing to bind either way
135
+ }
136
+ out.push(...punchWritePaths(child, under))
137
+ }
138
+ if (linked.length > 0 && !warnedSymlinkPunch.has(readPath)) {
139
+ warnedSymlinkPunch.add(readPath)
140
+ process.stderr.write(
141
+ `[vx] sandbox: a write grant under ${readPath} makes its ${linked.length} symlinked ` +
142
+ `entr${linked.length === 1 ? 'y' : 'ies'} (${linked
143
+ .slice(0, 3)
144
+ .map((l) => path.basename(l))
145
+ .join(', ')}${linked.length > 3 ? ', …' : ''}) plain directories inside the sandbox — ` +
146
+ `a package resolved through one loses its siblings. Move the write grant (${under
147
+ .map((w) => path.relative(readPath, w))
148
+ .join(', ')}) out of it.\n`,
149
+ )
150
+ }
151
+ return out
152
+ }
153
+
154
+ /**
155
+ * Merge the orchestrator-provided baseline (declared inputs / outputs /
156
+ * workspace-root anchor) with the user's resolved sandbox block to
157
+ * produce the SRT customConfig. Path arrays are unioned and deduped; every
158
+ * read grant is punched around the write grants (`punchWritePaths`).
159
+ *
160
+ * Network: `allow.network` missing → block all (allowedDomains: []);
161
+ * `true` → allow all (['*']); a domain list → exactly that list.
162
+ * `deny.network` is always passed as deniedDomains.
163
+ */
164
+ export function buildCustomConfig(
165
+ args: Pick<SandboxedRunArgs, 'config'>,
166
+ baselines: {
167
+ allowRead: readonly string[]
168
+ denyRead: readonly string[]
169
+ },
170
+ ): Parameters<SrtModule['SandboxManager']['wrapWithSandbox']>[2] {
171
+ const c = args.config
172
+ const denyRead = unique([...baselines.denyRead])
173
+ const allowWrite = bindableWrites(unique([...c.allowWrite]))
174
+ const allowRead = unique(
175
+ [...baselines.allowRead, ...c.allowRead].flatMap((r) => punchWritePaths(r, allowWrite)),
176
+ )
177
+
178
+ const custom: Parameters<SrtModule['SandboxManager']['wrapWithSandbox']>[2] = {
179
+ filesystem: {
180
+ denyRead,
181
+ allowRead,
182
+ allowWrite,
183
+ denyWrite: [],
184
+ ...(c.gitConfig !== undefined ? { allowGitConfig: c.gitConfig } : {}),
185
+ },
186
+ }
187
+
188
+ // `allow.network` / `deny.network` become SRT's domain lists. SRT requires
189
+ // both to be present on any network config, so we always supply both;
190
+ // omitted means no network at all.
191
+ custom.network = {
192
+ allowedDomains: c.network === true ? ['*'] : [...(c.network ?? [])],
193
+ deniedDomains: [...(c.denyNetwork ?? [])],
194
+ ...(c.unixSockets === true
195
+ ? { allowAllUnixSockets: true }
196
+ : c.unixSockets !== undefined
197
+ ? { allowUnixSockets: [...c.unixSockets] }
198
+ : {}),
199
+ ...(c.localBinding !== undefined ? { allowLocalBinding: localBindingOn(c) } : {}),
200
+ ...(c.machLookup !== undefined ? { allowMachLookup: [...c.machLookup] } : {}),
201
+ }
202
+
203
+ if (c.pty !== undefined) custom.allowPty = c.pty
204
+ if (c.weakerWhenNested !== undefined) {
205
+ custom.enableWeakerNestedSandbox = c.weakerWhenNested
206
+ }
207
+ if (c.weakerNetworkIsolation !== undefined) {
208
+ custom.enableWeakerNetworkIsolation = c.weakerNetworkIsolation
209
+ }
210
+ return custom
211
+ }
@@ -0,0 +1,70 @@
1
+ // Path helpers the sandbox modules share: canonical forms (the policy
2
+ // matches real paths), the task-relative absolutizer the strace pass uses,
3
+ // and the prefix test.
4
+
5
+ import { realpathSync } from 'node:fs'
6
+ import path from 'node:path'
7
+
8
+ /**
9
+ * The wildcard alphabet of a GRANT — what decides whether a declared read or
10
+ * write path is scanned for its matches or mounted as the path it names.
11
+ * Deliberately smaller than `BUN_GLOB_WILDCARDS` (util/paths.ts, `Bun.Glob`'s
12
+ * own set), which also counts `{}`: `write: ['g/{a,b}.txt']` is a
13
+ * LITERAL to the sandbox, gets a placeholder file, and is widened to its
14
+ * directory like any other file-shaped grant — measured ok, and pinned in
15
+ * `tests/sandbox-runtime.unsafe.test.ts`. Counting the brace here would
16
+ * move that spelling into the scan, which finds nothing before the task has
17
+ * written, and turn a working grant into `Read-only file system`. The two
18
+ * predicates answer different questions: whether a declaration must be
19
+ * MATCHED against other declarations (495; `GLOB_WILDCARDS`, where a bracket
20
+ * is literal since item 667), and whether a grant can be mounted (here).
21
+ * Item 577 gave each question one spelling.
22
+ */
23
+ export const MOUNT_WILDCARDS = /[*?[\]]/
24
+
25
+ /** True when a grant names one path the sandbox can mount, rather than a pattern to scan. */
26
+ export function isMountableLiteral(grant: string): boolean {
27
+ return !MOUNT_WILDCARDS.test(grant)
28
+ }
29
+
30
+ /**
31
+ * Canonicalize a path with realpath, tolerating paths that don't exist
32
+ * yet: resolve the longest existing ancestor and re-append the rest.
33
+ *
34
+ * Why: the sandbox policy matches on canonical paths (macOS seatbelt
35
+ * evaluates real vnode paths), and SRT's own normalization refuses to
36
+ * canonicalize bare symlinked roots like `/tmp` → `/private/tmp` (its
37
+ * boundary check only whitelists `/tmp/<child>` forms). Without this,
38
+ * `allowWrite: ['/tmp']` silently never matches on macOS.
39
+ */
40
+ export function toRealPath(p: string): string {
41
+ try {
42
+ return realpathSync(p)
43
+ } catch {
44
+ const parent = path.dirname(p)
45
+ if (parent === p) return p
46
+ return path.join(toRealPath(parent), path.basename(p))
47
+ }
48
+ }
49
+
50
+ export function absolutize(p: string, cwd?: string): string {
51
+ if (path.isAbsolute(p)) return p
52
+ return path.resolve(cwd ?? process.cwd(), p)
53
+ }
54
+
55
+ export function isUnderAny(abs: string, allow: Set<string>): boolean {
56
+ if (allow.has(abs)) return true
57
+ for (const a of allow) {
58
+ if (abs === a || abs.startsWith(a + path.sep)) return true
59
+ }
60
+ return false
61
+ }
62
+
63
+ export function unique(arr: readonly string[]): string[] {
64
+ return [...new Set(arr)]
65
+ }
66
+
67
+ /** `allow.localBinding` grants loopback: `true`, or a non-empty port list. */
68
+ export function localBindingOn(c: { localBinding?: boolean | readonly number[] }): boolean {
69
+ return c.localBinding === true || (Array.isArray(c.localBinding) && c.localBinding.length > 0)
70
+ }