@vzn/vx 0.0.396 → 0.0.397

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.396",
3
+ "version": "0.0.397",
4
4
  "description": "The fastest 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.396",
25
- "@vzn/vx-linux-arm64": "0.0.396",
26
- "@vzn/vx-darwin-x64": "0.0.396",
27
- "@vzn/vx-darwin-arm64": "0.0.396"
24
+ "@vzn/vx-linux-x64": "0.0.397",
25
+ "@vzn/vx-linux-arm64": "0.0.397",
26
+ "@vzn/vx-darwin-x64": "0.0.397",
27
+ "@vzn/vx-darwin-arm64": "0.0.397"
28
28
  },
29
29
  "dependencies": {
30
30
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
@@ -1145,6 +1145,15 @@ export async function wrapSandboxedCommand(
1145
1145
  wrapped = `exec ${readOnlyMasks(wrapped, scratch)}`
1146
1146
  if (process.platform === 'linux' && !hostHasIpv6())
1147
1147
  wrapped = `SOCAT_DEFAULT_LISTEN_IP=4 ${wrapped}`
1148
+ const held = portsHeld(ports)
1149
+ if (held.length > 0) {
1150
+ throw new UserError(
1151
+ `sandbox: localBinding port${held.length === 1 ? '' : 's'} ${held.join(', ')} ` +
1152
+ `${held.length === 1 ? 'is' : 'are'} already in use on this machine, so the task's own ` +
1153
+ `cannot be exposed there and a client would reach the other listener; stop what ` +
1154
+ `holds ${held.length === 1 ? 'it' : 'them'} or list another port`,
1155
+ )
1156
+ }
1148
1157
  if (args.server === true) liveServers.add(tag)
1149
1158
  if (ports.length > 0) {
1150
1159
  spawnHostBridges(ports, tag)
@@ -1402,6 +1411,37 @@ const hostBridges = new Map<
1402
1411
  { ports: readonly number[]; procs: Array<ReturnType<typeof Bun.spawn>> }
1403
1412
  >()
1404
1413
 
1414
+ /**
1415
+ * The ports a host listener already holds where the bridge would bind
1416
+ * (`127.0.0.1`, or every address). The listen wait below reads the same
1417
+ * table and took that listener for the bridge, whose bind had failed: the
1418
+ * task passed and a client of the port reached the other process
1419
+ * (2026-10-03). Linux, own procfs only, as the wait.
1420
+ */
1421
+ function portsHeld(ports: readonly number[]): number[] {
1422
+ if (ports.length === 0 || !procfsIsOwn()) return []
1423
+ const held = new Set<string>()
1424
+ for (const [file, any, loop] of [
1425
+ ['/proc/net/tcp', '00000000', '0100007F'],
1426
+ ['/proc/net/tcp6', '00000000000000000000000000000000', '00000000000000000000000001000000'],
1427
+ ] as const) {
1428
+ let table: string
1429
+ try {
1430
+ table = readFileSync(file, 'utf8')
1431
+ } catch {
1432
+ continue
1433
+ }
1434
+ for (const line of table.split('\n')) {
1435
+ const f = line.trim().split(/\s+/)
1436
+ if (f[3] !== '0A' || f[1] === undefined) continue
1437
+ const [addr, port] = f[1].split(':')
1438
+ // `::1` does not hold 127.0.0.1's port; every address does.
1439
+ if (addr === any || (file === '/proc/net/tcp' && addr === loop)) held.add(port!)
1440
+ }
1441
+ }
1442
+ return ports.filter((p) => held.has(p.toString(16).toUpperCase().padStart(4, '0')))
1443
+ }
1444
+
1405
1445
  /**
1406
1446
  * Wait until each host-side bridge listens on its port, so a server that
1407
1447
  * says it is ready inside the sandbox is reachable on the host: the socat
@@ -1409,7 +1449,8 @@ const hostBridges = new Map<
1409
1449
  * right after its ready line under I/O load (M-22). Read off
1410
1450
  * /proc/net/tcp (127.0.0.1, state 0A); skipped where /proc is not this
1411
1451
  * process's (its net table could be another namespace's), ended early by
1412
- * a bridge that exited (a port already taken), and bounded at 5 s.
1452
+ * a bridge that exited, and bounded at 5 s. A port another listener holds
1453
+ * reads as listening here, so the wrap refuses it first (`portsHeld`).
1413
1454
  */
1414
1455
  async function hostBridgesListen(ports: readonly number[], tag: string): Promise<void> {
1415
1456
  if (!procfsIsOwn()) return
@@ -61,6 +61,17 @@ export const WATCHED_BUILTIN_NAMES = [
61
61
  // A global one config set reached every config loaded after it in the
62
62
  // process: `--all` read it, `--filter` of the reader alone did not (D-122).
63
63
  'globalThis',
64
+ // What the check itself and vx's matching read through: a replaced
65
+ // `Reflect.ownKeys` blinded the check, and `RegExp.prototype.test` or
66
+ // `Date.now` ran vx on a config's choice (D-124).
67
+ 'Reflect',
68
+ 'Object',
69
+ 'Array',
70
+ 'RegExp.prototype',
71
+ 'Function.prototype',
72
+ 'Date',
73
+ 'Date.prototype',
74
+ 'Number.prototype',
64
75
  ] as const
65
76
 
66
77
  const WORKER_SRC = `
@@ -109,7 +120,10 @@ self.onmessage = async (e) => {
109
120
  const watched = e.data.blame
110
121
  ? ${JSON.stringify(WATCHED_BUILTIN_NAMES)}.map((n) => [n, n.split('.').reduce((o, k) => o[k], globalThis)])
111
122
  : null
112
- const own = (o) => new Map(Reflect.ownKeys(o).map((k) => [k, Object.getOwnPropertyDescriptor(o, k)]))
123
+ // Taken before the import: a config can replace them (D-124).
124
+ const RO = Reflect.ownKeys
125
+ const GD = Object.getOwnPropertyDescriptor
126
+ const own = (o) => new Map(RO(o).map((k) => [k, GD(o, k)]))
113
127
  const before = watched?.map(([, o]) => own(o))
114
128
  const envBefore = watched ? { ...live } : null
115
129
  const cwdBefore = watched ? globalThis.process.cwd() : null
@@ -448,7 +448,7 @@ export async function loadProjectConfigs(
448
448
  // Taken only when a config is evaluated in this process: reading every
449
449
  // descriptor of `Bun` makes Bun build its lazy members (`bun:sql`,
450
450
  // `node:stream`), ~7 ms of a two-config warm run where every load hit.
451
- let builtins: BuiltinSnapshot = []
451
+ let builtins: BuiltinSnapshot | undefined
452
452
  let env: Readonly<Record<string, string | undefined>> = {}
453
453
  let cwd = ''
454
454
  // More than one evaluation in flight: a change seen after one load may
@@ -618,20 +618,51 @@ const WATCHED_BUILTINS: ReadonlyArray<readonly [string, object]> = WATCHED_BUILT
618
618
  ] as const,
619
619
  )
620
620
 
621
+ // The check reads through these, taken when a round's snapshot is, before
622
+ // any of its configs runs: a config that set `Reflect.ownKeys = () => []`
623
+ // blinded it, and its `Object.prototype.exec` ran in another project's
624
+ // task (D-124). Each round keeps its own, so an overlapping round cannot
625
+ // hand it a replaced one. The compare and put-back use indexed loops only
626
+ // (a replaced `Array.prototype.forEach` skipped them).
627
+ interface Primitives {
628
+ ownKeys: typeof Reflect.ownKeys
629
+ descriptorOf: typeof Object.getOwnPropertyDescriptor
630
+ defineOwn: typeof Object.defineProperty
631
+ deleteOwn: typeof Reflect.deleteProperty
632
+ hasOwn: typeof Object.hasOwn
633
+ same: typeof Object.is
634
+ keyName: typeof String
635
+ }
636
+
621
637
  interface OwnProperties {
622
638
  keys: PropertyKey[]
623
639
  descriptors: PropertyDescriptor[]
624
- byKey: ReadonlyMap<PropertyKey, PropertyDescriptor>
625
640
  }
626
641
 
627
- type BuiltinSnapshot = readonly OwnProperties[]
642
+ interface BuiltinSnapshot {
643
+ readonly prims: Primitives
644
+ readonly props: readonly OwnProperties[]
645
+ }
628
646
 
629
647
  function builtinSnapshot(): BuiltinSnapshot {
630
- return WATCHED_BUILTINS.map(([, proto]) => {
631
- const keys = Reflect.ownKeys(proto)
632
- const descriptors = keys.map((k) => Object.getOwnPropertyDescriptor(proto, k)!)
633
- return { keys, descriptors, byKey: new Map(keys.map((k, j) => [k, descriptors[j]!])) }
634
- })
648
+ const prims: Primitives = {
649
+ ownKeys: Reflect.ownKeys,
650
+ descriptorOf: Object.getOwnPropertyDescriptor,
651
+ defineOwn: Object.defineProperty,
652
+ deleteOwn: Reflect.deleteProperty,
653
+ hasOwn: Object.hasOwn,
654
+ same: Object.is,
655
+ keyName: String,
656
+ }
657
+ const props: OwnProperties[] = []
658
+ for (let i = 0; i < WATCHED_BUILTINS.length; i++) {
659
+ const proto = WATCHED_BUILTINS[i]![1]
660
+ const keys = prims.ownKeys(proto)
661
+ const descriptors: PropertyDescriptor[] = []
662
+ for (let j = 0; j < keys.length; j++) descriptors[j] = prims.descriptorOf(proto, keys[j]!)!
663
+ props[i] = { keys, descriptors }
664
+ }
665
+ return { prims, props }
635
666
  }
636
667
 
637
668
  /**
@@ -641,13 +672,17 @@ function builtinSnapshot(): BuiltinSnapshot {
641
672
  * evaluated config: the full check was ~0.1 ms a config, ~100 ms of a
642
673
  * 1,000-config cold load.
643
674
  */
644
- function unchanged(proto: object, keys: readonly PropertyKey[], was: OwnProperties): boolean {
675
+ function unchanged(
676
+ p: Primitives,
677
+ proto: object,
678
+ keys: readonly PropertyKey[],
679
+ was: OwnProperties,
680
+ ): boolean {
645
681
  if (keys.length !== was.keys.length) return false
646
682
  for (let j = 0; j < keys.length; j++) {
647
683
  const key = keys[j]!
648
684
  if (key !== was.keys[j]) return false
649
- if (!sameDescriptor(was.descriptors[j]!, Object.getOwnPropertyDescriptor(proto, key)!))
650
- return false
685
+ if (!sameDescriptor(p, was.descriptors[j]!, p.descriptorOf(proto, key)!)) return false
651
686
  }
652
687
  return true
653
688
  }
@@ -676,27 +711,38 @@ function builtinsChanged(changed: readonly string[], configPath?: string): UserE
676
711
  }
677
712
 
678
713
  /** Puts back what changed since `before`, naming each property it put back. */
679
- function restoreBuiltins(before: BuiltinSnapshot): string[] {
714
+ function restoreBuiltins(before: BuiltinSnapshot | undefined): string[] {
680
715
  const changed: string[] = []
681
- WATCHED_BUILTINS.forEach(([name, proto], i) => {
682
- const keys = Reflect.ownKeys(proto)
683
- if (unchanged(proto, keys, before[i]!)) return
684
- const was = before[i]!.byKey
685
- for (const key of keys) {
686
- const prior = was.get(key)
687
- const now = Object.getOwnPropertyDescriptor(proto, key)!
688
- if (prior !== undefined && sameDescriptor(prior, now)) continue
689
- changed.push(`${name}.${String(key)}`)
690
- if (prior === undefined) Reflect.deleteProperty(proto, key)
691
- else Object.defineProperty(proto, key, prior)
692
- }
693
- for (const [key, prior] of was) {
694
- if (!Object.hasOwn(proto, key)) {
695
- changed.push(`${name}.${String(key)}`)
696
- Object.defineProperty(proto, key, prior)
716
+ if (before === undefined) return changed
717
+ const p = before.prims
718
+ for (let i = 0; i < WATCHED_BUILTINS.length; i++) {
719
+ const name = WATCHED_BUILTINS[i]![0]
720
+ const proto = WATCHED_BUILTINS[i]![1]
721
+ const was = before.props[i]!
722
+ const keys = p.ownKeys(proto)
723
+ if (unchanged(p, proto, keys, was)) continue
724
+ // Slow path, a change only: a linear lookup keeps it off `Map`.
725
+ for (let j = 0; j < keys.length; j++) {
726
+ const key = keys[j]!
727
+ let prior: PropertyDescriptor | undefined
728
+ for (let k = 0; k < was.keys.length; k++) {
729
+ if (was.keys[k] === key) {
730
+ prior = was.descriptors[k]
731
+ break
732
+ }
697
733
  }
734
+ if (prior !== undefined && sameDescriptor(p, prior, p.descriptorOf(proto, key)!)) continue
735
+ changed[changed.length] = name + '.' + p.keyName(key)
736
+ if (prior === undefined) p.deleteOwn(proto, key)
737
+ else p.defineOwn(proto, key, prior)
698
738
  }
699
- })
739
+ for (let k = 0; k < was.keys.length; k++) {
740
+ const key = was.keys[k]!
741
+ if (p.hasOwn(proto, key)) continue
742
+ changed[changed.length] = name + '.' + p.keyName(key)
743
+ p.defineOwn(proto, key, was.descriptors[k]!)
744
+ }
745
+ }
700
746
  return changed
701
747
  }
702
748
 
@@ -739,10 +785,10 @@ function restoreCwd(before: string): string[] {
739
785
  return ['process.cwd (a chdir)']
740
786
  }
741
787
 
742
- function sameDescriptor(a: PropertyDescriptor, b: PropertyDescriptor): boolean {
788
+ function sameDescriptor(p: Primitives, a: PropertyDescriptor, b: PropertyDescriptor): boolean {
743
789
  return (
744
790
  // `globalThis.NaN` is watched (D-122), and NaN !== NaN.
745
- Object.is(a.value, b.value) &&
791
+ p.same(a.value, b.value) &&
746
792
  a.get === b.get &&
747
793
  a.set === b.set &&
748
794
  a.writable === b.writable &&