@vzn/vx 0.0.254 → 0.0.256

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.254",
3
+ "version": "0.0.256",
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.254",
21
- "@vzn/vx-linux-arm64": "0.0.254",
22
- "@vzn/vx-darwin-x64": "0.0.254",
23
- "@vzn/vx-darwin-arm64": "0.0.254"
20
+ "@vzn/vx-linux-x64": "0.0.256",
21
+ "@vzn/vx-linux-arm64": "0.0.256",
22
+ "@vzn/vx-darwin-x64": "0.0.256",
23
+ "@vzn/vx-darwin-arm64": "0.0.256"
24
24
  },
25
25
  "dependencies": {
26
26
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
package/src/cli/init.ts CHANGED
@@ -216,11 +216,17 @@ function remoteCacheSignal(root: string, runner: 'turbo' | 'nx'): string | undef
216
216
  } catch {
217
217
  continue
218
218
  }
219
- if (typeof rc === 'object' && rc !== null && (rc as { enabled?: unknown }).enabled !== false)
219
+ if (typeof rc === 'object' && rc !== null) {
220
+ // Turned off in turbo.json is off, whatever CI sets.
221
+ if ((rc as { enabled?: unknown }).enabled === false) return undefined
220
222
  return `${f} names a remoteCache`
223
+ }
221
224
  }
222
225
  }
223
226
  const variable = REMOTE_CACHE_ENV[runner]
227
+ // A line that sets it (`TURBO_TOKEN: …`, `TURBO_TOKEN=…`), not one that
228
+ // names it: a comment or a doc line counted as "sets" (J's lead).
229
+ const sets = new RegExp(`^(?!\\s*#).*\\b${variable}\\s*[:=]`, 'm')
224
230
  const workflows = new Bun.Glob('.github/workflows/*.{yml,yaml}')
225
231
  const files = [...workflows.scanSync({ cwd: root, dot: true })].sort().concat(CI_FILES)
226
232
  for (const f of files) {
@@ -230,7 +236,7 @@ function remoteCacheSignal(root: string, runner: 'turbo' | 'nx'): string | undef
230
236
  } catch {
231
237
  continue
232
238
  }
233
- if (text.includes(variable)) return `${f} sets ${variable}`
239
+ if (sets.test(text)) return `${f} sets ${variable}`
234
240
  }
235
241
  return undefined
236
242
  }
@@ -20,8 +20,8 @@ export const PLUGIN_TEMPLATES: Readonly<Record<string, { plugin: string; test: s
20
20
  },
21
21
  executor: {
22
22
  plugin:
23
- "// `executor`: where a task's command runs. This one runs it here, in a\n// shell of its own, and stops the shell's whole process group when core\n// aborts the request (a stop, or `exec.timeout`): core cannot reach a\n// process an executor spawned.\nimport { definePlugin, type TaskExecutor, type VxPlugin } from '@vzn/vx'\n\nexport function shellExecutor(): VxPlugin {\n const executor: TaskExecutor = {\n name: 'shell',\n async execute(req) {\n const started = Date.now()\n const child = Bun.spawn(['sh', '-c', [req.command, ...req.forwardArgs].join(' ')], {\n cwd: req.cwd,\n env: req.env,\n stdout: 'pipe',\n stderr: 'pipe',\n detached: true,\n })\n const stop = (): void => {\n try {\n process.kill(-child.pid, 'SIGTERM')\n } catch {\n // already gone\n }\n }\n req.signal?.addEventListener('abort', stop, { once: true })\n const drain = async (stream: ReadableStream<Uint8Array>, on: (s: string) => void) => {\n const decoder = new TextDecoder()\n let all = ''\n for await (const chunk of stream) {\n const text = decoder.decode(chunk, { stream: true })\n all += text\n on(text)\n }\n return all\n }\n const [stdout, stderr, exitCode] = await Promise.all([\n drain(child.stdout, req.onStdout),\n drain(child.stderr, req.onStderr),\n child.exited,\n ])\n req.signal?.removeEventListener('abort', stop)\n return {\n exitCode,\n durationMs: Date.now() - started,\n stdout,\n stderr,\n violations: [],\n where: 'shell',\n }\n },\n }\n return definePlugin(import.meta, { executor: () => executor })\n}\n",
24
- test: "// A test for the example `executor` plugin beside it: a throwaway workspace\n// declares the plugin and vx's own run() drives it. `bun test` runs it.\nimport { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'\nimport os from 'node:os'\nimport path from 'node:path'\nimport { afterEach, beforeEach, expect, it } from 'bun:test'\nimport { run, type Logger } from '@vzn/vx'\n\nlet root: string\nbeforeEach(async () => {\n root = await mkdtemp(path.join(os.tmpdir(), 'vx-plugin-executor-'))\n await writeFile(path.join(root, 'package.json'), JSON.stringify({ name: 'ws', private: true }))\n await writeFile(path.join(root, 'pnpm-workspace.yaml'), 'packages:\\n - \"packages/*\"\\n')\n Bun.spawnSync({ cmd: ['git', 'init', '-q'], cwd: root })\n})\nafterEach(async () => {\n await rm(root, { recursive: true, force: true })\n})\n\n/** A package `name` whose `vx.config.mjs` is `config`. */\nasync function pkg(\n name: string,\n config: string,\n scripts?: Record<string, string>,\n): Promise<string> {\n const dir = path.join(root, 'packages', name)\n await mkdir(dir, { recursive: true })\n await writeFile(\n path.join(dir, 'package.json'),\n JSON.stringify({ name, version: '1.0.0', ...(scripts ? { scripts } : {}) }),\n )\n await writeFile(path.join(dir, 'vx.config.mjs'), config)\n return dir\n}\n\n/** A `vx.workspace.mjs` declaring `call`, e.g. `shellExecutor()`, from the plugin file beside this test. */\nasync function workspace(call: string): Promise<void> {\n const from = JSON.stringify(path.join(import.meta.dir, 'executor.ts'))\n await writeFile(\n path.join(root, 'vx.workspace.mjs'),\n `import { shellExecutor } from ${from}\\nexport default { plugins: [${call}] }\\n`,\n )\n}\n\nfunction log(order: string[] = []): Logger {\n return {\n status() {},\n taskStart(node) {\n order.push(`start ${node.id}`)\n },\n taskStdout() {},\n taskStderr() {},\n taskComplete(node) {\n order.push(`end ${node.id}`)\n },\n }\n}\n\nit('runs the command and says where; stops it on exec.timeout', async () => {\n const dir = await pkg(\n 'a',\n `export default { tasks: {\n hello: { exec: { command: 'echo hi > out.txt' } },\n slow: { exec: { command: 'exec sleep 30', timeout: 300 } },\n } }`,\n )\n await workspace('shellExecutor()')\n const ok = await run({ cwd: root, tasks: ['hello'], log: log(), handleSignals: false })\n expect(ok.outcomes.map((o) => [o.node.id, o.status, o.where])).toEqual([\n ['a#hello', 'success', 'shell'],\n ])\n expect(await Bun.file(path.join(dir, 'out.txt')).text()).toBe('hi\\n')\n const started = Date.now()\n const slow = await run({ cwd: root, tasks: ['slow'], log: log(), handleSignals: false })\n expect(slow.outcomes.map((o) => [o.node.id, o.status, o.timedOut === true])).toEqual([\n ['a#slow', 'failed', true],\n ])\n expect(Date.now() - started).toBeLessThan(10_000)\n}, 20_000)\n",
23
+ "// `executor`: where a task's command runs. This one runs it here, in a\n// shell of its own, and stops the shell's whole process group when core\n// aborts the request (a stop, or `exec.timeout`): core cannot reach a\n// process an executor spawned.\nimport { definePlugin, type TaskExecutor, type VxPlugin } from '@vzn/vx'\n\nexport function shellExecutor(): VxPlugin {\n const executor: TaskExecutor = {\n name: 'shell',\n async execute(req) {\n const started = Date.now()\n // vx's own `sh`: a bare name is looked up on `req.env`'s PATH, which\n // leads with the project's node_modules/.bin, where a dependency's\n // `sh` would run every command.\n const sh = Bun.which('sh') ?? '/bin/sh'\n const child = Bun.spawn([sh, '-c', [req.command, ...req.forwardArgs].join(' ')], {\n cwd: req.cwd,\n env: req.env,\n stdout: 'pipe',\n stderr: 'pipe',\n detached: true,\n })\n const stop = (): void => {\n try {\n process.kill(-child.pid, 'SIGTERM')\n } catch {\n // already gone\n }\n }\n req.signal?.addEventListener('abort', stop, { once: true })\n const drain = async (stream: ReadableStream<Uint8Array>, on: (s: string) => void) => {\n const decoder = new TextDecoder()\n let all = ''\n for await (const chunk of stream) {\n const text = decoder.decode(chunk, { stream: true })\n all += text\n on(text)\n }\n return all\n }\n const [stdout, stderr, exitCode] = await Promise.all([\n drain(child.stdout, req.onStdout),\n drain(child.stderr, req.onStderr),\n child.exited,\n ])\n req.signal?.removeEventListener('abort', stop)\n return {\n exitCode,\n durationMs: Date.now() - started,\n stdout,\n stderr,\n violations: [],\n where: 'shell',\n }\n },\n }\n return definePlugin(import.meta, { executor: () => executor })\n}\n",
24
+ test: "// A test for the example `executor` plugin beside it: a throwaway workspace\n// declares the plugin and vx's own run() drives it. `bun test` runs it.\nimport { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'\nimport os from 'node:os'\nimport path from 'node:path'\nimport { afterEach, beforeEach, expect, it } from 'bun:test'\nimport { run, type Logger } from '@vzn/vx'\n\nlet root: string\nbeforeEach(async () => {\n root = await mkdtemp(path.join(os.tmpdir(), 'vx-plugin-executor-'))\n await writeFile(path.join(root, 'package.json'), JSON.stringify({ name: 'ws', private: true }))\n await writeFile(path.join(root, 'pnpm-workspace.yaml'), 'packages:\\n - \"packages/*\"\\n')\n Bun.spawnSync({ cmd: ['git', 'init', '-q'], cwd: root })\n})\nafterEach(async () => {\n await rm(root, { recursive: true, force: true })\n})\n\n/** A package `name` whose `vx.config.mjs` is `config`. */\nasync function pkg(\n name: string,\n config: string,\n scripts?: Record<string, string>,\n): Promise<string> {\n const dir = path.join(root, 'packages', name)\n await mkdir(dir, { recursive: true })\n await writeFile(\n path.join(dir, 'package.json'),\n JSON.stringify({ name, version: '1.0.0', ...(scripts ? { scripts } : {}) }),\n )\n await writeFile(path.join(dir, 'vx.config.mjs'), config)\n return dir\n}\n\n/** A `vx.workspace.mjs` declaring `call`, e.g. `shellExecutor()`, from the plugin file beside this test. */\nasync function workspace(call: string): Promise<void> {\n const from = JSON.stringify(path.join(import.meta.dir, 'executor.ts'))\n await writeFile(\n path.join(root, 'vx.workspace.mjs'),\n `import { shellExecutor } from ${from}\\nexport default { plugins: [${call}] }\\n`,\n )\n}\n\nfunction log(order: string[] = []): Logger {\n return {\n status() {},\n taskStart(node) {\n order.push(`start ${node.id}`)\n },\n taskStdout() {},\n taskStderr() {},\n taskComplete(node) {\n order.push(`end ${node.id}`)\n },\n }\n}\n\nit('runs the command and says where; stops it on exec.timeout', async () => {\n const dir = await pkg(\n 'a',\n `export default { tasks: {\n hello: { exec: { command: 'echo hi > out.txt' } },\n slow: { exec: { command: 'exec sleep 30', timeout: 300 } },\n } }`,\n )\n await workspace('shellExecutor()')\n const ok = await run({ cwd: root, tasks: ['hello'], log: log(), handleSignals: false })\n expect(ok.outcomes.map((o) => [o.node.id, o.status, o.where])).toEqual([\n ['a#hello', 'success', 'shell'],\n ])\n expect(await Bun.file(path.join(dir, 'out.txt')).text()).toBe('hi\\n')\n const started = Date.now()\n const slow = await run({ cwd: root, tasks: ['slow'], log: log(), handleSignals: false })\n expect(slow.outcomes.map((o) => [o.node.id, o.status, o.timedOut === true])).toEqual([\n ['a#slow', 'failed', true],\n ])\n expect(Date.now() - started).toBeLessThan(10_000)\n}, 20_000)\n\nit(\"runs the command through this machine's sh, not a project's node_modules/.bin sh\", async () => {\n const dir = await pkg(\n 'a',\n `export default { tasks: { hello: { exec: { command: 'echo real' } } } }`,\n )\n await mkdir(path.join(dir, 'node_modules', '.bin'), { recursive: true })\n await writeFile(path.join(dir, 'node_modules', '.bin', 'sh'), '#!/bin/sh\\necho planted\\n', {\n mode: 0o755,\n })\n await workspace('shellExecutor()')\n const out: string[] = []\n const r = await run({\n cwd: root,\n tasks: ['hello'],\n log: { ...log(), taskStdout: (_n, chunk) => void out.push(chunk) },\n handleSignals: false,\n })\n expect([r.outcomes.map((o) => o.status), out.join('')]).toEqual([['success'], 'real\\n'])\n}, 20_000)\n",
25
25
  },
26
26
  graph: {
27
27
  plugin:
package/src/config.ts CHANGED
@@ -261,8 +261,10 @@ export interface SandboxGrants {
261
261
  */
262
262
  write?: string[]
263
263
  /**
264
- * Reachable domains. `true` allows all, `['*.example.com']` a pattern.
265
- * Omitted means no network at all.
264
+ * Reachable domains, `['*.example.com']` a pattern. The proxy is the
265
+ * run's: it filters every sandboxed task against the union of the lists
266
+ * the run's tasks declare, so `true` adds no domain and reaches only
267
+ * those (none in a run with no list). Omitted adds none either.
266
268
  */
267
269
  network?: true | string[]
268
270
  /**
@@ -9,8 +9,9 @@
9
9
  // This module adds nothing implicit on top — no /tmp, no project dir —
10
10
  // so what a task may touch is its vx.config.ts plus those baselines.
11
11
  //
12
- // Network is opt-in per task. By default the sandbox blocks all outbound
13
- // traffic; tasks that need it set `sandbox.network: true`.
12
+ // Network is a domain list, and the run's: SRT's one proxy filters every
13
+ // sandboxed task against the union of the lists the run's tasks declare.
14
+ // With no list, no domain is reachable; `sandbox.network: true` adds none.
14
15
  //
15
16
  // Platform reality:
16
17
  // macOS — `SandboxViolationStore` is populated from the system log
@@ -520,9 +521,8 @@ function hostHasIpv6(): boolean {
520
521
  * servers + (on macOS) the violation log monitor. Safe to call repeatedly
521
522
  * — SRT itself returns early on the second call.
522
523
  *
523
- * The base config sets network to "block everything" (empty allowedDomains).
524
- * Per-task wrapping passes a customConfig that re-enables network for
525
- * tasks with `sandbox.network: true`.
524
+ * The base config sets network to "block everything" (empty allowedDomains)
525
+ * unless `allowedDomains` names some; `sandbox.network: true` adds none.
526
526
  *
527
527
  * @param opts.allowedDomains every domain any sandboxed task in this run
528
528
  * declared. SRT's filtering proxy is per-RUN and reads its allowlist from
@@ -2007,15 +2007,21 @@ async function wantsStraceDetection(): Promise<false | 'plain' | 'seccomp'> {
2007
2007
  })
2008
2008
  const out = await new Response(p.stdout).text()
2009
2009
  await p.exited
2010
- if (p.exitCode !== 0) straceAvailableCache = false
2011
- else {
2010
+ if (p.exitCode !== 0) {
2011
+ straceAvailableCache = false
2012
+ warnUntraced(`strace --version exited ${p.exitCode}`)
2013
+ } else {
2012
2014
  const m = /version (\d+)\.(\d+)/.exec(out)
2013
2015
  const [major, minor] = m ? [Number(m[1]), Number(m[2])] : [0, 0]
2014
2016
  const form = major > 5 || (major === 5 && minor >= 3) ? 'seccomp' : 'plain'
2015
2017
  straceAvailableCache = (await traceAttaches(form)) ? form : false
2016
2018
  }
2017
2019
  } catch {
2020
+ // Not on PATH. Said as the refused attach is: without it an undeclared
2021
+ // read is denied but never reported, so a task that tolerates the miss
2022
+ // passes and caches with no word of it (J's lead).
2018
2023
  straceAvailableCache = false
2024
+ warnUntraced('strace is not on PATH')
2019
2025
  }
2020
2026
  return straceAvailableCache
2021
2027
  }
@@ -2048,14 +2054,17 @@ async function traceAttaches(form: 'plain' | 'seccomp'): Promise<boolean> {
2048
2054
  )
2049
2055
  const err = await new Response(p.stderr).text()
2050
2056
  if ((await p.exited) === 0) return true
2051
- if (!warnedNoTrace) {
2052
- warnedNoTrace = true
2053
- process.stderr.write(
2054
- `[vx] sandbox: strace cannot trace here (${err.trim().split('\n')[0] ?? `exit ${p.exitCode}`}), ` +
2055
- `so sandboxed tasks run without the report of the reads the sandbox denied; ` +
2056
- `the sandbox still enforces\n`,
2057
- )
2058
- }
2057
+ warnUntraced(`strace cannot trace here (${err.trim().split('\n')[0] ?? `exit ${p.exitCode}`})`)
2059
2058
  return false
2060
2059
  }
2061
2060
  let warnedNoTrace = false
2061
+
2062
+ /** Said once per process: why sandboxed tasks run untraced, and what that loses. */
2063
+ function warnUntraced(why: string): void {
2064
+ if (warnedNoTrace) return
2065
+ warnedNoTrace = true
2066
+ process.stderr.write(
2067
+ `[vx] sandbox: ${why}, so sandboxed tasks run without the report of the reads the sandbox denied; ` +
2068
+ `the sandbox still enforces\n`,
2069
+ )
2070
+ }
@@ -81,9 +81,10 @@ export function sandboxRunUnion(nodes: Iterable<TaskNode>): SandboxRunUnion | nu
81
81
  for (const n of sandboxed) {
82
82
  const allow = n.config.exec?.sandbox?.allow
83
83
  const net = allow?.network
84
- // `network: true` is deliberately absent from the union: it SKIPS the
85
- // proxy rather than going through it, so folding it in as `*` would
84
+ // `network: true` adds nothing to the union: folded in as `*` it would
86
85
  // widen the allowlist every OTHER task in the run is filtered against.
86
+ // It does not skip the proxy either, so it reaches only the domains
87
+ // some task of the run lists (schema.md § network is per-RUN).
87
88
  if (Array.isArray(net)) for (const d of net) domains.add(d)
88
89
  for (const d of n.config.exec?.sandbox?.deny?.network ?? []) denied.add(d)
89
90
  const sockets = allow?.unixSockets
@@ -256,9 +256,11 @@ export function configLoadError(err: unknown, configPath: string, kind: string):
256
256
  if (name === 'ResolveMessage') {
257
257
  const spec = /Cannot find (?:package|module) ['"]([^'"]+)['"]/.exec(message)?.[1]
258
258
  const what = spec === undefined ? message.replace(BUST_QUERY, '') : `cannot find '${spec}'`
259
+ // The package the specifier names (`@vzn/vx-otel/x` → `@vzn/vx-otel`):
260
+ // a missing plugin was told to install core (J-59).
259
261
  const hint =
260
262
  spec?.startsWith('@vzn/vx') === true
261
- ? `; install it in the workspace: bun add -d @vzn/vx`
263
+ ? `; install it in the workspace: bun add -d ${spec.split('/').slice(0, 2).join('/')}`
262
264
  : ''
263
265
  return new UserError(`${kind} config ${configPath}: ${what}${hint}`)
264
266
  }