@vzn/vx 0.0.198 → 0.0.200

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.198",
3
+ "version": "0.0.200",
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.198",
21
- "@vzn/vx-linux-arm64": "0.0.198",
22
- "@vzn/vx-darwin-x64": "0.0.198",
23
- "@vzn/vx-darwin-arm64": "0.0.198"
20
+ "@vzn/vx-linux-x64": "0.0.200",
21
+ "@vzn/vx-linux-arm64": "0.0.200",
22
+ "@vzn/vx-darwin-x64": "0.0.200",
23
+ "@vzn/vx-darwin-arm64": "0.0.200"
24
24
  },
25
25
  "dependencies": {
26
26
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
package/src/cli/run.ts CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  import type { ContinueMode } from '../graph/index.js'
17
17
  import { type CachePolicy, FULL_CACHE_POLICY, parseCachePolicy } from '../cache/index.js'
18
18
  import { findCwdProject, pickTask, resolveFilters } from './select.js'
19
- import { nxTargetHint } from './task-verb.js'
19
+ import { nxTargetHint, taskNamesHere } from './task-verb.js'
20
20
  import { MAX_TIMEOUT_MS, isUserError, parseDecimalInt, machineParallelism } from '../util/index.js'
21
21
  import { formatGraphDot, formatPlanJson, formatPlanText } from './plan-format.js'
22
22
 
@@ -555,6 +555,9 @@ export async function resolveRunOptions(
555
555
  return opts
556
556
  }
557
557
 
558
+ /** How many task names a run with no task names before `and N more`. */
559
+ const TASKS_SHOWN = 12
560
+
558
561
  export async function runCmd(args: readonly string[]): Promise<number> {
559
562
  const parsed = parseRunArgs(args)
560
563
  if (parsed.error) {
@@ -570,8 +573,15 @@ export async function runCmd(args: readonly string[]): Promise<number> {
570
573
  // explicit anchored positional.
571
574
  if (tasks.length === 0) {
572
575
  if (!process.stdin.isTTY) {
576
+ const names = await taskNamesHere(cwd)
577
+ const shown = (names ?? []).slice(0, TASKS_SHOWN)
578
+ const more = (names?.length ?? 0) - shown.length
579
+ const tasksHere =
580
+ shown.length === 0
581
+ ? 'vx run <task>, e.g. vx run build'
582
+ : `tasks here: ${shown.join(', ')}${more > 0 ? ` and ${more} more` : ''}`
573
583
  process.stderr.write(
574
- `vx run: missing task name (stdin is not a TTY, so no picker; vx run <task>, e.g. vx run build)${seeHelp('run')}\n`,
584
+ `vx run: missing task name (stdin is not a TTY, so no picker; ${tasksHere})${seeHelp('run')}\n`,
575
585
  )
576
586
  return 1
577
587
  }
@@ -43,6 +43,22 @@ export async function nxTargetHint(tasks: readonly string[], cwd: string): Promi
43
43
  return `\`${tasks.join(' ')}\` is Nx's project:target: vx run ${specs.join(' ')}`
44
44
  }
45
45
 
46
+ /**
47
+ * The task names `vx run <task>` can take here: the cwd project's, else
48
+ * every project's, sorted; null when no workspace loads. What a run with
49
+ * no task and no terminal for the picker names instead of a guess.
50
+ */
51
+ export async function taskNamesHere(cwd: string): Promise<string[] | null> {
52
+ const projects = await workspaceProjects(cwd)
53
+ if (projects === null) return null
54
+ const own = await findCwdProject(cwd)
55
+ const names = new Set<string>()
56
+ for (const p of projects.values())
57
+ if (own === null || p.name === own)
58
+ for (const t of Object.keys(p.config.tasks ?? {})) names.add(t)
59
+ return [...names].sort()
60
+ }
61
+
46
62
  async function workspaceProjects(
47
63
  cwd: string,
48
64
  ): Promise<Awaited<ReturnType<typeof loadCliProjects>> | null> {
@@ -52,7 +52,7 @@ export function srtDefaultWritePaths(): string[] {
52
52
  const lower = (s: string): string => s.toLowerCase()
53
53
 
54
54
  /** Whether `rel` (from the scan root, `/`-separated) is one of rg's hits for SRT's patterns. */
55
- function isHit(rel: string): boolean {
55
+ function isHit(rel: string, gitConfig: boolean): boolean {
56
56
  const segs = rel.split('/').map(lower)
57
57
  if (DANGEROUS_FILES.has(segs[segs.length - 1]!)) return true
58
58
  const inside = (dir: string): boolean => {
@@ -63,9 +63,13 @@ function isHit(rel: string): boolean {
63
63
  return false
64
64
  }
65
65
  if (DANGEROUS_DIRS.some(inside) || inside('.git/hooks')) return true
66
- // `**/.git/config`, and only while `allowGitConfig` is off: vx never
67
- // turns it on in the run's config, which is the one SRT's scan reads.
68
- return segs.length >= 2 && segs[segs.length - 2] === '.git' && segs[segs.length - 1] === 'config'
66
+ // `**/.git/config`, unless the task grants `gitConfig` (B-41).
67
+ return (
68
+ !gitConfig &&
69
+ segs.length >= 2 &&
70
+ segs[segs.length - 2] === '.git' &&
71
+ segs[segs.length - 1] === 'config'
72
+ )
69
73
  }
70
74
 
71
75
  /** The deny path SRT derives from a hit (`linuxGetMandatoryDenyPaths`), or none. */
@@ -93,7 +97,11 @@ function denyOf(cwd: string, rel: string): string | undefined {
93
97
  * path with a glob character is dropped from `denyWrite` on Linux, and
94
98
  * the write it guards must not be left open.
95
99
  */
96
- export function scopedMandatoryDenies(cwd: string, writePaths: readonly string[]): string[] {
100
+ export function scopedMandatoryDenies(
101
+ cwd: string,
102
+ writePaths: readonly string[],
103
+ gitConfig = false,
104
+ ): string[] {
97
105
  const roots = new Set<string>()
98
106
  for (const raw of writePaths) {
99
107
  const w = raw.replace(/\/+$/, '') || '/'
@@ -112,7 +120,7 @@ export function scopedMandatoryDenies(cwd: string, writePaths: readonly string[]
112
120
  const childRel = rel === '' ? e.name : `${rel}/${e.name}`
113
121
  if (e.isDirectory()) {
114
122
  if (depth + 1 < SEARCH_DEPTH) visit(path.join(abs, e.name), childRel, depth + 1)
115
- } else if (isHit(childRel)) {
123
+ } else if (isHit(childRel, gitConfig)) {
116
124
  const d = denyOf(cwd, childRel)
117
125
  if (d !== undefined) denies.add(d)
118
126
  }
@@ -130,7 +138,7 @@ export function scopedMandatoryDenies(cwd: string, writePaths: readonly string[]
130
138
  }
131
139
  if (dir) {
132
140
  if (depth < SEARCH_DEPTH) visit(root, rel, depth)
133
- } else if (depth > 0 && isHit(rel)) {
141
+ } else if (depth > 0 && isHit(rel, gitConfig)) {
134
142
  const d = denyOf(cwd, rel)
135
143
  if (d !== undefined) denies.add(d)
136
144
  }
@@ -412,14 +412,16 @@ function trackTaskTmpdir(dir: string): void {
412
412
  let srtUp = false
413
413
  /**
414
414
  * The run's config while one of its tasks lifts SRT's `socket(AF_UNIX)`
415
- * block, else undefined. SRT reads the lift from its run-wide config, so
416
- * one task's `unixSockets` or port list lifted it for every sandboxed task
417
- * of the run, and a task that asked for neither reached the host's docker
418
- * or ssh-agent socket (L-6). Such a run sets the lift per task, for the
419
- * span of its wrap, which is when SRT reads it.
415
+ * block or grants `gitConfig`, else undefined. SRT reads both from its
416
+ * run-wide config, so one task's `unixSockets` or port list lifted the
417
+ * block for every sandboxed task of the run, and a task that asked for
418
+ * neither reached the host's docker or ssh-agent socket (L-6); the
419
+ * per-task `allowGitConfig` vx passed was never read at all (B-41). Such a
420
+ * run sets both per task, for the span of its wrap, which is when SRT
421
+ * reads them.
420
422
  */
421
- let socketRun: Parameters<SrtModule['SandboxManager']['updateConfig']>[0] | undefined
422
- /** Wraps of a `socketRun`, one at a time: each holds SRT's config for its task. */
423
+ let perTaskRun: Parameters<SrtModule['SandboxManager']['updateConfig']>[0] | undefined
424
+ /** Wraps of a `perTaskRun`, one at a time: each holds SRT's config for its task. */
423
425
  let wrapTurn: Promise<unknown> = Promise.resolve()
424
426
 
425
427
  /**
@@ -540,6 +542,8 @@ export async function initSandbox(opts?: {
540
542
  * sets it for each task's own wrap (L-6).
541
543
  */
542
544
  allowAllUnixSockets?: boolean
545
+ /** Whether any task of the run grants `gitConfig`: SRT reads it run-wide, so it is set per wrap (B-41). */
546
+ gitConfig?: boolean
543
547
  }): Promise<void> {
544
548
  // A reset a server's exit started unawaited: a watch cycle stops its
545
549
  // server and starts its run at once, and an init under that reset
@@ -571,7 +575,7 @@ export async function initSandbox(opts?: {
571
575
  true,
572
576
  )
573
577
  srtUp = true
574
- socketRun = opts?.allowAllUnixSockets === true ? config : undefined
578
+ perTaskRun = opts?.allowAllUnixSockets === true || opts?.gitConfig === true ? config : undefined
575
579
  // `initialize()` returns early once SRT is up, and on Linux the
576
580
  // availability probe brought it up with an EMPTY config before the run's
577
581
  // own call — so the run's allowlist and unix-socket allowance never
@@ -607,7 +611,7 @@ export async function resetSandbox(): Promise<void> {
607
611
  const { SandboxManager } = await loadSrt()
608
612
  await SandboxManager.reset()
609
613
  srtUp = false
610
- socketRun = undefined
614
+ perTaskRun = undefined
611
615
  availabilityCache.clear()
612
616
  straceAvailableCache = undefined
613
617
  })()
@@ -942,10 +946,11 @@ export async function wrapSandboxedCommand(
942
946
  customConfig!.filesystem!.allowWrite!.push(toRealPath(tmp))
943
947
  if (scopedDenyScan) {
944
948
  customConfig!.filesystem!.denyWrite!.push(
945
- ...scopedMandatoryDenies(process.cwd(), [
946
- ...srtDefaultWritePaths(),
947
- ...customConfig!.filesystem!.allowWrite!,
948
- ]),
949
+ ...scopedMandatoryDenies(
950
+ process.cwd(),
951
+ [...srtDefaultWritePaths(), ...customConfig!.filesystem!.allowWrite!],
952
+ args.config.gitConfig === true,
953
+ ),
949
954
  )
950
955
  }
951
956
  // Seatbelt re-allows a read inside a denied region only by name; on Linux
@@ -967,6 +972,7 @@ export async function wrapSandboxedCommand(
967
972
  inner,
968
973
  customConfig,
969
974
  ports.length > 0 || asksUnixSockets(args.config),
975
+ args.config.gitConfig === true,
970
976
  )
971
977
  if (process.platform === 'darwin') {
972
978
  const rules = [
@@ -1099,19 +1105,21 @@ function asksUnixSockets(c: Pick<ResolvedSandboxConfig, 'unixSockets'>): boolean
1099
1105
  return c.unixSockets === true || (c.unixSockets !== undefined && c.unixSockets.length > 0)
1100
1106
  }
1101
1107
 
1102
- /** SRT's wrap, with the socket lift this task asked for or none (L-6). */
1108
+ /** SRT's wrap, with the socket lift and the git-config grant this task asked for, or none (L-6, B-41). */
1103
1109
  function wrapForTask(
1104
1110
  SandboxManager: SrtModule['SandboxManager'],
1105
1111
  command: string,
1106
1112
  customConfig: Parameters<SrtModule['SandboxManager']['wrapWithSandbox']>[2],
1107
1113
  sockets: boolean,
1114
+ gitConfig: boolean,
1108
1115
  ): Promise<string> {
1109
- const run = socketRun
1116
+ const run = perTaskRun
1110
1117
  if (run === undefined) return SandboxManager.wrapWithSandbox(command, undefined, customConfig)
1111
1118
  const turn = wrapTurn.then(() => {
1112
1119
  SandboxManager.updateConfig({
1113
1120
  ...run,
1114
1121
  network: { ...run.network, allowAllUnixSockets: sockets },
1122
+ filesystem: { ...run.filesystem, allowGitConfig: gitConfig },
1115
1123
  })
1116
1124
  return SandboxManager.wrapWithSandbox(command, undefined, customConfig)
1117
1125
  })
@@ -64,6 +64,8 @@ export interface SandboxRunUnion {
64
64
  deniedDomains: string[]
65
65
  /** Whether SRT's all-or-nothing `socket(AF_UNIX)` filter is lifted for the run. */
66
66
  unixSockets: boolean
67
+ /** Whether any task grants `gitConfig`, which SRT reads run-wide and vx sets per wrap (B-41). */
68
+ gitConfig: boolean
67
69
  /** Whether EVERY sandboxed task accepts the weaker nested profile. */
68
70
  weakerNested: boolean
69
71
  }
@@ -75,6 +77,7 @@ export function sandboxRunUnion(nodes: Iterable<TaskNode>): SandboxRunUnion | nu
75
77
  const domains = new Set<string>()
76
78
  const denied = new Set<string>()
77
79
  let unixSockets = false
80
+ let gitConfig = false
78
81
  for (const n of sandboxed) {
79
82
  const allow = n.config.exec?.sandbox?.allow
80
83
  const net = allow?.network
@@ -87,8 +90,9 @@ export function sandboxRunUnion(nodes: Iterable<TaskNode>): SandboxRunUnion | nu
87
90
  if (sockets === true || (Array.isArray(sockets) && sockets.length > 0)) unixSockets = true
88
91
  const lb = allow?.localBinding
89
92
  if (Array.isArray(lb) && lb.length > 0 && process.platform === 'linux') unixSockets = true
93
+ if (allow?.gitConfig === true) gitConfig = true
90
94
  }
91
- return { domains: [...domains], deniedDomains: [...denied], unixSockets, weakerNested }
95
+ return { domains: [...domains], deniedDomains: [...denied], unixSockets, gitConfig, weakerNested }
92
96
  }
93
97
 
94
98
  /**
@@ -115,7 +119,7 @@ export function sandboxRunUnion(nodes: Iterable<TaskNode>): SandboxRunUnion | nu
115
119
  export function prepareSandbox(nodes: Iterable<TaskNode>): SandboxArmer | null {
116
120
  const union = sandboxRunUnion(nodes)
117
121
  if (union === null) return null
118
- const { domains, deniedDomains, unixSockets, weakerNested } = union
122
+ const { domains, deniedDomains, unixSockets, gitConfig, weakerNested } = union
119
123
  let pending: Promise<void> | undefined
120
124
  let armed = false
121
125
  return {
@@ -131,6 +135,7 @@ export function prepareSandbox(nodes: Iterable<TaskNode>): SandboxArmer | null {
131
135
  allowedDomains: domains,
132
136
  deniedDomains,
133
137
  ...(unixSockets ? { allowAllUnixSockets: true } : {}),
138
+ ...(gitConfig ? { gitConfig: true } : {}),
134
139
  })
135
140
  } catch (err) {
136
141
  // A throw from the runtime itself (its bridge needs socat, which