@vzn/vx 0.0.365 → 0.0.367

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.365",
3
+ "version": "0.0.367",
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.365",
25
- "@vzn/vx-linux-arm64": "0.0.365",
26
- "@vzn/vx-darwin-x64": "0.0.365",
27
- "@vzn/vx-darwin-arm64": "0.0.365"
24
+ "@vzn/vx-linux-x64": "0.0.367",
25
+ "@vzn/vx-linux-arm64": "0.0.367",
26
+ "@vzn/vx-darwin-x64": "0.0.367",
27
+ "@vzn/vx-darwin-arm64": "0.0.367"
28
28
  },
29
29
  "dependencies": {
30
30
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
@@ -543,6 +543,35 @@ function hostHasIpv6(): boolean {
543
543
  return hasIpv6
544
544
  }
545
545
 
546
+ /**
547
+ * Refuse a network entry SRT's own schema refuses, naming it. vx handed
548
+ * the union over unchecked, and SRT's proxy matched as it could: a URL
549
+ * (`https://example.com`), a dotless host or a port past 65535 matched
550
+ * nothing, so the grant silently reached no host, and `'*'`, which the
551
+ * schema refuses as too broad, opened every host to every sandboxed task
552
+ * of the run, the allowlist being the run's (2026-10-02).
553
+ */
554
+ function assertDomains(
555
+ schema: SrtModule['NetworkConfigSchema'],
556
+ allowed: readonly string[],
557
+ denied: readonly string[],
558
+ ): void {
559
+ const bad = [
560
+ ...allowed
561
+ .filter((d) => !schema.shape.allowedDomains.safeParse([d]).success)
562
+ .map((d) => `allow.network "${d}"`),
563
+ ...denied
564
+ .filter((d) => !schema.shape.deniedDomains.safeParse([d]).success)
565
+ .map((d) => `deny.network "${d}"`),
566
+ ]
567
+ if (bad.length === 0) return
568
+ throw new UserError(
569
+ `sandbox: ${bad.join(', ')} is not a host pattern: name a host ("example.com"), a ` +
570
+ `subdomain wildcard ("*.example.com") or either with a port ("example.com:443"); no ` +
571
+ `scheme or path, and "*" or "*.com" is refused as too broad (a bare "*" only in deny)`,
572
+ )
573
+ }
574
+
546
575
  /**
547
576
  * One-time SRT initialization per orchestrator run. Starts the proxy
548
577
  * servers + (on macOS) the violation log monitor. Safe to call repeatedly
@@ -586,7 +615,8 @@ export async function initSandbox(opts?: {
586
615
  await resetting
587
616
  // Before SRT starts, so the very first task already has one.
588
617
  await mkdir(sandboxTmpdir(), { recursive: true })
589
- const { SandboxManager } = await loadSrt()
618
+ const { SandboxManager, NetworkConfigSchema } = await loadSrt()
619
+ assertDomains(NetworkConfigSchema, opts?.allowedDomains ?? [], opts?.deniedDomains ?? [])
590
620
  scopedDenyScan = process.platform === 'linux' && canScopeDenyScan(process.cwd())
591
621
  const config: Parameters<typeof SandboxManager.initialize>[0] = {
592
622
  network: {
@@ -11,6 +11,7 @@ import type { ExecConfig } from '../config.js'
11
11
  import {
12
12
  bindableWrites,
13
13
  initSandbox,
14
+ resetSandbox,
14
15
  probeSandbox,
15
16
  punchWalls,
16
17
  resolveSandboxConfig,
@@ -141,6 +142,10 @@ export function prepareSandbox(nodes: Iterable<TaskNode>): SandboxArmer | null {
141
142
  ...(gitConfig ? { gitConfig: true } : {}),
142
143
  })
143
144
  } catch (err) {
145
+ // The Linux probe brings the runtime up, and only an armed run
146
+ // resets it: a refusal past the probe left its proxy holding the
147
+ // process open, and the run hung after its summary (2026-10-02).
148
+ await resetSandbox().catch(() => {})
144
149
  // A throw from the runtime itself (its bridge needs socat, which
145
150
  // the dependency check does not cover) gets the same one-line
146
151
  // verdict as a refused probe, not an internal error with a stack.
@@ -63,7 +63,16 @@ export function secretMask(
63
63
  ): SecretMask | null {
64
64
  const values = new Set<string>()
65
65
  const add = (value: string | undefined): void => {
66
- if (value !== undefined && value.length >= MIN_SECRET_CHARS) values.add(value)
66
+ if (value === undefined || value.length < MIN_SECRET_CHARS) return
67
+ values.add(value)
68
+ // A multi-line value (a PEM key) is also masked line by line, as GitHub
69
+ // Actions does: a tool that indents or reflows it prints no copy of the
70
+ // whole value, and every line leaked (L-36).
71
+ if (!value.includes('\n')) return
72
+ for (const line of value.split(/\r?\n/)) {
73
+ const trimmed = line.trim()
74
+ if (trimmed.length >= MIN_SECRET_CHARS) values.add(trimmed)
75
+ }
67
76
  }
68
77
  for (const source of sources) {
69
78
  if (source === undefined) continue