@vzn/vx 0.0.88 → 0.0.90

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.88",
3
+ "version": "0.0.90",
4
4
  "description": "An open, extensible monorepo task runner.",
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.88",
21
- "@vzn/vx-linux-arm64": "0.0.88",
22
- "@vzn/vx-darwin-x64": "0.0.88",
23
- "@vzn/vx-darwin-arm64": "0.0.88"
20
+ "@vzn/vx-linux-x64": "0.0.90",
21
+ "@vzn/vx-linux-arm64": "0.0.90",
22
+ "@vzn/vx-darwin-x64": "0.0.90",
23
+ "@vzn/vx-darwin-arm64": "0.0.90"
24
24
  },
25
25
  "dependencies": {
26
26
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
package/src/cli/select.ts CHANGED
@@ -353,7 +353,9 @@ export async function pickTask(
353
353
  }
354
354
  }
355
355
  if (entries.length === 0) {
356
- process.stderr.write(`vx run: no tasks declared in any project\n`)
356
+ process.stderr.write(
357
+ 'vx run: no tasks declared in any project; declare one under `tasks` in a vx.config, or run `vx init` to write them from package.json scripts\n',
358
+ )
357
359
  return null
358
360
  }
359
361
  const out = io.output ?? process.stdout
package/src/cli/why.ts CHANGED
@@ -181,7 +181,9 @@ export async function whyCmd(args: readonly string[]): Promise<number> {
181
181
 
182
182
  const why = whyDidThisRerun(db, runId, taskId)
183
183
  if (!why.found) {
184
- throw new UserError(`vx why: run ${runId} has no row for ${taskId}`)
184
+ throw new UserError(
185
+ `vx why: run ${runId} has no row for ${taskId}; \`vx last --list\` shows the recorded runs, \`vx last <runId>\` what one ran`,
186
+ )
185
187
  }
186
188
  const diff = cacheKeyDiff(db, runId, taskId)
187
189
 
@@ -356,6 +356,17 @@ function trackTaskTmpdir(dir: string): void {
356
356
 
357
357
  /** Whether SRT is up in this process — set by `initSandbox`, cleared by `resetSandbox`. */
358
358
  let srtUp = false
359
+ /**
360
+ * The run's config while one of its tasks lifts SRT's `socket(AF_UNIX)`
361
+ * block, else undefined. SRT reads the lift from its run-wide config, so
362
+ * one task's `unixSockets` or port list lifted it for every sandboxed task
363
+ * of the run, and a task that asked for neither reached the host's docker
364
+ * or ssh-agent socket (L-6). Such a run sets the lift per task, for the
365
+ * span of its wrap, which is when SRT reads it.
366
+ */
367
+ let socketRun: Parameters<SrtModule['SandboxManager']['updateConfig']>[0] | undefined
368
+ /** Wraps of a `socketRun`, one at a time: each holds SRT's config for its task. */
369
+ let wrapTurn: Promise<unknown> = Promise.resolve()
359
370
 
360
371
  /**
361
372
  * SRT listens on `<tmpdir>/srt-mux-<pid>-<seq>.sock`, seq from 0, and a
@@ -396,11 +407,11 @@ async function unlinkStaleMuxSockets(): Promise<void> {
396
407
  export async function initSandbox(opts?: {
397
408
  allowedDomains?: readonly string[]
398
409
  /**
399
- * Lift SRT's seccomp block on `socket(AF_UNIX)` for every sandboxed
400
- * task of the run (Linux; per-run like the proxy allowlist, since SRT
401
- * reads it at `initialize()` only). Armed when any task declares
402
- * `unixSockets` or a `localBinding` port list — the port bridge is a
403
- * unix socket the task's side has to create.
410
+ * Whether any task of the run lifts SRT's seccomp block on
411
+ * `socket(AF_UNIX)`: one that declares `unixSockets` or a `localBinding`
412
+ * port list (the port bridge is a unix socket the task's side has to
413
+ * create). SRT reads the lift from its run-wide config, so `wrapForTask`
414
+ * sets it for each task's own wrap (L-6).
404
415
  */
405
416
  allowAllUnixSockets?: boolean
406
417
  }): Promise<void> {
@@ -428,6 +439,7 @@ export async function initSandbox(opts?: {
428
439
  true,
429
440
  )
430
441
  srtUp = true
442
+ socketRun = opts?.allowAllUnixSockets === true ? config : undefined
431
443
  // `initialize()` returns early once SRT is up, and on Linux the
432
444
  // availability probe brought it up with an EMPTY config before the run's
433
445
  // own call — so the run's allowlist and unix-socket allowance never
@@ -463,6 +475,7 @@ export async function resetSandbox(): Promise<void> {
463
475
  const { SandboxManager } = await loadSrt()
464
476
  await SandboxManager.reset()
465
477
  srtUp = false
478
+ socketRun = undefined
466
479
  availabilityCache.clear()
467
480
  straceAvailableCache = undefined
468
481
  })()
@@ -793,7 +806,12 @@ export async function wrapSandboxedCommand(
793
806
  : { command: taggedCommand, forwards: false, traced: false }
794
807
  const inner =
795
808
  ports.length > 0 ? `${portBridgeInner(ports, tag)} ${grouped.command}` : grouped.command
796
- let wrapped = await SandboxManager.wrapWithSandbox(inner, undefined, customConfig)
809
+ let wrapped = await wrapForTask(
810
+ SandboxManager,
811
+ inner,
812
+ customConfig,
813
+ ports.length > 0 || asksUnixSockets(args.config),
814
+ )
797
815
  if (process.platform === 'darwin') {
798
816
  const rules = [
799
817
  ...macProfileRules(args.config),
@@ -918,6 +936,30 @@ function ownGroupCommand(
918
936
  /** The descriptor an in-sandbox strace writes its trace to (`ownGroupCommand`). */
919
937
  const TRACE_FD = 5
920
938
 
939
+ function asksUnixSockets(c: Pick<ResolvedSandboxConfig, 'unixSockets'>): boolean {
940
+ return c.unixSockets === true || (c.unixSockets !== undefined && c.unixSockets.length > 0)
941
+ }
942
+
943
+ /** SRT's wrap, with the socket lift this task asked for or none (L-6). */
944
+ function wrapForTask(
945
+ SandboxManager: SrtModule['SandboxManager'],
946
+ command: string,
947
+ customConfig: Parameters<SrtModule['SandboxManager']['wrapWithSandbox']>[2],
948
+ sockets: boolean,
949
+ ): Promise<string> {
950
+ const run = socketRun
951
+ if (run === undefined) return SandboxManager.wrapWithSandbox(command, undefined, customConfig)
952
+ const turn = wrapTurn.then(() => {
953
+ SandboxManager.updateConfig({
954
+ ...run,
955
+ network: { ...run.network, allowAllUnixSockets: sockets },
956
+ })
957
+ return SandboxManager.wrapWithSandbox(command, undefined, customConfig)
958
+ })
959
+ wrapTurn = turn.catch(() => undefined)
960
+ return turn
961
+ }
962
+
921
963
  /** The ports a list grants, deduped; `true` bridges nothing (the host sees no port on Linux). */
922
964
  export function bridgedPorts(c: Pick<ResolvedSandboxConfig, 'localBinding'>): number[] {
923
965
  return Array.isArray(c.localBinding) ? [...new Set(c.localBinding)] : []
@@ -140,13 +140,16 @@ export function migrateScripts(metas: readonly ProjectMeta[]): MigrationPlan {
140
140
  if (runnable.length === 0) continue
141
141
  const hasBuild = names.includes('build')
142
142
  const has = (n: string): boolean => names.includes(n)
143
+ // Lifecycle hooks are npm's, not tasks anyone runs by name, and a hook
144
+ // of a script that exists rides inside that script's command.
145
+ const isTask = (n: string): boolean => {
146
+ if (!has(n) || LIFECYCLE.test(n)) return false
147
+ const hookOf = /^(pre|post)(.+)$/.exec(n)
148
+ return hookOf === null || !has(hookOf[2]!) || LIFECYCLE.test(hookOf[2]!)
149
+ }
143
150
  const tasks: GeneratedTask[] = []
144
151
  for (const name of names) {
145
- // Lifecycle hooks are npm's, not tasks anyone runs by name.
146
- if (LIFECYCLE.test(name)) continue
147
- // A hook of a script that exists rides inside that script's command.
148
- const hookOf = /^(pre|post)(.+)$/.exec(name)
149
- if (hookOf !== null && has(hookOf[2]!) && !LIFECYCLE.test(hookOf[2]!)) continue
152
+ if (!isTask(name)) continue
150
153
 
151
154
  const todos: string[] = []
152
155
  const own = scripts[name] as string
@@ -155,7 +158,10 @@ export function migrateScripts(metas: readonly ProjectMeta[]): MigrationPlan {
155
158
  // package manager and never ride inside a task — `pack` stays alone.
156
159
  const hook = (h: string): boolean => has(h) && !LIFECYCLE.test(h)
157
160
  const hooks = [`pre${name}`, `post${name}`].filter(hook)
158
- if (delegate !== null && has(delegate) && delegate !== name && hooks.length === 0) {
161
+ // A group over a script that is no task (`setup: npm run prepare`)
162
+ // named a task nothing defines, and the run `vx init` suggested
163
+ // refused the config (D-12).
164
+ if (delegate !== null && isTask(delegate) && delegate !== name && hooks.length === 0) {
159
165
  // A group: no exec, the graph runs the target. `npm test` calling
160
166
  // `vitest` through `npm run test:unit` is two tasks, not a subprocess.
161
167
  const task: Record<string, unknown> = { dependsOn: [delegate] }