@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vzn/vx",
3
- "version": "0.0.313",
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.313",
21
- "@vzn/vx-linux-arm64": "0.0.313",
22
- "@vzn/vx-darwin-x64": "0.0.313",
23
- "@vzn/vx-darwin-arm64": "0.0.313"
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)`, or `unavailable — <why>; 3
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
@@ -17,6 +17,7 @@ export {
17
17
  probeSandbox,
18
18
  unavailableReason,
19
19
  resetSandbox,
20
+ untracedReason,
20
21
  resolveSandboxConfig,
21
22
  runSandboxed,
22
23
  releaseBridges,
@@ -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 wantsStraceDetection(): Promise<false | 'plain' | 'seccomp'> {
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
- straceAvailableCache = (await traceAttaches(form)) ? form : false
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
- * Whether strace may attach here, asked once with the flags a task's trace
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 traceAttaches(form: 'plain' | 'seccomp'): Promise<boolean> {
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 true
2057
- warnUntraced(`strace cannot trace here (${err.trim().split('\n')[0] ?? `exit ${p.exitCode}`})`)
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
- return { available: verdict.available, reason: stableSandboxReason(verdict.reason), declared }
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
- Object.fromEntries(
301
- Object.entries(scriptsOf(meta)).filter(
302
- ([n, v]) => typeof v === 'string' && !memberTasks.has(n) && !RUNS_MEMBERS.test(` ${v}`),
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