@vzn/vx 0.0.313 → 0.0.315
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/cli/info.ts +5 -1
- package/src/exec/index.ts +1 -0
- package/src/exec/sandbox-runtime.ts +34 -14
- package/src/orchestrator/doctor.ts +14 -5
- package/src/workspace/migrate-scripts.ts +34 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.315",
|
|
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.315",
|
|
21
|
+
"@vzn/vx-linux-arm64": "0.0.315",
|
|
22
|
+
"@vzn/vx-darwin-x64": "0.0.315",
|
|
23
|
+
"@vzn/vx-darwin-arm64": "0.0.315"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@anthropic-ai/sandbox-runtime": "^0.0.76"
|
package/src/cli/info.ts
CHANGED
|
@@ -114,13 +114,17 @@ export function renderInfo(f: InfoFacts): string {
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
/**
|
|
117
|
-
* `available (3 tasks declare exec.sandbox)`,
|
|
117
|
+
* `available (3 tasks declare exec.sandbox)`, with `, untraced — <why>, so
|
|
118
|
+
* the reads it denies go unreported` where no strace can trace, or
|
|
119
|
+
* `unavailable — <why>; 3
|
|
118
120
|
* tasks declare exec.sandbox and will fail`: a declared sandbox whose
|
|
119
121
|
* runtime cannot start fails the task, so the doctor names it before a run
|
|
120
122
|
* does.
|
|
121
123
|
*/
|
|
122
124
|
function describeSandbox(s: InfoFacts['sandbox']): string {
|
|
123
125
|
const declared = `${s.declared} task${s.declared === 1 ? '' : 's'} declare${s.declared === 1 ? 's' : ''} exec.sandbox`
|
|
126
|
+
if (s.available && s.untraced !== null)
|
|
127
|
+
return `available (${declared}), untraced — ${s.untraced}, so the reads it denies go unreported`
|
|
124
128
|
if (s.available) return `available (${declared})`
|
|
125
129
|
return `unavailable — ${s.reason}; ${declared}${s.declared > 0 ? ' and will fail' : ''}`
|
|
126
130
|
}
|
package/src/exec/index.ts
CHANGED
|
@@ -1994,11 +1994,13 @@ function writeGrantMatchedNothing(grant: string): void {
|
|
|
1994
1994
|
/**
|
|
1995
1995
|
* Memoized check: is `strace` on PATH on a Linux host, and does it know
|
|
1996
1996
|
* `--seccomp-bpf` (5.3+)? `'seccomp'` is the fast form; `'plain'` traces
|
|
1997
|
-
* every syscall through ptrace and is kept only for an old strace.
|
|
1997
|
+
* every syscall through ptrace and is kept only for an old strace. `why`
|
|
1998
|
+
* names what is missing when there is no form, for the warning and for
|
|
1999
|
+
* `vx info`.
|
|
1998
2000
|
*/
|
|
1999
|
-
let straceAvailableCache: false | 'plain' | 'seccomp' | undefined
|
|
2000
|
-
async function
|
|
2001
|
-
if (process.platform !== 'linux') return false
|
|
2001
|
+
let straceAvailableCache: { form: false | 'plain' | 'seccomp'; why: string } | undefined
|
|
2002
|
+
async function straceState(): Promise<{ form: false | 'plain' | 'seccomp'; why: string }> {
|
|
2003
|
+
if (process.platform !== 'linux') return { form: false, why: '' }
|
|
2002
2004
|
if (straceAvailableCache !== undefined) return straceAvailableCache
|
|
2003
2005
|
try {
|
|
2004
2006
|
const p = Bun.spawn([executablePath('strace'), '--version'], {
|
|
@@ -2008,26 +2010,45 @@ async function wantsStraceDetection(): Promise<false | 'plain' | 'seccomp'> {
|
|
|
2008
2010
|
const out = await new Response(p.stdout).text()
|
|
2009
2011
|
await p.exited
|
|
2010
2012
|
if (p.exitCode !== 0) {
|
|
2011
|
-
straceAvailableCache = false
|
|
2012
|
-
warnUntraced(`strace --version exited ${p.exitCode}`)
|
|
2013
|
+
straceAvailableCache = { form: false, why: `strace --version exited ${p.exitCode}` }
|
|
2013
2014
|
} else {
|
|
2014
2015
|
const m = /version (\d+)\.(\d+)/.exec(out)
|
|
2015
2016
|
const [major, minor] = m ? [Number(m[1]), Number(m[2])] : [0, 0]
|
|
2016
2017
|
const form = major > 5 || (major === 5 && minor >= 3) ? 'seccomp' : 'plain'
|
|
2017
|
-
|
|
2018
|
+
const refused = await traceRefusal(form)
|
|
2019
|
+
straceAvailableCache =
|
|
2020
|
+
refused === null
|
|
2021
|
+
? { form, why: '' }
|
|
2022
|
+
: { form: false, why: `strace cannot trace here (${refused})` }
|
|
2018
2023
|
}
|
|
2019
2024
|
} catch {
|
|
2020
2025
|
// Not on PATH. Said as the refused attach is: without it an undeclared
|
|
2021
2026
|
// read is denied but never reported, so a task that tolerates the miss
|
|
2022
2027
|
// passes and caches with no word of it (J's lead).
|
|
2023
|
-
straceAvailableCache = false
|
|
2024
|
-
warnUntraced('strace is not on PATH')
|
|
2028
|
+
straceAvailableCache = { form: false, why: 'strace is not on PATH' }
|
|
2025
2029
|
}
|
|
2026
2030
|
return straceAvailableCache
|
|
2027
2031
|
}
|
|
2028
2032
|
|
|
2033
|
+
async function wantsStraceDetection(): Promise<false | 'plain' | 'seccomp'> {
|
|
2034
|
+
const { form, why } = await straceState()
|
|
2035
|
+
if (form === false && why !== '') warnUntraced(why)
|
|
2036
|
+
return form
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
/**
|
|
2040
|
+
* Why a sandboxed task on this host runs without the report of the reads
|
|
2041
|
+
* the sandbox denied, or null when it is traced (or the host is not
|
|
2042
|
+
* Linux, where the report does not come from strace). Says nothing on
|
|
2043
|
+
* stderr: `vx info` reports it as a fact (B-51's lead).
|
|
2044
|
+
*/
|
|
2045
|
+
export async function untracedReason(): Promise<string | null> {
|
|
2046
|
+
const { why } = await straceState()
|
|
2047
|
+
return why === '' ? null : why
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2029
2050
|
/**
|
|
2030
|
-
*
|
|
2051
|
+
* Why strace may not attach here (null when it may), asked once with the flags a task's trace
|
|
2031
2052
|
* uses (`ownGroupCommand`). `--version` answers on a host that refuses
|
|
2032
2053
|
* ptrace (Yama's `ptrace_scope` 2 or 3, a container's seccomp profile),
|
|
2033
2054
|
* and there every sandboxed task failed twice, the retry included, on
|
|
@@ -2035,7 +2056,7 @@ async function wantsStraceDetection(): Promise<false | 'plain' | 'seccomp'> {
|
|
|
2035
2056
|
* run goes untraced, as without strace: bwrap still enforces, and only the
|
|
2036
2057
|
* read-violation report is lost, which is said once.
|
|
2037
2058
|
*/
|
|
2038
|
-
async function
|
|
2059
|
+
async function traceRefusal(form: 'plain' | 'seccomp'): Promise<string | null> {
|
|
2039
2060
|
const p = Bun.spawn(
|
|
2040
2061
|
[
|
|
2041
2062
|
executablePath('strace'),
|
|
@@ -2053,9 +2074,8 @@ async function traceAttaches(form: 'plain' | 'seccomp'): Promise<boolean> {
|
|
|
2053
2074
|
{ stdin: 'ignore', stdout: 'ignore', stderr: 'pipe' },
|
|
2054
2075
|
)
|
|
2055
2076
|
const err = await new Response(p.stderr).text()
|
|
2056
|
-
if ((await p.exited) === 0) return
|
|
2057
|
-
|
|
2058
|
-
return false
|
|
2077
|
+
if ((await p.exited) === 0) return null
|
|
2078
|
+
return err.trim().split('\n')[0] || `exit ${p.exitCode}`
|
|
2059
2079
|
}
|
|
2060
2080
|
let warnedNoTrace = false
|
|
2061
2081
|
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
machineParallelism,
|
|
19
19
|
} from '../util/index.js'
|
|
20
20
|
import { VERSION } from '../version.js'
|
|
21
|
-
import { probeSandbox, resetSandbox } from '../exec/index.js'
|
|
21
|
+
import { probeSandbox, resetSandbox, untracedReason } from '../exec/index.js'
|
|
22
22
|
import {
|
|
23
23
|
buildPackageGraph,
|
|
24
24
|
computeWorkspaceFingerprint,
|
|
@@ -97,9 +97,12 @@ export interface InfoFacts {
|
|
|
97
97
|
* tasks declare one. A declared sandbox whose runtime cannot start is a
|
|
98
98
|
* hard failure at run time, not a downgrade — so the doctor says so
|
|
99
99
|
* first (root inside a container, a missing bubblewrap, a nested
|
|
100
|
-
* seatbelt), with the probe's own reason.
|
|
100
|
+
* seatbelt), with the probe's own reason. `untraced` is why an available
|
|
101
|
+
* Linux sandbox cannot report the reads it denied (no `strace`, or one
|
|
102
|
+
* that may not attach), else null: the sandbox still enforces, and a task
|
|
103
|
+
* that tolerates a denied read passes with no word of it.
|
|
101
104
|
*/
|
|
102
|
-
sandbox: { available: boolean; reason: string; declared: number }
|
|
105
|
+
sandbox: { available: boolean; reason: string; declared: number; untraced: string | null }
|
|
103
106
|
}
|
|
104
107
|
|
|
105
108
|
export interface CollectInfoOptions {
|
|
@@ -231,10 +234,16 @@ export async function collectInfo(cwd: string, opts: CollectInfoOptions = {}): P
|
|
|
231
234
|
async function sandboxFact(declared: number): Promise<InfoFacts['sandbox']> {
|
|
232
235
|
try {
|
|
233
236
|
const verdict = await probeSandbox()
|
|
234
|
-
|
|
237
|
+
const untraced = verdict.available ? await untracedReason() : null
|
|
238
|
+
return {
|
|
239
|
+
available: verdict.available,
|
|
240
|
+
reason: stableSandboxReason(verdict.reason),
|
|
241
|
+
declared,
|
|
242
|
+
untraced,
|
|
243
|
+
}
|
|
235
244
|
} catch (err) {
|
|
236
245
|
const message = err instanceof Error ? err.message : String(err)
|
|
237
|
-
return { available: false, reason: stableSandboxReason(message), declared }
|
|
246
|
+
return { available: false, reason: stableSandboxReason(message), declared, untraced: null }
|
|
238
247
|
} finally {
|
|
239
248
|
await resetSandbox()
|
|
240
249
|
}
|
|
@@ -254,6 +254,33 @@ function upstreamBuildOnWorker(tasks: GeneratedTask[]): void {
|
|
|
254
254
|
const RUNS_MEMBERS =
|
|
255
255
|
/(^|[\s;&|(])(turbo|nx|lerna|ultra|wireit|nps|moon|rush|vx)(\s|$)|\s(-r|--recursive|--filter|-F|--workspaces|-ws|--workspace|-w|-C|--dir|--cwd|--prefix|--if-present|--parallel|--stream)(\s|=|$)|\bworkspaces?\s+(foreach|run)\b|\bvp\s+run\s|\bcd\s/
|
|
256
256
|
|
|
257
|
+
/** A package manager running a script by name: `pnpm build`, `npm run x`, `bun run x`. */
|
|
258
|
+
const RUNS_SCRIPT =
|
|
259
|
+
/(?:^|[\s;&|(])(?:pnpm|npm|yarn|bun)\s+(?:run(?:-script)?\s+)?([^\s;&|()'"-][^\s;&|()'"]*)/g
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* The scripts that run the members, directly or through another of these
|
|
263
|
+
* scripts: vite's `ci-docs` (`pnpm build && pnpm docs-build`) runs each
|
|
264
|
+
* member's build through the root's own `build` (`pnpm -r … run build`).
|
|
265
|
+
*/
|
|
266
|
+
function runningMembers(scripts: Readonly<Record<string, unknown>>): Set<string> {
|
|
267
|
+
const text = Object.entries(scripts).filter(
|
|
268
|
+
(e): e is [string, string] => typeof e[1] === 'string',
|
|
269
|
+
)
|
|
270
|
+
const out = new Set(text.filter(([, v]) => RUNS_MEMBERS.test(` ${v}`)).map(([n]) => n))
|
|
271
|
+
for (let grew = true; grew;) {
|
|
272
|
+
grew = false
|
|
273
|
+
for (const [n, v] of text) {
|
|
274
|
+
if (out.has(n)) continue
|
|
275
|
+
if ([...v.matchAll(RUNS_SCRIPT)].some((m) => out.has(m[1]!))) {
|
|
276
|
+
out.add(n)
|
|
277
|
+
grew = true
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return out
|
|
282
|
+
}
|
|
283
|
+
|
|
257
284
|
/**
|
|
258
285
|
* `outside`: the root manifest when the root is no member (pnpm's and
|
|
259
286
|
* Yarn's default), whose own scripts (`lint: eslint .`) went unmapped
|
|
@@ -296,12 +323,15 @@ export function migrateScripts(
|
|
|
296
323
|
.filter(([, v]) => typeof v === 'string' && v !== '')
|
|
297
324
|
.map(([n]) => n),
|
|
298
325
|
)
|
|
299
|
-
const rootScripts = (meta: ProjectMeta): Record<string, unknown> =>
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
326
|
+
const rootScripts = (meta: ProjectMeta): Record<string, unknown> => {
|
|
327
|
+
const scripts = scriptsOf(meta)
|
|
328
|
+
const runs = runningMembers(scripts)
|
|
329
|
+
return Object.fromEntries(
|
|
330
|
+
Object.entries(scripts).filter(
|
|
331
|
+
([n, v]) => typeof v === 'string' && !memberTasks.has(n) && !runs.has(n),
|
|
303
332
|
),
|
|
304
333
|
)
|
|
334
|
+
}
|
|
305
335
|
const rootMapped =
|
|
306
336
|
rootMeta === undefined
|
|
307
337
|
? 0
|