@vzn/vx 0.0.16 → 0.0.21

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 (138) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +135 -124
  3. package/index.ts +10 -0
  4. package/launcher.mjs +10 -11
  5. package/package.json +11 -7
  6. package/src/bin.ts +14 -4
  7. package/src/cache/archive.ts +659 -0
  8. package/src/cache/cache.ts +769 -1101
  9. package/src/cache/chained-cache.ts +194 -0
  10. package/src/cache/config-evals.ts +89 -0
  11. package/src/cache/file-hashes.ts +262 -0
  12. package/src/cache/git-inputs.ts +637 -0
  13. package/src/cache/index.ts +24 -5
  14. package/src/cache/inputs.ts +373 -456
  15. package/src/cache/layer.ts +604 -0
  16. package/src/cache/layered-cache.ts +96 -43
  17. package/src/cache/output-index.ts +224 -0
  18. package/src/cache/policy.ts +84 -0
  19. package/src/cache/run-history.ts +147 -0
  20. package/src/cache/tar-stream.ts +358 -0
  21. package/src/cache/zstd.ts +160 -0
  22. package/src/cli/cache.ts +75 -21
  23. package/src/cli/completions.ts +104 -0
  24. package/src/cli/core-alias.ts +25 -0
  25. package/src/cli/help.ts +195 -128
  26. package/src/cli/index.ts +116 -46
  27. package/src/cli/info.ts +122 -63
  28. package/src/cli/init.ts +75 -0
  29. package/src/cli/last.ts +172 -0
  30. package/src/cli/lock.ts +2 -1
  31. package/src/cli/plan-format.ts +72 -6
  32. package/src/cli/plugin-commands.ts +103 -0
  33. package/src/cli/run.ts +245 -271
  34. package/src/cli/select.ts +332 -0
  35. package/src/cli/show.ts +144 -69
  36. package/src/cli/upgrade.ts +9 -7
  37. package/src/cli/watch.ts +858 -110
  38. package/src/cli/why.ts +223 -0
  39. package/src/cli/workspace-config.ts +126 -0
  40. package/src/config.ts +187 -150
  41. package/src/exec/env.ts +18 -1
  42. package/src/exec/executor.ts +224 -0
  43. package/src/exec/index.ts +24 -2
  44. package/src/exec/local-executor.ts +41 -0
  45. package/src/exec/runner.ts +166 -40
  46. package/src/exec/sandbox-binds.ts +182 -0
  47. package/src/exec/sandbox-paths.ts +50 -0
  48. package/src/exec/sandbox-runtime.ts +642 -260
  49. package/src/exec/sandbox-violations.ts +280 -0
  50. package/src/graph/index.ts +4 -10
  51. package/src/graph/scheduler.ts +232 -203
  52. package/src/graph/task-graph.ts +222 -8
  53. package/src/index.ts +146 -153
  54. package/src/orchestrator/admission.ts +137 -0
  55. package/src/orchestrator/deferred-outputs.ts +147 -0
  56. package/src/orchestrator/doctor.ts +255 -0
  57. package/src/orchestrator/download-policy.ts +171 -0
  58. package/src/orchestrator/events.ts +72 -8
  59. package/src/orchestrator/execute-task.ts +374 -549
  60. package/src/orchestrator/failure-mode.ts +202 -0
  61. package/src/orchestrator/framed-output.ts +126 -69
  62. package/src/orchestrator/history.ts +126 -59
  63. package/src/orchestrator/hit-restore.ts +221 -0
  64. package/src/orchestrator/index.ts +77 -93
  65. package/src/orchestrator/local-shortcircuit.ts +22 -0
  66. package/src/orchestrator/lockfile-claim.ts +281 -0
  67. package/src/orchestrator/logger.ts +208 -35
  68. package/src/orchestrator/metrics.ts +102 -1388
  69. package/src/orchestrator/miss-save.ts +169 -0
  70. package/src/orchestrator/options.ts +55 -34
  71. package/src/orchestrator/persistent.ts +79 -0
  72. package/src/orchestrator/placement.ts +182 -0
  73. package/src/orchestrator/plan.ts +144 -1
  74. package/src/orchestrator/plugin-host.ts +357 -120
  75. package/src/orchestrator/plugin.ts +298 -58
  76. package/src/orchestrator/prepare.ts +202 -106
  77. package/src/orchestrator/projects.ts +259 -0
  78. package/src/orchestrator/remote-prefetch.ts +12 -8
  79. package/src/orchestrator/run-artifacts.ts +63 -16
  80. package/src/orchestrator/run-context.ts +167 -29
  81. package/src/orchestrator/run-records.ts +149 -0
  82. package/src/orchestrator/run-report.ts +74 -47
  83. package/src/orchestrator/run.ts +534 -417
  84. package/src/orchestrator/sandbox-request.ts +243 -0
  85. package/src/orchestrator/save-lane.ts +47 -0
  86. package/src/orchestrator/signals.ts +111 -0
  87. package/src/orchestrator/stable-keys.ts +129 -31
  88. package/src/orchestrator/status-line.ts +49 -9
  89. package/src/orchestrator/summary.ts +90 -26
  90. package/src/orchestrator/tally.ts +78 -35
  91. package/src/orchestrator/task-hash.ts +111 -15
  92. package/src/orchestrator/task-log-buffer.ts +288 -0
  93. package/src/orchestrator/telemetry-host.ts +34 -6
  94. package/src/orchestrator/telemetry.ts +182 -36
  95. package/src/orchestrator/upstream.ts +72 -31
  96. package/src/util/cgroup.ts +160 -0
  97. package/src/util/edit-distance.ts +70 -0
  98. package/src/util/errors.ts +15 -1
  99. package/src/util/index.ts +15 -2
  100. package/src/util/num.ts +37 -0
  101. package/src/util/paths.ts +64 -0
  102. package/src/util/settle.ts +67 -0
  103. package/src/util/size.ts +5 -1
  104. package/src/util/tail.ts +66 -0
  105. package/src/util/timing.ts +63 -0
  106. package/src/util/verbs.ts +39 -0
  107. package/src/workspace/affected.ts +250 -23
  108. package/src/workspace/config-cache.ts +313 -0
  109. package/src/workspace/config-eval.ts +217 -0
  110. package/src/workspace/config-imports.ts +162 -0
  111. package/src/workspace/config-schema.ts +837 -0
  112. package/src/workspace/filter.ts +65 -10
  113. package/src/workspace/fingerprint.ts +53 -4
  114. package/src/workspace/index.ts +42 -3
  115. package/src/workspace/lockfile.ts +25 -9
  116. package/src/workspace/migrate-scripts.ts +134 -0
  117. package/src/workspace/migration.ts +314 -0
  118. package/src/workspace/nested-dirs.ts +21 -7
  119. package/src/workspace/package-graph.ts +114 -41
  120. package/src/workspace/project-loader.ts +224 -450
  121. package/src/workspace/workspace.ts +298 -72
  122. package/src/cache/cas-backend.ts +0 -97
  123. package/src/cache/digest.ts +0 -47
  124. package/src/cache/tar.ts +0 -343
  125. package/src/cli/backend.ts +0 -23
  126. package/src/cli/mcp-rpc.ts +0 -292
  127. package/src/cli/mcp.ts +0 -92
  128. package/src/cli/migrate-nx.ts +0 -450
  129. package/src/cli/migrate-turbo.ts +0 -364
  130. package/src/cli/migrate.ts +0 -219
  131. package/src/orchestrator/devframe-surface.ts +0 -74
  132. package/src/orchestrator/predict.ts +0 -106
  133. package/src/orchestrator/protocol.ts +0 -156
  134. package/src/orchestrator/resources.ts +0 -59
  135. package/src/orchestrator/run-state.ts +0 -145
  136. package/src/orchestrator/verify.ts +0 -164
  137. package/src/orchestrator/wire-render.ts +0 -92
  138. package/src/orchestrator/wire.ts +0 -212
@@ -0,0 +1,182 @@
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 { 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
+ function bindableWrites(paths: readonly string[]): string[] {
30
+ if (process.platform !== 'linux') return [...paths]
31
+ return unique(
32
+ paths.map((p) => {
33
+ if (/[*?[\]]/.test(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
+ /** Directories already warned about below — once per process, not per spawn. */
46
+ const warnedSymlinkPunch = new Set<string>()
47
+
48
+ /**
49
+ * Expand a read grant so it is never an ANCESTOR of a write grant.
50
+ *
51
+ * bwrap builds the sandbox out of mounts, and SRT emits them write-first:
52
+ * `--bind <out>` then `--ro-bind <readPath>`. When the read path is an
53
+ * ancestor of the write path the read-only mount lands ON TOP of the
54
+ * writable one and every write fails with `Read-only file system`
55
+ * (`pushReadDenyDirMounts`, SRT 0.0.75 — its skip only covers the reverse
56
+ * nesting). Verified in a Linux container 2026-09-05:
57
+ *
58
+ * read=[proj] write=[proj/dist] → mkdir: Read-only file system
59
+ * read=[proj/src] write=[proj/dist] → ok
60
+ * read=[proj] no writes → ok
61
+ *
62
+ * So punch the write paths out: grant the ancestor's children instead,
63
+ * recursing only along the branches that actually contain one. Same probe,
64
+ * same command: `read=[src, lib, package.json] write=[dist]` → ok. macOS
65
+ * never needed this (seatbelt is precedence-based, not mounts), and it is
66
+ * harmless there, so both platforms take the same path.
67
+ *
68
+ * A grant with no write path under it is returned untouched — the common
69
+ * case costs nothing, not even a readdir.
70
+ */
71
+ export function punchWritePaths(readPath: string, writePaths: readonly string[]): string[] {
72
+ // Linux only. The shadowing is a property of bwrap MOUNTS; macOS seatbelt
73
+ // evaluates rules by precedence, so a read grant on a directory and a
74
+ // write grant inside it coexist. Punching there costs the directory
75
+ // ENTRY: granting every child is not granting the dir, so a command that
76
+ // stats its own cwd — `bun build` — is denied it and dies with
77
+ // `error: An unknown error occurred (Unexpected)` (2026-09-05).
78
+ if (process.platform !== 'linux') return [readPath]
79
+ const under = writePaths.filter((w) => w !== readPath && w.startsWith(readPath + path.sep))
80
+ if (under.length === 0) return [readPath]
81
+ let entries: string[]
82
+ try {
83
+ entries = readdirSync(readPath)
84
+ } catch {
85
+ // Unreadable or not a directory: nothing to expand, hand it over as is.
86
+ return [readPath]
87
+ }
88
+ const out: string[] = []
89
+ const linked: string[] = []
90
+ for (const entry of entries) {
91
+ const child = path.join(readPath, entry)
92
+ // A write path is already bound read-write, which is readable.
93
+ if (under.includes(child)) continue
94
+ // bwrap resolves a bind SOURCE, so a symlinked child is mounted as the
95
+ // directory it points at: inside the sandbox the link is gone. For a
96
+ // package in Bun's isolated node_modules layout that severs it from
97
+ // the `.bun/` siblings its own dependencies resolve through — astro
98
+ // could not find `yargs-parser` for four days of red CI (2026-09-09).
99
+ // SRT's config carries no `--symlink`, so the only fix is the grant:
100
+ // say which one, loudly.
101
+ try {
102
+ if (lstatSync(child).isSymbolicLink()) linked.push(child)
103
+ } catch {
104
+ // vanished between readdir and lstat: nothing to bind either way
105
+ }
106
+ out.push(...punchWritePaths(child, under))
107
+ }
108
+ if (linked.length > 0 && !warnedSymlinkPunch.has(readPath)) {
109
+ warnedSymlinkPunch.add(readPath)
110
+ process.stderr.write(
111
+ `[vx] sandbox: a write grant under ${readPath} makes its ${linked.length} symlinked ` +
112
+ `entr${linked.length === 1 ? 'y' : 'ies'} (${linked
113
+ .slice(0, 3)
114
+ .map((l) => path.basename(l))
115
+ .join(', ')}${linked.length > 3 ? ', …' : ''}) plain directories inside the sandbox — ` +
116
+ `a package resolved through one loses its siblings. Move the write grant (${under
117
+ .map((w) => path.relative(readPath, w))
118
+ .join(', ')}) out of it.\n`,
119
+ )
120
+ }
121
+ return out
122
+ }
123
+
124
+ /**
125
+ * Merge the orchestrator-provided baseline (declared inputs / outputs /
126
+ * workspace-root anchor) with the user's resolved sandbox block to
127
+ * produce the SRT customConfig. Path arrays are unioned and deduped; every
128
+ * read grant is punched around the write grants (`punchWritePaths`).
129
+ *
130
+ * Network: `allow.network` missing → block all (allowedDomains: []);
131
+ * `true` → allow all (['*']); a domain list → exactly that list.
132
+ * `deny.network` is always passed as deniedDomains.
133
+ */
134
+ export function buildCustomConfig(
135
+ args: Pick<SandboxedRunArgs, 'config'>,
136
+ baselines: {
137
+ allowRead: readonly string[]
138
+ allowWrite: readonly string[]
139
+ denyRead: readonly string[]
140
+ },
141
+ ): Parameters<SrtModule['SandboxManager']['wrapWithSandbox']>[2] {
142
+ const c = args.config
143
+ const denyRead = unique([...baselines.denyRead])
144
+ const allowWrite = bindableWrites(unique([...baselines.allowWrite, ...c.allowWrite]))
145
+ const allowRead = unique(
146
+ [...baselines.allowRead, ...c.allowRead].flatMap((r) => punchWritePaths(r, allowWrite)),
147
+ )
148
+
149
+ const custom: Parameters<SrtModule['SandboxManager']['wrapWithSandbox']>[2] = {
150
+ filesystem: {
151
+ denyRead,
152
+ allowRead,
153
+ allowWrite,
154
+ denyWrite: [],
155
+ ...(c.gitConfig !== undefined ? { allowGitConfig: c.gitConfig } : {}),
156
+ },
157
+ }
158
+
159
+ // `allow.network` / `deny.network` become SRT's domain lists. SRT requires
160
+ // both to be present on any network config, so we always supply both;
161
+ // omitted means no network at all.
162
+ custom.network = {
163
+ allowedDomains: c.network === true ? ['*'] : [...(c.network ?? [])],
164
+ deniedDomains: [...(c.denyNetwork ?? [])],
165
+ ...(c.unixSockets === true
166
+ ? { allowAllUnixSockets: true }
167
+ : c.unixSockets !== undefined
168
+ ? { allowUnixSockets: [...c.unixSockets] }
169
+ : {}),
170
+ ...(c.localBinding !== undefined ? { allowLocalBinding: localBindingOn(c) } : {}),
171
+ ...(c.machLookup !== undefined ? { allowMachLookup: [...c.machLookup] } : {}),
172
+ }
173
+
174
+ if (c.pty !== undefined) custom.allowPty = c.pty
175
+ if (c.weakerWhenNested !== undefined) {
176
+ custom.enableWeakerNestedSandbox = c.weakerWhenNested
177
+ }
178
+ if (c.weakerNetworkIsolation !== undefined) {
179
+ custom.enableWeakerNetworkIsolation = c.weakerNetworkIsolation
180
+ }
181
+ return custom
182
+ }
@@ -0,0 +1,50 @@
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 os from 'node:os'
7
+ import path from 'node:path'
8
+
9
+ /**
10
+ * Canonicalize a path with realpath, tolerating paths that don't exist
11
+ * yet: resolve the longest existing ancestor and re-append the rest.
12
+ *
13
+ * Why: the sandbox policy matches on canonical paths (macOS seatbelt
14
+ * evaluates real vnode paths), and SRT's own normalization refuses to
15
+ * canonicalize bare symlinked roots like `/tmp` → `/private/tmp` (its
16
+ * boundary check only whitelists `/tmp/<child>` forms). Without this,
17
+ * `allowWrite: ['/tmp']` silently never matches on macOS.
18
+ */
19
+ export function toRealPath(p: string): string {
20
+ try {
21
+ return realpathSync(p)
22
+ } catch {
23
+ const parent = path.dirname(p)
24
+ if (parent === p) return p
25
+ return path.join(toRealPath(parent), path.basename(p))
26
+ }
27
+ }
28
+
29
+ export function absolutize(p: string, cwd?: string): string {
30
+ if (p.startsWith('~')) return path.join(os.homedir(), p.slice(1))
31
+ if (path.isAbsolute(p)) return p
32
+ return path.resolve(cwd ?? process.cwd(), p)
33
+ }
34
+
35
+ export function isUnderAny(abs: string, allow: Set<string>): boolean {
36
+ if (allow.has(abs)) return true
37
+ for (const a of allow) {
38
+ if (abs === a || abs.startsWith(a + path.sep)) return true
39
+ }
40
+ return false
41
+ }
42
+
43
+ export function unique(arr: readonly string[]): string[] {
44
+ return [...new Set(arr)]
45
+ }
46
+
47
+ /** `allow.localBinding` grants loopback: `true`, or a non-empty port list. */
48
+ export function localBindingOn(c: { localBinding?: boolean | readonly number[] }): boolean {
49
+ return c.localBinding === true || (Array.isArray(c.localBinding) && c.localBinding.length > 0)
50
+ }