@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.
|
|
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.
|
|
25
|
-
"@vzn/vx-linux-arm64": "0.0.
|
|
26
|
-
"@vzn/vx-darwin-x64": "0.0.
|
|
27
|
-
"@vzn/vx-darwin-arm64": "0.0.
|
|
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
|
|
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
|
-
|
|
90
|
-
|
|
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()
|
package/src/cache/tar-stream.ts
CHANGED
|
@@ -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
|
-
|
|
67
|
-
|
|
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
|
|
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
|
-
|
|
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
|
[
|