@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.
- package/LICENSE +1 -1
- package/README.md +135 -124
- package/index.ts +10 -0
- package/launcher.mjs +10 -11
- package/package.json +11 -7
- package/src/bin.ts +14 -4
- package/src/cache/archive.ts +659 -0
- package/src/cache/cache.ts +769 -1101
- package/src/cache/chained-cache.ts +194 -0
- package/src/cache/config-evals.ts +89 -0
- package/src/cache/file-hashes.ts +262 -0
- package/src/cache/git-inputs.ts +637 -0
- package/src/cache/index.ts +24 -5
- package/src/cache/inputs.ts +373 -456
- package/src/cache/layer.ts +604 -0
- package/src/cache/layered-cache.ts +96 -43
- package/src/cache/output-index.ts +224 -0
- package/src/cache/policy.ts +84 -0
- package/src/cache/run-history.ts +147 -0
- package/src/cache/tar-stream.ts +358 -0
- package/src/cache/zstd.ts +160 -0
- package/src/cli/cache.ts +75 -21
- package/src/cli/completions.ts +104 -0
- package/src/cli/core-alias.ts +25 -0
- package/src/cli/help.ts +195 -128
- package/src/cli/index.ts +116 -46
- package/src/cli/info.ts +122 -63
- package/src/cli/init.ts +75 -0
- package/src/cli/last.ts +172 -0
- package/src/cli/lock.ts +2 -1
- package/src/cli/plan-format.ts +72 -6
- package/src/cli/plugin-commands.ts +103 -0
- package/src/cli/run.ts +245 -271
- package/src/cli/select.ts +332 -0
- package/src/cli/show.ts +144 -69
- package/src/cli/upgrade.ts +9 -7
- package/src/cli/watch.ts +858 -110
- package/src/cli/why.ts +223 -0
- package/src/cli/workspace-config.ts +126 -0
- package/src/config.ts +187 -150
- package/src/exec/env.ts +18 -1
- package/src/exec/executor.ts +224 -0
- package/src/exec/index.ts +24 -2
- package/src/exec/local-executor.ts +41 -0
- package/src/exec/runner.ts +166 -40
- package/src/exec/sandbox-binds.ts +182 -0
- package/src/exec/sandbox-paths.ts +50 -0
- package/src/exec/sandbox-runtime.ts +642 -260
- package/src/exec/sandbox-violations.ts +280 -0
- package/src/graph/index.ts +4 -10
- package/src/graph/scheduler.ts +232 -203
- package/src/graph/task-graph.ts +222 -8
- package/src/index.ts +146 -153
- package/src/orchestrator/admission.ts +137 -0
- package/src/orchestrator/deferred-outputs.ts +147 -0
- package/src/orchestrator/doctor.ts +255 -0
- package/src/orchestrator/download-policy.ts +171 -0
- package/src/orchestrator/events.ts +72 -8
- package/src/orchestrator/execute-task.ts +374 -549
- package/src/orchestrator/failure-mode.ts +202 -0
- package/src/orchestrator/framed-output.ts +126 -69
- package/src/orchestrator/history.ts +126 -59
- package/src/orchestrator/hit-restore.ts +221 -0
- package/src/orchestrator/index.ts +77 -93
- package/src/orchestrator/local-shortcircuit.ts +22 -0
- package/src/orchestrator/lockfile-claim.ts +281 -0
- package/src/orchestrator/logger.ts +208 -35
- package/src/orchestrator/metrics.ts +102 -1388
- package/src/orchestrator/miss-save.ts +169 -0
- package/src/orchestrator/options.ts +55 -34
- package/src/orchestrator/persistent.ts +79 -0
- package/src/orchestrator/placement.ts +182 -0
- package/src/orchestrator/plan.ts +144 -1
- package/src/orchestrator/plugin-host.ts +357 -120
- package/src/orchestrator/plugin.ts +298 -58
- package/src/orchestrator/prepare.ts +202 -106
- package/src/orchestrator/projects.ts +259 -0
- package/src/orchestrator/remote-prefetch.ts +12 -8
- package/src/orchestrator/run-artifacts.ts +63 -16
- package/src/orchestrator/run-context.ts +167 -29
- package/src/orchestrator/run-records.ts +149 -0
- package/src/orchestrator/run-report.ts +74 -47
- package/src/orchestrator/run.ts +534 -417
- package/src/orchestrator/sandbox-request.ts +243 -0
- package/src/orchestrator/save-lane.ts +47 -0
- package/src/orchestrator/signals.ts +111 -0
- package/src/orchestrator/stable-keys.ts +129 -31
- package/src/orchestrator/status-line.ts +49 -9
- package/src/orchestrator/summary.ts +90 -26
- package/src/orchestrator/tally.ts +78 -35
- package/src/orchestrator/task-hash.ts +111 -15
- package/src/orchestrator/task-log-buffer.ts +288 -0
- package/src/orchestrator/telemetry-host.ts +34 -6
- package/src/orchestrator/telemetry.ts +182 -36
- package/src/orchestrator/upstream.ts +72 -31
- package/src/util/cgroup.ts +160 -0
- package/src/util/edit-distance.ts +70 -0
- package/src/util/errors.ts +15 -1
- package/src/util/index.ts +15 -2
- package/src/util/num.ts +37 -0
- package/src/util/paths.ts +64 -0
- package/src/util/settle.ts +67 -0
- package/src/util/size.ts +5 -1
- package/src/util/tail.ts +66 -0
- package/src/util/timing.ts +63 -0
- package/src/util/verbs.ts +39 -0
- package/src/workspace/affected.ts +250 -23
- package/src/workspace/config-cache.ts +313 -0
- package/src/workspace/config-eval.ts +217 -0
- package/src/workspace/config-imports.ts +162 -0
- package/src/workspace/config-schema.ts +837 -0
- package/src/workspace/filter.ts +65 -10
- package/src/workspace/fingerprint.ts +53 -4
- package/src/workspace/index.ts +42 -3
- package/src/workspace/lockfile.ts +25 -9
- package/src/workspace/migrate-scripts.ts +134 -0
- package/src/workspace/migration.ts +314 -0
- package/src/workspace/nested-dirs.ts +21 -7
- package/src/workspace/package-graph.ts +114 -41
- package/src/workspace/project-loader.ts +224 -450
- package/src/workspace/workspace.ts +298 -72
- package/src/cache/cas-backend.ts +0 -97
- package/src/cache/digest.ts +0 -47
- package/src/cache/tar.ts +0 -343
- package/src/cli/backend.ts +0 -23
- package/src/cli/mcp-rpc.ts +0 -292
- package/src/cli/mcp.ts +0 -92
- package/src/cli/migrate-nx.ts +0 -450
- package/src/cli/migrate-turbo.ts +0 -364
- package/src/cli/migrate.ts +0 -219
- package/src/orchestrator/devframe-surface.ts +0 -74
- package/src/orchestrator/predict.ts +0 -106
- package/src/orchestrator/protocol.ts +0 -156
- package/src/orchestrator/resources.ts +0 -59
- package/src/orchestrator/run-state.ts +0 -145
- package/src/orchestrator/verify.ts +0 -164
- package/src/orchestrator/wire-render.ts +0 -92
- 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
|
+
}
|