@vzn/vx 0.0.129 → 0.0.131
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/package.json +5 -5
- package/src/exec/index.ts +1 -1
- package/src/exec/sandbox-paths.ts +9 -1
- package/src/exec/sandbox-runtime.ts +9 -14
- package/src/exec/sandbox-violations.ts +2 -2
- package/src/orchestrator/sandbox-request.ts +13 -17
- package/src/orchestrator/upstream.ts +7 -1
- package/src/workspace/config-schema.ts +20 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.131",
|
|
4
4
|
"description": "A fast, correct task runner and build cache for JavaScript monorepos.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
"node": ">=18"
|
|
18
18
|
},
|
|
19
19
|
"optionalDependencies": {
|
|
20
|
-
"@vzn/vx-linux-x64": "0.0.
|
|
21
|
-
"@vzn/vx-linux-arm64": "0.0.
|
|
22
|
-
"@vzn/vx-darwin-x64": "0.0.
|
|
23
|
-
"@vzn/vx-darwin-arm64": "0.0.
|
|
20
|
+
"@vzn/vx-linux-x64": "0.0.131",
|
|
21
|
+
"@vzn/vx-linux-arm64": "0.0.131",
|
|
22
|
+
"@vzn/vx-darwin-x64": "0.0.131",
|
|
23
|
+
"@vzn/vx-darwin-arm64": "0.0.131"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@anthropic-ai/sandbox-runtime": "^0.0.76"
|
package/src/exec/index.ts
CHANGED
|
@@ -28,7 +28,7 @@ export {
|
|
|
28
28
|
thrownReason,
|
|
29
29
|
} from './sandbox-runtime.js'
|
|
30
30
|
export { type DeniedCall } from './sandbox-violations.js'
|
|
31
|
-
export { isMountableLiteral, MOUNT_WILDCARDS, toRealPath } from './sandbox-paths.js'
|
|
31
|
+
export { atOrUnder, isMountableLiteral, MOUNT_WILDCARDS, toRealPath } from './sandbox-paths.js'
|
|
32
32
|
export { bindableWrites, punchWalls } from './sandbox-binds.js'
|
|
33
33
|
export { isLocalExecutor, localExecutor } from './local-executor.js'
|
|
34
34
|
export { holdGroups, killTree, untilGroupsGone } from './kill-tree.js'
|
|
@@ -52,10 +52,18 @@ export function absolutize(p: string, cwd?: string): string {
|
|
|
52
52
|
return path.resolve(cwd ?? process.cwd(), p)
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* `p` is `dir` or below it, by path: a sibling sharing `dir`'s name prefix
|
|
57
|
+
* is not, and `/` already ends in the separator.
|
|
58
|
+
*/
|
|
59
|
+
export function atOrUnder(p: string, dir: string): boolean {
|
|
60
|
+
return p === dir || p.startsWith(dir.endsWith(path.sep) ? dir : dir + path.sep)
|
|
61
|
+
}
|
|
62
|
+
|
|
55
63
|
export function isUnderAny(abs: string, allow: Set<string>): boolean {
|
|
56
64
|
if (allow.has(abs)) return true
|
|
57
65
|
for (const a of allow) {
|
|
58
|
-
if (abs
|
|
66
|
+
if (atOrUnder(abs, a)) return true
|
|
59
67
|
}
|
|
60
68
|
return false
|
|
61
69
|
}
|
|
@@ -64,6 +64,7 @@ import {
|
|
|
64
64
|
} from '../util/index.js'
|
|
65
65
|
import { bindableWrites, buildCustomConfig } from './sandbox-binds.js'
|
|
66
66
|
import {
|
|
67
|
+
atOrUnder,
|
|
67
68
|
isMountableLiteral,
|
|
68
69
|
localBindingOn,
|
|
69
70
|
MOUNT_WILDCARDS,
|
|
@@ -719,16 +720,14 @@ function canonicalBaselines(
|
|
|
719
720
|
function assertWriteStaysHome(grant: string, real: string, projectDir: string): void {
|
|
720
721
|
if (grant.startsWith('~') || path.isAbsolute(grant)) return
|
|
721
722
|
const lexical = path.resolve(projectDir, grant)
|
|
722
|
-
|
|
723
|
-
p === dir || p.startsWith(dir.endsWith(path.sep) ? dir : dir + path.sep)
|
|
724
|
-
if (!inside(lexical, projectDir)) return
|
|
723
|
+
if (!atOrUnder(lexical, projectDir)) return
|
|
725
724
|
const home = toRealPath(projectDir)
|
|
726
725
|
// `toRealPath` stops at a DANGLING link and keeps its own path, which is
|
|
727
726
|
// inside; the link's target is where a write through it would land.
|
|
728
727
|
const target = throughLinks(real)
|
|
729
|
-
if (
|
|
728
|
+
if (atOrUnder(real, home) && atOrUnder(target, home)) return
|
|
730
729
|
throw new UserError(
|
|
731
|
-
`exec.sandbox.allow.write: "${grant}" resolves through a symlink to ${
|
|
730
|
+
`exec.sandbox.allow.write: "${grant}" resolves through a symlink to ${atOrUnder(real, home) ? target : real}, outside the ` +
|
|
732
731
|
`project — a write grant binds the path it names in the project, and vx does not follow a ` +
|
|
733
732
|
`link out of it. Remove the link, or grant the target by its own path.`,
|
|
734
733
|
)
|
|
@@ -1557,8 +1556,6 @@ async function runSandboxedOnce(
|
|
|
1557
1556
|
*/
|
|
1558
1557
|
function readableUnder(dir: string, granted: readonly string[]): boolean {
|
|
1559
1558
|
const target = toRealPath(dir)
|
|
1560
|
-
const under = (a: string, b: string): boolean =>
|
|
1561
|
-
a === b || a.startsWith(b.endsWith(path.sep) ? b : b + path.sep)
|
|
1562
1559
|
return granted.some((p) => {
|
|
1563
1560
|
const g = toRealPath(p)
|
|
1564
1561
|
// On Linux a grant INSIDE the cwd makes the cwd listable too: bwrap
|
|
@@ -1566,7 +1563,7 @@ function readableUnder(dir: string, granted: readonly string[]): boolean {
|
|
|
1566
1563
|
// children around the walls (`wallOff`), so every failing root task
|
|
1567
1564
|
// was told its cwd was unreadable while it listed it (B-20). Seatbelt
|
|
1568
1565
|
// grants no parent, so on macOS the cwd itself must be covered.
|
|
1569
|
-
return
|
|
1566
|
+
return atOrUnder(target, g) || (process.platform === 'linux' && atOrUnder(g, target))
|
|
1570
1567
|
})
|
|
1571
1568
|
}
|
|
1572
1569
|
|
|
@@ -1736,9 +1733,7 @@ export function wallsGlobsReach(grants: readonly string[], walls: readonly strin
|
|
|
1736
1733
|
const heads = grants
|
|
1737
1734
|
.filter((g) => !isMountableLiteral(g))
|
|
1738
1735
|
.map((g) => path.dirname(g.slice(0, g.search(MOUNT_WILDCARDS))))
|
|
1739
|
-
return walls.filter((w) =>
|
|
1740
|
-
heads.some((h) => w === h || w.startsWith(h === path.sep ? h : h + path.sep)),
|
|
1741
|
-
)
|
|
1736
|
+
return walls.filter((w) => heads.some((h) => atOrUnder(w, h)))
|
|
1742
1737
|
}
|
|
1743
1738
|
|
|
1744
1739
|
/**
|
|
@@ -1760,7 +1755,7 @@ export function darwinWallRules(
|
|
|
1760
1755
|
for (const w of walls) {
|
|
1761
1756
|
const at = `(subpath "${sbplResolvedPath(w, 'wall')}")`
|
|
1762
1757
|
const kept = literals
|
|
1763
|
-
.filter((l) => isMountableLiteral(l) && (l
|
|
1758
|
+
.filter((l) => isMountableLiteral(l) && atOrUnder(l, w))
|
|
1764
1759
|
.map((l) => `(require-not (subpath "${sbplResolvedPath(l, 'grant')}"))`)
|
|
1765
1760
|
rules.push(
|
|
1766
1761
|
kept.length === 0
|
|
@@ -1827,12 +1822,12 @@ function expandGrants(
|
|
|
1827
1822
|
for (const hit of scanOrNothing(pattern, base)) {
|
|
1828
1823
|
const abs = path.join(base, hit)
|
|
1829
1824
|
const real = toRealPath(abs)
|
|
1830
|
-
if (real
|
|
1825
|
+
if (!atOrUnder(real, home)) continue
|
|
1831
1826
|
// A hit that IS a wall, or lies inside one, was matched, not named:
|
|
1832
1827
|
// `read: ['*']` in a root project bound `.git` and `.vx`, and
|
|
1833
1828
|
// `packages/*` a nested project its key excludes (B-1). A grant that
|
|
1834
1829
|
// names a wall literally never reaches this loop and stays.
|
|
1835
|
-
if (walls.some((w) => real
|
|
1830
|
+
if (walls.some((w) => atOrUnder(real, w))) continue
|
|
1836
1831
|
out.push(abs)
|
|
1837
1832
|
hits++
|
|
1838
1833
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// no grant can avoid, minus what the task chose to ignore.
|
|
6
6
|
|
|
7
7
|
import path from 'node:path'
|
|
8
|
-
import { absolutize, isUnderAny, localBindingOn, toRealPath } from './sandbox-paths.js'
|
|
8
|
+
import { absolutize, atOrUnder, isUnderAny, localBindingOn, toRealPath } from './sandbox-paths.js'
|
|
9
9
|
import type {
|
|
10
10
|
ResolvedSandboxConfig,
|
|
11
11
|
SandboxedRunArgs,
|
|
@@ -119,7 +119,7 @@ export async function parseStraceViolations(
|
|
|
119
119
|
const abs = toRealPath(absolutize(rawPath, baselines.cwd))
|
|
120
120
|
// Only report paths under the workspace-root deny anchor — system
|
|
121
121
|
// libs / /proc / /sys / etc. probes are not interesting violations.
|
|
122
|
-
if (!denyAnchors.some((root) => abs
|
|
122
|
+
if (!denyAnchors.some((root) => atOrUnder(abs, root))) continue
|
|
123
123
|
// Skip paths the user explicitly allowed (and their descendants).
|
|
124
124
|
if (isUnderAny(abs, allowAbs)) continue
|
|
125
125
|
const key = `${syscall}|${abs}`
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
toRealPath,
|
|
19
19
|
type ExecuteRequest,
|
|
20
20
|
isMountableLiteral,
|
|
21
|
+
atOrUnder,
|
|
21
22
|
type SandboxViolation,
|
|
22
23
|
} from '../exec/index.js'
|
|
23
24
|
import type { TaskNode } from '../graph/index.js'
|
|
@@ -299,7 +300,11 @@ async function linkedDeps(
|
|
|
299
300
|
const full = path.join(dir, e.name)
|
|
300
301
|
if (e.isSymbolicLink()) {
|
|
301
302
|
const target = await realpath(full).catch(() => undefined)
|
|
302
|
-
if (
|
|
303
|
+
if (
|
|
304
|
+
target !== undefined &&
|
|
305
|
+
!dirs.some((d) => atOrUnder(target, d)) &&
|
|
306
|
+
!atOrUnder(self, target)
|
|
307
|
+
) {
|
|
303
308
|
found.push([target, full, scope + e.name])
|
|
304
309
|
}
|
|
305
310
|
} else if (scope === '' && e.isDirectory() && e.name.startsWith('@')) {
|
|
@@ -321,7 +326,7 @@ async function linkedDeps(
|
|
|
321
326
|
const granted: string[] = []
|
|
322
327
|
const withheld: WithheldLink[] = []
|
|
323
328
|
for (const [target, { link, name }] of targets) {
|
|
324
|
-
if (!
|
|
329
|
+
if (!atOrUnder(target, root) || keyedDirs.has(target)) granted.push(target)
|
|
325
330
|
else
|
|
326
331
|
withheld.push({
|
|
327
332
|
dir: target,
|
|
@@ -355,7 +360,7 @@ export function reachedWithheld(
|
|
|
355
360
|
violations: readonly SandboxViolation[],
|
|
356
361
|
): WithheldLink[] {
|
|
357
362
|
return withheld.filter((w) =>
|
|
358
|
-
violations.some((v) => v.path !== undefined &&
|
|
363
|
+
violations.some((v) => v.path !== undefined && atOrUnder(v.path, w.dir)),
|
|
359
364
|
)
|
|
360
365
|
}
|
|
361
366
|
|
|
@@ -364,15 +369,6 @@ function posixRel(from: string, p: string): string {
|
|
|
364
369
|
return path.relative(from, p).split(path.sep).join('/')
|
|
365
370
|
}
|
|
366
371
|
|
|
367
|
-
/**
|
|
368
|
-
* `p` is `dir` or below it — by path, so a sibling sharing `dir`'s name
|
|
369
|
-
* prefix is not. `/` already ends in the separator, and a link to it
|
|
370
|
-
* holds every project.
|
|
371
|
-
*/
|
|
372
|
-
function within(p: string, dir: string): boolean {
|
|
373
|
-
return p === dir || p.startsWith(dir.endsWith(path.sep) ? dir : dir + path.sep)
|
|
374
|
-
}
|
|
375
|
-
|
|
376
372
|
/**
|
|
377
373
|
* Ensure each declared write path exists on the host as either an empty
|
|
378
374
|
* file (a literal path) or a directory (a glob's static prefix, or a
|
|
@@ -451,8 +447,8 @@ function wallOff(
|
|
|
451
447
|
for (const bind of bindableWrites(config.allowWrite)) {
|
|
452
448
|
// A bind outside the workspace is the user's own path (`/tmp/x`,
|
|
453
449
|
// `~/.cache/y`), spelled there on purpose; only one inside is judged.
|
|
454
|
-
if (bind
|
|
455
|
-
const wall = walls.find((w) => w
|
|
450
|
+
if (!atOrUnder(bind, home)) continue
|
|
451
|
+
const wall = walls.find((w) => atOrUnder(w, bind))
|
|
456
452
|
if (wall === undefined) continue
|
|
457
453
|
throw new UserError(
|
|
458
454
|
`exec.sandbox.allow.write: the grant binding ${bind} would make ${wall} writable — another ` +
|
|
@@ -558,8 +554,8 @@ export function commandWriteReach(node: TaskNode, workspaceRoot: string): WriteR
|
|
|
558
554
|
let reach: 'none' | 'project' = 'none'
|
|
559
555
|
for (const grant of exec.sandbox.allow?.write ?? []) {
|
|
560
556
|
const abs = path.resolve(node.projectDir, expandHome(grantPrefix(grant)))
|
|
561
|
-
if (
|
|
562
|
-
else if (
|
|
557
|
+
if (atOrUnder(abs, node.projectDir)) reach = 'project'
|
|
558
|
+
else if (atOrUnder(abs, workspaceRoot) || atOrUnder(workspaceRoot, abs)) return 'workspace'
|
|
563
559
|
}
|
|
564
560
|
return reach
|
|
565
561
|
}
|
|
@@ -579,7 +575,7 @@ export function mayWriteFingerprint(node: TaskNode, workspaceRoot: string): bool
|
|
|
579
575
|
for (const grant of exec.sandbox.allow?.write ?? []) {
|
|
580
576
|
const abs = path.resolve(node.projectDir, expandHome(grantPrefix(grant)))
|
|
581
577
|
for (const f of WORKSPACE_FINGERPRINT_FILES) {
|
|
582
|
-
if (
|
|
578
|
+
if (atOrUnder(path.join(workspaceRoot, f), abs)) return true
|
|
583
579
|
}
|
|
584
580
|
}
|
|
585
581
|
return false
|
|
@@ -18,7 +18,13 @@ import { UserError } from '../util/index.js'
|
|
|
18
18
|
*/
|
|
19
19
|
export function keyUpstream(node: TaskNode, upstream: TaskOutcome[]): TaskOutcome[] {
|
|
20
20
|
const order = node.orderOnly
|
|
21
|
-
|
|
21
|
+
// A restore-tier hit dispatches before its dependencies settle, and the
|
|
22
|
+
// scheduler hands it their outcomes so far: a hole stays a hole, as it
|
|
23
|
+
// does with no order-only edge (reading `.node` of one threw).
|
|
24
|
+
const keyed =
|
|
25
|
+
order === undefined
|
|
26
|
+
? upstream
|
|
27
|
+
: upstream.filter((o) => o === undefined || !order.includes(o.node.id))
|
|
22
28
|
return node.excludedUpstream === undefined ? keyed : [...keyed, ...node.excludedUpstream]
|
|
23
29
|
}
|
|
24
30
|
|
|
@@ -722,6 +722,26 @@ const FOREIGN_FIELDS: ReadonlyMap<ReadonlySet<string>, Readonly<Record<string, s
|
|
|
722
722
|
},
|
|
723
723
|
],
|
|
724
724
|
[EXEC_FIELDS, { cmd: '`command`', script: '`command`' }],
|
|
725
|
+
// Turbo's turbo.json and Nx's nx.json keys, written into vx.workspace
|
|
726
|
+
// (D-38).
|
|
727
|
+
[
|
|
728
|
+
WORKSPACE_FIELDS,
|
|
729
|
+
{
|
|
730
|
+
pipeline:
|
|
731
|
+
"each package's `vx.config` `tasks` (`bunx @vzn/vx-migrate` writes them from turbo.json)",
|
|
732
|
+
tasks:
|
|
733
|
+
"each package's `vx.config` `tasks` (`bunx @vzn/vx-migrate` writes them from turbo.json)",
|
|
734
|
+
targetDefaults:
|
|
735
|
+
"each package's `vx.config` `tasks`, sharing defaults through an imported module",
|
|
736
|
+
namedInputs: 'a module each `vx.config` imports, spread into `cache.inputs.files`',
|
|
737
|
+
globalEnv: '`cache.inputs.env` on the tasks it keys',
|
|
738
|
+
globalDependencies: '`cache.inputs.workspaceFiles` on the tasks it keys',
|
|
739
|
+
remoteCache:
|
|
740
|
+
'a cache plugin in `plugins` (`turboCache()` or `nxCache()` from `@vzn/vx-migrate`)',
|
|
741
|
+
parallel: '`concurrency`',
|
|
742
|
+
cacheDirectory: '`cacheDir`',
|
|
743
|
+
},
|
|
744
|
+
],
|
|
725
745
|
])
|
|
726
746
|
|
|
727
747
|
function assertKnownFields(value: object, allowed: ReadonlySet<string>, where: string): void {
|