@vzn/vx 0.0.598 → 0.0.600

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.598",
3
+ "version": "0.0.600",
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.598",
25
- "@vzn/vx-linux-arm64": "0.0.598",
26
- "@vzn/vx-darwin-x64": "0.0.598",
27
- "@vzn/vx-darwin-arm64": "0.0.598"
24
+ "@vzn/vx-linux-x64": "0.0.600",
25
+ "@vzn/vx-linux-arm64": "0.0.600",
26
+ "@vzn/vx-darwin-x64": "0.0.600",
27
+ "@vzn/vx-darwin-arm64": "0.0.600"
28
28
  },
29
29
  "dependencies": {
30
30
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
@@ -209,6 +209,9 @@ interface PackArgs {
209
209
  within?: string
210
210
  }
211
211
 
212
+ /** The largest second count ustar's 12-byte mtime field holds: 11 octal digits. */
213
+ const USTAR_MTIME_MAX = 8 ** 11 - 1
214
+
212
215
  /** At or below this many outputs, `planArtifact` stats them on the calling thread. */
213
216
  const ON_THREAD_STATS = 32
214
217
 
@@ -309,10 +312,10 @@ export async function planArtifact(args: PackArgs): Promise<ArtifactPlan> {
309
312
  size: st.size,
310
313
  mode,
311
314
  mtimeMs: Math.floor(st.mtimeMs),
312
- // ustar's octal field holds no sign: an mtime before 1970 made the
313
- // header unreadable and every save of it a "corrupt artifact". The
314
- // sidecar above carries the real value.
315
- mtime: Math.max(0, Math.floor(st.mtimeMs / 1000)),
315
+ // ustar's octal field holds no sign and 11 digits: an mtime before
316
+ // 1970 made the header unreadable, and one past March 2242 failed
317
+ // every save of the task. The sidecar above carries the real value.
318
+ mtime: Math.min(USTAR_MTIME_MAX, Math.max(0, Math.floor(st.mtimeMs / 1000))),
316
319
  }
317
320
  }),
318
321
  )
@@ -1570,13 +1570,14 @@ export class Cache implements CacheLayer {
1570
1570
  } catch (err) {
1571
1571
  // A UserError here is the extractor naming the tree's fault (an
1572
1572
  // output directory that links out of the project).
1573
- if (
1574
- err instanceof ArchiveSecurityError ||
1575
- err instanceof CorruptArtifactError ||
1576
- err instanceof UserError
1577
- ) {
1578
- throw err
1579
- }
1573
+ if (err instanceof CorruptArtifactError || err instanceof UserError) throw err
1574
+ // The save and the ingest proved every name safe, so one that reads
1575
+ // unsafe now is damaged or tampered bytes: a long name's pax record
1576
+ // has no header checksum, and the name is judged before the CRC at
1577
+ // the end is read. Thrown as is, it failed the task on every run with
1578
+ // the entry kept (X-115).
1579
+ if (err instanceof ArchiveSecurityError)
1580
+ throw new CorruptArtifactError(hash, err.message, err)
1580
1581
  // What is on disk, not what is in the archive: a directory standing
1581
1582
  // where the entry holds a file, or a file where it needs a directory.
1582
1583
  // The clean removes the FILES the output globs select and the
@@ -1554,6 +1554,12 @@ function literalReadPaths(
1554
1554
  * `/**` is stripped before the compile). A task's read grant whose only
1555
1555
  * brackets are escaped names one path too, and Linux binds it whole, so
1556
1556
  * it gets the same subtree: `out/\[id\]` read `out/[id]` and none of it.
1557
+ *
1558
+ * A `*` or `?` meant as itself (escaped in a grant, or in a baseline's
1559
+ * name) has no spelling: SRT's rewrite of each runs inside a class too,
1560
+ * and a backslash compiles as a literal one, so `a\*.txt` granted
1561
+ * `a\bc.txt` and never `a*.txt`. Left out and said once, as on Linux
1562
+ * (`bindableReads`).
1557
1563
  */
1558
1564
  export function seatbeltBrackets(
1559
1565
  config: Parameters<SrtModule['SandboxManager']['wrapWithSandbox']>[2],
@@ -1563,7 +1569,9 @@ export function seatbeltBrackets(
1563
1569
  if (fs === undefined) return config
1564
1570
  const literal = (p: string): string => p.replaceAll('\\[', '[[]').replaceAll('\\]', ']')
1565
1571
  const bracketed = new Set(names.filter((n) => /[[\]]/.test(n)))
1572
+ const baseline = new Set(names)
1566
1573
  const read = (p: string): string[] => {
1574
+ if (!seatbeltSpells(p, 'read', baseline.has(p))) return []
1567
1575
  const name = bracketed.has(p) ? p : /\\[[\]]/.test(p) ? namedPath(p) : undefined
1568
1576
  if (name === undefined) return [literal(p)]
1569
1577
  const at = name.replaceAll('[', '[[]')
@@ -1574,7 +1582,7 @@ export function seatbeltBrackets(
1574
1582
  filesystem: {
1575
1583
  ...fs,
1576
1584
  denyRead: fs.denyRead.map((p) => p.replaceAll('[', '[[]')),
1577
- allowWrite: fs.allowWrite.map(literal),
1585
+ allowWrite: fs.allowWrite.filter((p) => seatbeltSpells(p, 'write', false)).map(literal),
1578
1586
  ...(fs.allowRead !== undefined
1579
1587
  ? { allowRead: [...new Set(fs.allowRead.flatMap(read))] }
1580
1588
  : {}),
@@ -1582,6 +1590,28 @@ export function seatbeltBrackets(
1582
1590
  }
1583
1591
  }
1584
1592
 
1593
+ /** A `*` or `?` behind an odd run of backslashes: the grant means the character. */
1594
+ const ESCAPED_WILDCARD = /(?:^|[^\\])(?:\\\\)*\\[*?]/
1595
+
1596
+ const warnedUnspellable = new Set<string>()
1597
+
1598
+ /** Whether seatbelt's SRT can take `grant` as written; said once when not. */
1599
+ function seatbeltSpells(grant: string, kind: 'read' | 'write', name: boolean): boolean {
1600
+ const m = name ? /[*?]/.exec(grant) : ESCAPED_WILDCARD.exec(grant)
1601
+ if (m === null) return true
1602
+ const at = name ? m.index : m.index + m[0].length - 2
1603
+ if (!warnedUnspellable.has(`${kind} ${grant}`)) {
1604
+ warnedUnspellable.add(`${kind} ${grant}`)
1605
+ process.stderr.write(
1606
+ `[vx] sandbox: the ${kind} grant ${grant} names a literal ${grant[at + (name ? 0 : 1)]}, ` +
1607
+ `which the macOS sandbox reads as a pattern and cannot spell as a name, so it is not ` +
1608
+ `granted. Rename it, or grant the directory above it: ` +
1609
+ `${grant.slice(0, grant.lastIndexOf(path.sep, at) + 1)}\n`,
1610
+ )
1611
+ }
1612
+ return false
1613
+ }
1614
+
1585
1615
  /** SRT's wrap, with the socket lift and the git-config grant this task asked for, or none (L-6, B-41). */
1586
1616
  function wrapForTask(
1587
1617
  SandboxManager: SrtModule['SandboxManager'],
@@ -531,7 +531,12 @@ async function executePersistentTask(args: ExecuteArgs): Promise<TaskOutcome> {
531
531
  return {
532
532
  node,
533
533
  status: 'failed',
534
- exitCode: ready?.reason === 'timeout' ? await spawn.child.exited : (ready?.exitCode ?? 1),
534
+ // A server that traps the TERM and exits 0 still timed out, as a
535
+ // one-shot task that does so reports 143 (X-116).
536
+ exitCode:
537
+ ready?.reason === 'timeout'
538
+ ? (await spawn.child.exited) || signalExitCode('SIGTERM')
539
+ : (ready?.exitCode ?? 1),
535
540
  durationMs: spawn.readyMs(),
536
541
  ...(ready !== undefined ? { notReady: ready.reason } : {}),
537
542
  wallclockStartNs,