@vzn/vx 0.0.363 → 0.0.364

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.363",
3
+ "version": "0.0.364",
4
4
  "description": "A fast, correct task runner and build cache for JavaScript monorepos.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -21,10 +21,10 @@
21
21
  "node": ">=18"
22
22
  },
23
23
  "optionalDependencies": {
24
- "@vzn/vx-linux-x64": "0.0.363",
25
- "@vzn/vx-linux-arm64": "0.0.363",
26
- "@vzn/vx-darwin-x64": "0.0.363",
27
- "@vzn/vx-darwin-arm64": "0.0.363"
24
+ "@vzn/vx-linux-x64": "0.0.364",
25
+ "@vzn/vx-linux-arm64": "0.0.364",
26
+ "@vzn/vx-darwin-x64": "0.0.364",
27
+ "@vzn/vx-darwin-arm64": "0.0.364"
28
28
  },
29
29
  "dependencies": {
30
30
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
package/src/bin.ts CHANGED
@@ -9,10 +9,14 @@ async function main(): Promise<void> {
9
9
  // `vx --version` needs one leaf module; the dispatcher and the util
10
10
  // barrel it pulled in were 20 ms of its 28 (2026-10-01).
11
11
  if (argv.length === 1 && (argv[0] === '--version' || argv[0] === 'version')) {
12
- process.stdout.write(`vx ${(await import('./version.js')).VERSION}\n`)
12
+ // `Bun.stdout`, not `process.stdout`: the Node stream's first touch is
13
+ // ~7 ms, half of this verb (2026-10-02). A reader gone (EPIPE) is no
14
+ // failure of the line.
15
+ await Bun.write(Bun.stdout, `vx ${(await import('./version.js')).VERSION}\n`).catch(() => {})
13
16
  settled = true
14
17
  return
15
18
  }
19
+ listenForReadersGone()
16
20
  const { registerCoreAlias, run } = await import('./cli/index.js')
17
21
  // Every `import … from '@vzn/vx'` this process evaluates — a plugin
18
22
  // package, a workspace or project config — resolves to THIS core, not to
@@ -86,8 +90,11 @@ process.on('beforeExit', () => {
86
90
  // and exits with its own verdict; what was going to the reader goes
87
91
  // nowhere. Same for stderr (`2>&1 | head`). Here and not in the logger:
88
92
  // an embedder's streams are the embedder's.
89
- for (const stream of [process.stdout, process.stderr]) {
90
- stream.on('error', () => {})
93
+ // Set up past `--version`, which never touches the Node streams.
94
+ function listenForReadersGone(): void {
95
+ for (const stream of [process.stdout, process.stderr]) {
96
+ stream.on('error', () => {})
97
+ }
91
98
  }
92
99
 
93
100
  void main()
@@ -53,6 +53,8 @@ function octal(h: Uint8Array, off: number, len: number): number {
53
53
  for (let i = off + 1; i < off + len; i++) n = n * 256 + h[i]!
54
54
  return n
55
55
  }
56
+ const plain = plainOctal(h, off, len)
57
+ if (plain !== null) return plain
56
58
  const s = field(h, off, len).trim()
57
59
  if (s === '') return 0
58
60
  // Every digit, not the longest parseable prefix: `parseInt` would read
@@ -61,10 +63,33 @@ function octal(h: Uint8Array, off: number, len: number): number {
61
63
  return parseInt(s, 8)
62
64
  }
63
65
 
66
+ /**
67
+ * The field every writer emits — octal digits between ASCII spaces, ended
68
+ * by a NUL or the field's end — read off the bytes; null for anything
69
+ * else, which `octal` reads the slow way. Three fields a header through a
70
+ * decoder and a regex were most of a small artifact's read (2026-10-02).
71
+ */
72
+ function plainOctal(h: Uint8Array, off: number, len: number): number | null {
73
+ const end = off + len
74
+ let i = off
75
+ while (i < end && h[i] === 0x20) i++
76
+ let n = 0
77
+ while (i < end && h[i]! >= 0x30 && h[i]! <= 0x37) n = n * 8 + (h[i++]! - 0x30)
78
+ while (i < end && h[i] === 0x20) i++
79
+ return i === end || h[i] === 0 ? n : null
80
+ }
81
+
82
+ function isZeroBlock(h: Uint8Array): boolean {
83
+ for (let i = 0; i < BLOCK; i++) if (h[i] !== 0) return false
84
+ return true
85
+ }
86
+
64
87
  function checksumOk(h: Uint8Array): boolean {
65
88
  const stored = octal(h, 148, 8)
66
- let sum = 0
67
- for (let i = 0; i < BLOCK; i++) sum += i >= 148 && i < 156 ? 32 : h[i]!
89
+ // The checksum field counts as eight spaces.
90
+ let sum = 8 * 32
91
+ for (let i = 0; i < 148; i++) sum += h[i]!
92
+ for (let i = 156; i < BLOCK; i++) sum += h[i]!
68
93
  return sum === stored
69
94
  }
70
95
 
@@ -107,6 +132,18 @@ class Source {
107
132
 
108
133
  /** Exactly `n` bytes, or null at a clean end (nothing buffered), or a TarFormatError mid-entry. */
109
134
  async exact(n: number, what: string): Promise<Uint8Array | null> {
135
+ // Within one chunk, a view: the copy below was a third of a small
136
+ // artifact's read. Nothing writes to what this returns.
137
+ const c0 = this.chunks[0]
138
+ if (c0 !== undefined && c0.byteLength - this.head >= n) {
139
+ const view = c0.subarray(this.head, this.head + n)
140
+ this.head += n
141
+ if (this.head === c0.byteLength) {
142
+ this.chunks.shift()
143
+ this.head = 0
144
+ }
145
+ return view
146
+ }
110
147
  const out = new Uint8Array(n)
111
148
  let got = 0
112
149
  while (got < n) {
@@ -163,7 +200,7 @@ export async function* tarEntries(stream: ReadableStream<Uint8Array>): AsyncGene
163
200
  if (zeroBlocks === 0) throw new TarFormatError('archive has no end-of-archive marker')
164
201
  return
165
202
  }
166
- if (h.every((b) => b === 0)) {
203
+ if (isZeroBlock(h)) {
167
204
  zeroBlocks++
168
205
  if (zeroBlocks >= 2) return
169
206
  continue
@@ -89,6 +89,7 @@ import {
89
89
  } from './sandbox-deny-scan.js'
90
90
  import {
91
91
  parseStraceViolations,
92
+ refusedConnections,
92
93
  refusedWrites,
93
94
  refusedWritesOutside,
94
95
  reportableViolations,
@@ -1732,6 +1733,7 @@ async function runSandboxedOnce(
1732
1733
  bindableWrites(args.config.allowWrite),
1733
1734
  scratch,
1734
1735
  ),
1736
+ ...refusedConnections(records.map((v) => v.line)),
1735
1737
  ]
1736
1738
  : []
1737
1739
  if (straceLog) {
@@ -419,6 +419,8 @@ function matchesIgnore(
419
419
  * crossing, and the task can grant it.
420
420
  */
421
421
  function describeMacViolation(line: string): Partial<SandboxViolation> {
422
+ const proxy = PROXY_DENY_RE.exec(line)
423
+ if (proxy !== null) return { target: proxy[1]!, ignorable: ['network'] }
422
424
  const m = /deny\(\d+\)\s+(\S+)\s+(.+?)\s*$/.exec(line)
423
425
  if (m === null) return {}
424
426
  const [op, target] = [m[1]!, m[2]!]
@@ -589,6 +591,28 @@ function filterIgnored(
589
591
  return violations.filter((v) => !matchesIgnore(v, ignore))
590
592
  }
591
593
 
594
+ /**
595
+ * SRT's filtering proxy records a connection it refused as `deny
596
+ * network-outbound <host>:<port> (<reason>)`, on both platforms: the one
597
+ * component that knows the host. Read as its own shape, so `ignore.network`
598
+ * silences it by host; the seatbelt pattern above wants `deny(<n>)`.
599
+ */
600
+ const PROXY_DENY_RE = /^deny network-outbound (\S+) \([^)]*\)$/
601
+
602
+ /**
603
+ * Linux: the connections the proxy refused, from the store records. Only
604
+ * the write observer's records were read there, so a task denied a host
605
+ * failed with its own `403` and no report, and one that survived the
606
+ * refusal passed, where macOS reports the same record (2026-10-02).
607
+ */
608
+ export function refusedConnections(records: readonly string[]): SandboxViolation[] {
609
+ return [...new Set(records.filter((r) => PROXY_DENY_RE.test(r)))].map((line) => ({
610
+ line,
611
+ timestamp: new Date(),
612
+ ...describeMacViolation(line),
613
+ }))
614
+ }
615
+
592
616
  /**
593
617
  * The writes SRT's Linux observer saw that this task's binds do not cover.
594
618
  * The observer reports every write-intent syscall as `deny <syscall>
@@ -612,6 +636,11 @@ export function refusedWrites(
612
636
  const m = /^deny (\S+) (\/.*)$/.exec(record)
613
637
  if (m === null) continue
614
638
  const [, syscall, raw] = m as unknown as [string, string, string]
639
+ // A descriptor or pseudo-file is the task's, and resolving it here
640
+ // reads vx's own: strace's log is `/dev/fd/5`, and a vx started with
641
+ // `5>out.log` in the project reported a write to `out.log` and failed
642
+ // a clean task (2026-10-02).
643
+ if (/^\/(?:dev|proc)\//.test(raw)) continue
615
644
  const abs = toRealPath(raw)
616
645
  if (isUnderAny(abs, binds) || underGlob(abs, globs)) continue
617
646
  const key = `${syscall}|${abs}`
@@ -915,7 +915,9 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
915
915
  signal: res.signal,
916
916
  hidden: req.sandbox && ((f: string) => !sandboxReads(req.sandbox!, f)),
917
917
  })
918
- if (verdict !== undefined) log.taskStderr(node, `\n${verdict}\n`)
918
+ // The line quotes the command's first word, which a config may have
919
+ // built from a secret: masked as the task's own output is (L-37).
920
+ if (verdict !== undefined) log.taskStderr(node, `\n${secrets?.mask(verdict) ?? verdict}\n`)
919
921
  // A committed file another task's clean removed, still gone: vx
920
922
  // cleans outputs before a run where Turbo does not, so a reader with
921
923
  // no edge to the producer fails naming only the file (A-48).
@@ -777,6 +777,36 @@ const FOREIGN_FIELDS: ReadonlyMap<ReadonlySet<string>, Readonly<Record<string, s
777
777
  'a command run outside vx: a task never reads the terminal (its stdin is EOF, or a pipe vx holds under `persistent`)',
778
778
  },
779
779
  ],
780
+ // wireit's `files` / `output` and include-style spellings written into
781
+ // a cache block, refused with no word on where they go (D-118).
782
+ [
783
+ CACHE_FIELDS,
784
+ {
785
+ files: '`inputs.files`',
786
+ output: '`outputs.files`',
787
+ env: '`inputs.env`',
788
+ dependencies: "the task's `dependsOn` (an upstream's key folds into this one)",
789
+ enabled: 'no field: a task with a `cache` block caches, one without runs every time',
790
+ },
791
+ ],
792
+ [
793
+ CACHE_INPUT_FIELDS,
794
+ {
795
+ globs: '`files`',
796
+ include: '`files`',
797
+ patterns: '`files`',
798
+ exclude: "`files` with a `!` entry (`'!**/*.test.ts'`)",
799
+ ignore: "`files` with a `!` entry (`'!**/*.test.ts'`)",
800
+ },
801
+ ],
802
+ [
803
+ CACHE_OUTPUT_FIELDS,
804
+ {
805
+ globs: '`files`',
806
+ include: '`files`',
807
+ exclude: "`files` with a `!` entry (`'!dist/cache/**'`)",
808
+ },
809
+ ],
780
810
  // A package's turbo.json and Nx's project.json keys, written into a
781
811
  // project's vx.config (D-56). `tags` had read as a typo of `tasks`.
782
812
  [