@vzn/vx 0.0.382 → 0.0.384

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.382",
3
+ "version": "0.0.384",
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.382",
25
- "@vzn/vx-linux-arm64": "0.0.382",
26
- "@vzn/vx-darwin-x64": "0.0.382",
27
- "@vzn/vx-darwin-arm64": "0.0.382"
24
+ "@vzn/vx-linux-x64": "0.0.384",
25
+ "@vzn/vx-linux-arm64": "0.0.384",
26
+ "@vzn/vx-darwin-x64": "0.0.384",
27
+ "@vzn/vx-darwin-arm64": "0.0.384"
28
28
  },
29
29
  "dependencies": {
30
30
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
@@ -632,12 +632,14 @@ export async function initSandbox(opts?: {
632
632
  ...bundledJavaAgent(),
633
633
  }
634
634
  if (!srtUp) await unlinkStaleMuxSockets()
635
+ const listening = srtCleanupAdopted ? undefined : cleanupListeners()
635
636
  await SandboxManager.initialize(
636
637
  config,
637
638
  undefined,
638
639
  // enableLogMonitor — macOS-only; populates the SandboxViolationStore.
639
640
  true,
640
641
  )
642
+ if (listening !== undefined) adoptSrtCleanup(listening)
641
643
  srtUp = true
642
644
  perTaskRun = opts?.allowAllUnixSockets === true || opts?.gitConfig === true ? config : undefined
643
645
  // `initialize()` returns early once SRT is up, and on Linux the
@@ -674,11 +676,21 @@ export async function resetSandbox(): Promise<void> {
674
676
  const reset = (async () => {
675
677
  const { SandboxManager } = await loadSrt()
676
678
  await SandboxManager.reset()
677
- srtUp = false
678
- perTaskRun = undefined
679
- availabilityCache.clear()
680
- straceAvailableCache = undefined
679
+ srtDown()
681
680
  })()
681
+ trackReset(reset)
682
+ await reset
683
+ }
684
+
685
+ function srtDown(): void {
686
+ srtUp = false
687
+ perTaskRun = undefined
688
+ availabilityCache.clear()
689
+ straceAvailableCache = undefined
690
+ }
691
+
692
+ /** `reset` is the one `initSandbox` waits out before it starts SRT again. */
693
+ function trackReset(reset: Promise<unknown>): void {
682
694
  const settled = reset.then(
683
695
  () => {},
684
696
  () => {},
@@ -687,7 +699,38 @@ export async function resetSandbox(): Promise<void> {
687
699
  void settled.then(() => {
688
700
  if (resetting === settled) resetting = undefined
689
701
  })
690
- await reset
702
+ }
703
+
704
+ const SRT_CLEANUP_EVENTS = ['exit', 'SIGINT', 'SIGTERM'] as const
705
+ let srtCleanupAdopted = false
706
+
707
+ function cleanupListeners(): Map<string, unknown[]> {
708
+ return new Map(SRT_CLEANUP_EVENTS.map((ev) => [ev, process.listeners(ev)]))
709
+ }
710
+
711
+ /**
712
+ * SRT's first `initialize` registers its own once-only `exit`, SIGINT and
713
+ * SIGTERM listeners, each an unawaited `reset()`. That reset kills the
714
+ * bridges at once but clears SRT's init promise only once its proxies
715
+ * have closed, so an `initSandbox` in between had `initialize` return
716
+ * early on the dying session, and the next wrap threw "Linux HTTP bridge
717
+ * socket does not exist" (the bridge-socket row's gate failure, M-35).
718
+ * Each listener stays, once-only as SRT made it; vx now tracks the reset
719
+ * it starts, as its own.
720
+ */
721
+ function adoptSrtCleanup(before: Map<string, unknown[]>): void {
722
+ for (const ev of SRT_CLEANUP_EVENTS) {
723
+ const had = before.get(ev)!
724
+ for (const l of process.listeners(ev) as ((...a: unknown[]) => unknown)[]) {
725
+ if (had.includes(l)) continue
726
+ srtCleanupAdopted = true
727
+ process.removeListener(ev, l)
728
+ process.once(ev, (...a: unknown[]) => {
729
+ srtDown()
730
+ trackReset(Promise.resolve(l(...a)))
731
+ })
732
+ }
733
+ }
691
734
  }
692
735
 
693
736
  export interface SandboxedRunArgs {
@@ -1794,7 +1837,7 @@ async function runSandboxedOnce(
1794
1837
  config: args.config,
1795
1838
  skip: [taskTmpRoot(), ...srtDefaultWritePaths()],
1796
1839
  })
1797
- if (outside.length > 0) violations.push(outsideWritesHint(outside))
1840
+ if (outside.length > 0) violations.push(outsideWritesHint(outside, baselines.denyRead))
1798
1841
  }
1799
1842
 
1800
1843
  // The one denial macOS never logs. MEASURED 2026-09-05, same machine, two
@@ -1862,20 +1905,33 @@ async function runSandboxedOnce(
1862
1905
  }
1863
1906
 
1864
1907
  /** The hint for writes refused outside the project, a few paths named. */
1865
- function outsideWritesHint(paths: readonly string[]): SandboxViolation {
1908
+ function outsideWritesHint(paths: readonly string[], walled: readonly string[]): SandboxViolation {
1866
1909
  const shown = paths.slice(0, 5).join(', ')
1867
1910
  const more = paths.length > 5 ? ` and ${paths.length - 5} more` : ''
1868
1911
  const home = toRealPath(os.homedir())
1869
1912
  const dir = path.dirname(paths[0]!)
1870
1913
  const spelled = atOrUnder(dir, home) ? `~${dir.slice(home.length)}` : dir
1871
- return {
1872
- timestamp: new Date(),
1873
- hint: true,
1874
- line:
1875
- `vx: the sandbox refused writes outside the project, which are not reported as ` +
1876
- `violations: ${shown}${more}. If the task needs one, grant its directory, e.g. ` +
1877
- `\`allow: { write: ['${spelled}/'] }\`.`,
1878
- }
1914
+ const refused =
1915
+ `vx: the sandbox refused writes outside the project, which are not reported as ` +
1916
+ `violations: ${shown}${more}.`
1917
+ // Granting a system temp directory opens it to every write of the task;
1918
+ // the task already has a temp directory of its own. A workspace kept
1919
+ // under one is the workspace, and its directory is the grant.
1920
+ const first = paths[0]!
1921
+ const scratchTemp =
1922
+ hostTempRoots().some((t) => atOrUnder(first, t)) &&
1923
+ !walled.some((w) => atOrUnder(first, toRealPath(w)))
1924
+ const line = scratchTemp
1925
+ ? `${refused} The task has its own temp directory, empty at its start: write under ` +
1926
+ `$TMPDIR (os.tmpdir() in Node and Bun) instead of a fixed path.`
1927
+ : `${refused} If the task needs one, grant its directory, e.g. ` +
1928
+ `\`allow: { write: ['${spelled}/'] }\`.`
1929
+ return { timestamp: new Date(), hint: true, line }
1930
+ }
1931
+
1932
+ /** The host's shared temp directories, canonical: what a fixed temp path in a tool names. */
1933
+ function hostTempRoots(): string[] {
1934
+ return [...new Set(['/tmp', '/var/tmp', os.tmpdir()].map(toRealPath))]
1879
1935
  }
1880
1936
 
1881
1937
  function hiddenReadsHint(paths: readonly string[], within: string): SandboxViolation {