@vzn/vx 0.0.368 → 0.0.370
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.370",
|
|
4
4
|
"description": "A fast, correct task runner and build cache for JavaScript monorepos.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -21,10 +21,10 @@
|
|
|
21
21
|
"node": ">=18"
|
|
22
22
|
},
|
|
23
23
|
"optionalDependencies": {
|
|
24
|
-
"@vzn/vx-linux-x64": "0.0.
|
|
25
|
-
"@vzn/vx-linux-arm64": "0.0.
|
|
26
|
-
"@vzn/vx-darwin-x64": "0.0.
|
|
27
|
-
"@vzn/vx-darwin-arm64": "0.0.
|
|
24
|
+
"@vzn/vx-linux-x64": "0.0.370",
|
|
25
|
+
"@vzn/vx-linux-arm64": "0.0.370",
|
|
26
|
+
"@vzn/vx-darwin-x64": "0.0.370",
|
|
27
|
+
"@vzn/vx-darwin-arm64": "0.0.370"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@anthropic-ai/sandbox-runtime": "^0.0.76"
|
|
@@ -88,6 +88,7 @@ import {
|
|
|
88
88
|
srtDefaultWritePaths,
|
|
89
89
|
} from './sandbox-deny-scan.js'
|
|
90
90
|
import {
|
|
91
|
+
hiddenReadsOutside,
|
|
91
92
|
parseStraceViolations,
|
|
92
93
|
refusedConnections,
|
|
93
94
|
refusedWrites,
|
|
@@ -1820,6 +1821,16 @@ async function runSandboxedOnce(
|
|
|
1820
1821
|
})
|
|
1821
1822
|
}
|
|
1822
1823
|
|
|
1824
|
+
if (exitCode !== 0) {
|
|
1825
|
+
const hidden = hiddenReadsOutside(recorded, {
|
|
1826
|
+
within: args.reportWithin,
|
|
1827
|
+
linked: args.reportLinked,
|
|
1828
|
+
config: args.config,
|
|
1829
|
+
skip: [taskTmpRoot()],
|
|
1830
|
+
})
|
|
1831
|
+
if (hidden.length > 0) violations.push(hiddenReadsHint(hidden, args.reportWithin))
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1823
1834
|
try {
|
|
1824
1835
|
SandboxManager.cleanupAfterCommand()
|
|
1825
1836
|
} catch {
|
|
@@ -1867,6 +1878,19 @@ function outsideWritesHint(paths: readonly string[]): SandboxViolation {
|
|
|
1867
1878
|
}
|
|
1868
1879
|
}
|
|
1869
1880
|
|
|
1881
|
+
function hiddenReadsHint(paths: readonly string[], within: string): SandboxViolation {
|
|
1882
|
+
const shown = paths.slice(0, 5).join(', ')
|
|
1883
|
+
const more = paths.length > 5 ? ` and ${paths.length - 5} more` : ''
|
|
1884
|
+
return {
|
|
1885
|
+
timestamp: new Date(),
|
|
1886
|
+
hint: true,
|
|
1887
|
+
line:
|
|
1888
|
+
`vx: the sandbox hid paths outside the project that exist on this machine, which are ` +
|
|
1889
|
+
`not reported as violations: ${shown}${more}. If the task reads one, grant it, e.g. ` +
|
|
1890
|
+
`\`allow: { read: ['${path.relative(toRealPath(within), paths[0]!)}'] }\`.`,
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1870
1894
|
/**
|
|
1871
1895
|
* Is `dir` inside a path the sandbox granted for reading? bwrap binds and
|
|
1872
1896
|
* seatbelt rules are both prefix-based, so a grant covers the subtree.
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// task's owner can act on — inside the project, minus the loopback denial
|
|
5
5
|
// no grant can avoid, minus what the task chose to ignore.
|
|
6
6
|
|
|
7
|
+
import { statSync } from 'node:fs'
|
|
7
8
|
import path from 'node:path'
|
|
8
9
|
import { absolutize, atOrUnder, isUnderAny, localBindingOn, toRealPath } from './sandbox-paths.js'
|
|
9
10
|
import { bindableReads } from './sandbox-binds.js'
|
|
@@ -500,6 +501,44 @@ export function refusedWritesOutside(
|
|
|
500
501
|
return [...paths]
|
|
501
502
|
}
|
|
502
503
|
|
|
504
|
+
/**
|
|
505
|
+
* The paths outside the project a read was denied that exist on this
|
|
506
|
+
* machine, files first: the reads `reportableViolations` drops at the
|
|
507
|
+
* wall. A task that failed on one said only what its tool says for a
|
|
508
|
+
* missing file — `tsc` extending a root `tsconfig.base.json` read "File
|
|
509
|
+
* not found" — and nothing named the sandbox (2026-10-02). Never a
|
|
510
|
+
* violation — a failed task's hint (`runSandboxed`). What a process walks
|
|
511
|
+
* through to reach its cwd (the project's ancestors) and what `skip` names
|
|
512
|
+
* are left out, as is a path that does not exist on the host: that miss
|
|
513
|
+
* is the tool's own.
|
|
514
|
+
*/
|
|
515
|
+
export function hiddenReadsOutside(
|
|
516
|
+
violations: readonly SandboxViolation[],
|
|
517
|
+
opts: {
|
|
518
|
+
within: string
|
|
519
|
+
linked?: readonly string[]
|
|
520
|
+
config: ResolvedSandboxConfig
|
|
521
|
+
skip: readonly string[]
|
|
522
|
+
},
|
|
523
|
+
): string[] {
|
|
524
|
+
const reported = reportedWithin(opts.within, opts.linked ?? [])
|
|
525
|
+
const home = toRealPath(opts.within)
|
|
526
|
+
const skip = opts.skip.map(toRealPath)
|
|
527
|
+
const found = new Map<string, boolean>()
|
|
528
|
+
for (const v of filterIgnored(describe(violations), opts.config.ignore)) {
|
|
529
|
+
const p = v.path
|
|
530
|
+
if (p === undefined || v.hint === true || !v.ignorable?.includes('read') || reported(v))
|
|
531
|
+
continue
|
|
532
|
+
if (found.has(p) || atOrUnder(home, p) || skip.some((s) => atOrUnder(p, s))) continue
|
|
533
|
+
try {
|
|
534
|
+
found.set(p, statSync(p).isDirectory())
|
|
535
|
+
} catch {
|
|
536
|
+
// Not on the host either: the tool's own miss.
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return [...found].sort((a, b) => Number(a[1]) - Number(b[1])).map(([p]) => p)
|
|
540
|
+
}
|
|
541
|
+
|
|
503
542
|
/** Every record described: a seatbelt one parsed, a Linux one as produced. */
|
|
504
543
|
function describe(violations: readonly SandboxViolation[]): SandboxViolation[] {
|
|
505
544
|
return violations.map((v) =>
|
|
@@ -109,6 +109,7 @@ self.onmessage = async (e) => {
|
|
|
109
109
|
const own = (o) => new Map(Reflect.ownKeys(o).map((k) => [k, Object.getOwnPropertyDescriptor(o, k)]))
|
|
110
110
|
const before = watched?.map(([, o]) => own(o))
|
|
111
111
|
const envBefore = watched ? { ...live } : null
|
|
112
|
+
const cwdBefore = watched ? globalThis.process.cwd() : null
|
|
112
113
|
const same = (a, b) =>
|
|
113
114
|
a !== undefined && b !== undefined && a.value === b.value && a.get === b.get && a.set === b.set &&
|
|
114
115
|
a.writable === b.writable && a.enumerable === b.enumerable && a.configurable === b.configurable
|
|
@@ -122,6 +123,7 @@ self.onmessage = async (e) => {
|
|
|
122
123
|
})
|
|
123
124
|
for (const k of new Set([...Object.keys(envBefore), ...Object.keys(live)]))
|
|
124
125
|
if (envBefore[k] !== live[k]) out.push('process.env.' + k)
|
|
126
|
+
if (globalThis.process.cwd() !== cwdBefore) out.push('process.cwd (a chdir)')
|
|
125
127
|
return out
|
|
126
128
|
}
|
|
127
129
|
try {
|
|
@@ -738,6 +738,24 @@ export function migrateScripts(
|
|
|
738
738
|
return !LIFECYCLE.test(n) && (base === undefined || !(base in scripts))
|
|
739
739
|
})
|
|
740
740
|
}
|
|
741
|
+
// Why a root mapped nothing: `eslint .` beside a member's `lint` runs
|
|
742
|
+
// nothing of the workspace, and the note said it did.
|
|
743
|
+
const rootLeftOut = (): string => {
|
|
744
|
+
if (rootMeta === undefined) return 'run the workspace'
|
|
745
|
+
const scripts = scriptsOf(rootMeta)
|
|
746
|
+
const named = Object.keys(scripts).filter(
|
|
747
|
+
(n) => typeof scripts[n] === 'string' && scripts[n] !== '' && !LIFECYCLE.test(n),
|
|
748
|
+
)
|
|
749
|
+
const runs = runningMembers(
|
|
750
|
+
scripts,
|
|
751
|
+
rootMeta.dir,
|
|
752
|
+
metas.filter((m) => m !== root).map((m) => m.dir),
|
|
753
|
+
)
|
|
754
|
+
const shared = named.filter((n) => memberTasks.has(n) && !runs.has(n))
|
|
755
|
+
if (shared.length === 0) return 'run the workspace'
|
|
756
|
+
const list = `${shared.slice(0, 8).join(', ')}${shared.length > 8 ? ', …' : ''}`
|
|
757
|
+
return `${shared.length < named.length ? 'run the workspace or ' : ''}share a member's task name (${list})`
|
|
758
|
+
}
|
|
741
759
|
const rootName =
|
|
742
760
|
root?.name ??
|
|
743
761
|
(outsideRuns
|
|
@@ -768,7 +786,7 @@ export function migrateScripts(
|
|
|
768
786
|
// A nameless root's vx.config is skipped (vx names projects by their
|
|
769
787
|
// manifest's name), so the hand-written one needs a name first (vuejs/core).
|
|
770
788
|
notes.push(
|
|
771
|
-
`${rootName} (the workspace root) not mapped: its scripts
|
|
789
|
+
`${rootName} (the workspace root) not mapped: its scripts ${rootLeftOut()}; declare its own tasks in its vx.config by hand` +
|
|
772
790
|
(rootName === 'package.json' ? ', after giving its package.json a "name"' : ''),
|
|
773
791
|
)
|
|
774
792
|
}
|
|
@@ -441,6 +441,7 @@ export async function loadProjectConfigs(
|
|
|
441
441
|
// `node:stream`), ~7 ms of a two-config warm run where every load hit.
|
|
442
442
|
let builtins: BuiltinSnapshot = []
|
|
443
443
|
let env: Readonly<Record<string, string | undefined>> = {}
|
|
444
|
+
let cwd = ''
|
|
444
445
|
// More than one evaluation in flight: a change seen after one load may
|
|
445
446
|
// be another's.
|
|
446
447
|
let overlapping = false
|
|
@@ -484,7 +485,9 @@ export async function loadProjectConfigs(
|
|
|
484
485
|
: await loadDefaultExport(configPath, 'Project', bytes!)
|
|
485
486
|
// Before anything reads through them: a replaced `Array.prototype.includes`
|
|
486
487
|
// turned the JSON-data walk's own check into "a cyclic reference".
|
|
487
|
-
const changed = repeat
|
|
488
|
+
const changed = repeat
|
|
489
|
+
? []
|
|
490
|
+
: [...restoreBuiltins(builtins), ...restoreEnv(env), ...restoreCwd(cwd)]
|
|
488
491
|
// Loads overlap, so another config's change can surface after this
|
|
489
492
|
// one: named here, the refusal blamed the wrong file (D-119). The round
|
|
490
493
|
// finds the one that made it.
|
|
@@ -536,6 +539,7 @@ export async function loadProjectConfigs(
|
|
|
536
539
|
if (misses.length > 0) {
|
|
537
540
|
builtins = builtinSnapshot()
|
|
538
541
|
env = { ...process.env }
|
|
542
|
+
cwd = process.cwd()
|
|
539
543
|
}
|
|
540
544
|
overlapping = misses.length > 1
|
|
541
545
|
let next = 0
|
|
@@ -554,7 +558,10 @@ export async function loadProjectConfigs(
|
|
|
554
558
|
if (r.closure !== undefined) learnedClosures.push(r.closure)
|
|
555
559
|
}
|
|
556
560
|
}
|
|
557
|
-
const changed =
|
|
561
|
+
const changed =
|
|
562
|
+
misses.length > 0
|
|
563
|
+
? [...restoreBuiltins(builtins), ...restoreEnv(env), ...restoreCwd(cwd)]
|
|
564
|
+
: []
|
|
558
565
|
if (first?.failed instanceof ChangedInRound) {
|
|
559
566
|
for (const i of misses) {
|
|
560
567
|
const configPath = prepared[i]!.configPath
|
|
@@ -644,9 +651,13 @@ class ChangedInRound {
|
|
|
644
651
|
/** Without `configPath`, no config alone was found to make the change. */
|
|
645
652
|
function builtinsChanged(changed: readonly string[], configPath?: string): UserError {
|
|
646
653
|
const who = configPath === undefined ? 'a project config' : configPath
|
|
647
|
-
const env =
|
|
648
|
-
|
|
649
|
-
|
|
654
|
+
const env =
|
|
655
|
+
(changed.some((c) => c.startsWith('process.env.'))
|
|
656
|
+
? '; a task gets an env var through `exec.env.define` or `passThrough`'
|
|
657
|
+
: '') +
|
|
658
|
+
(changed.some((c) => c.startsWith('process.cwd'))
|
|
659
|
+
? '; a task runs in its project directory, and `cd <dir> && …` in `exec.command` moves it'
|
|
660
|
+
: '')
|
|
650
661
|
return new UserError(
|
|
651
662
|
`${who} changed ${changed.join(', ')} while it was evaluated — a config must not change the built-ins vx runs on: other configs are read through them and cache keys are made with them${env}`,
|
|
652
663
|
)
|
|
@@ -702,6 +713,20 @@ function restoreEnv(before: Readonly<Record<string, string | undefined>>): strin
|
|
|
702
713
|
return changed
|
|
703
714
|
}
|
|
704
715
|
|
|
716
|
+
/**
|
|
717
|
+
* A config's `process.chdir()` moved the whole process: every relative path
|
|
718
|
+
* vx resolves after it, its own and a plugin's, read from the config's
|
|
719
|
+
* choice (D-120). Put back, and named as the change.
|
|
720
|
+
*/
|
|
721
|
+
function restoreCwd(before: string): string[] {
|
|
722
|
+
// Through globalThis: the playground bundles this module, and its
|
|
723
|
+
// browser shim carries no free process global.
|
|
724
|
+
const proc = globalThis.process
|
|
725
|
+
if (before === '' || proc.cwd() === before) return []
|
|
726
|
+
proc.chdir(before)
|
|
727
|
+
return ['process.cwd (a chdir)']
|
|
728
|
+
}
|
|
729
|
+
|
|
705
730
|
function sameDescriptor(a: PropertyDescriptor, b: PropertyDescriptor): boolean {
|
|
706
731
|
return (
|
|
707
732
|
a.value === b.value &&
|