@vzn/vx 0.0.594 → 0.0.596
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/README.md +1 -1
- package/package.json +5 -5
- package/src/bin.ts +26 -11
- package/src/cache/archive.ts +2 -2
- package/src/cache/cache.ts +235 -174
- package/src/cache/chained-cache.ts +25 -7
- package/src/cache/config-evals.ts +19 -4
- package/src/cache/git-inputs.ts +19 -8
- package/src/cache/index.ts +1 -0
- package/src/cache/inputs.ts +122 -49
- package/src/cache/layer.ts +7 -5
- package/src/cache/layered-cache.ts +25 -2
- package/src/cache/policy.ts +5 -0
- package/src/cache/tar-stream.ts +1 -1
- package/src/cache/zstd.ts +10 -4
- package/src/cli/cache.ts +11 -15
- package/src/cli/completions.ts +42 -6
- package/src/cli/foreign-flags.ts +26 -5
- package/src/cli/help.ts +5 -3
- package/src/cli/init.ts +1 -1
- package/src/cli/last.ts +4 -1
- package/src/cli/plan-format.ts +9 -0
- package/src/cli/plugin-templates.ts +2 -2
- package/src/cli/run.ts +60 -42
- package/src/cli/select.ts +30 -3
- package/src/cli/show.ts +42 -3
- package/src/cli/watch-filter.ts +77 -13
- package/src/cli/watch-fs.ts +15 -7
- package/src/cli/watch-judge.ts +16 -9
- package/src/cli/watch.ts +22 -15
- package/src/config.ts +26 -26
- package/src/exec/env.ts +5 -0
- package/src/exec/index.ts +1 -0
- package/src/exec/proc-sample.ts +6 -2
- package/src/exec/runner.ts +98 -40
- package/src/exec/sandbox-paths.ts +12 -6
- package/src/exec/sandbox-runtime.ts +136 -44
- package/src/exec/sandbox-violations.ts +58 -11
- package/src/graph/index.ts +1 -0
- package/src/graph/scheduler.ts +29 -13
- package/src/graph/task-graph.ts +134 -25
- package/src/index.ts +6 -0
- package/src/orchestrator/admission.ts +12 -3
- package/src/orchestrator/events.ts +10 -1
- package/src/orchestrator/execute-task.ts +65 -20
- package/src/orchestrator/failure-mode.ts +8 -2
- package/src/orchestrator/framed-output.ts +3 -3
- package/src/orchestrator/hit-restore.ts +6 -3
- package/src/orchestrator/local-shortcircuit.ts +4 -1
- package/src/orchestrator/lockfile-claim.ts +43 -46
- package/src/orchestrator/logger.ts +11 -6
- package/src/orchestrator/metrics.ts +19 -8
- package/src/orchestrator/miss-save.ts +2 -2
- package/src/orchestrator/options.ts +7 -0
- package/src/orchestrator/plan.ts +28 -4
- package/src/orchestrator/plugin-host.ts +25 -3
- package/src/orchestrator/plugin.ts +39 -17
- package/src/orchestrator/prepare.ts +13 -4
- package/src/orchestrator/run-artifacts.ts +2 -1
- package/src/orchestrator/run.ts +72 -12
- package/src/orchestrator/sandbox-request.ts +65 -9
- package/src/orchestrator/save-lane.ts +5 -5
- package/src/orchestrator/signals.ts +5 -2
- package/src/orchestrator/summary.ts +16 -5
- package/src/orchestrator/telemetry.ts +16 -7
- package/src/util/index.ts +7 -1
- package/src/util/paths.ts +23 -3
- package/src/util/secret-mask.ts +36 -0
- package/src/util/settle.ts +15 -0
- package/src/workspace/affected.ts +30 -8
- package/src/workspace/config-cache.ts +25 -4
- package/src/workspace/config-imports.ts +8 -6
- package/src/workspace/config-schema.ts +74 -9
- package/src/workspace/filter.ts +19 -0
- package/src/workspace/migrate-scripts.ts +5 -3
- package/src/workspace/migration.ts +1 -1
- package/src/workspace/package-graph.ts +15 -3
- package/src/workspace/project-loader.ts +76 -2
- package/src/workspace/repo-id.ts +42 -4
- package/src/workspace/workspace.ts +98 -20
package/src/config.ts
CHANGED
|
@@ -292,10 +292,10 @@ export interface SandboxConfig {
|
|
|
292
292
|
|
|
293
293
|
/** The denials `sandbox.ignore` can leave out of the report: patterns per class. */
|
|
294
294
|
export interface SandboxIgnore {
|
|
295
|
-
read?: string[]
|
|
296
|
-
write?: string[]
|
|
297
|
-
systemInfo?: string[]
|
|
298
|
-
network?: string[]
|
|
295
|
+
read?: readonly string[]
|
|
296
|
+
write?: readonly string[]
|
|
297
|
+
systemInfo?: readonly string[]
|
|
298
|
+
network?: readonly string[]
|
|
299
299
|
}
|
|
300
300
|
|
|
301
301
|
export interface SandboxGrants {
|
|
@@ -304,29 +304,29 @@ export interface SandboxGrants {
|
|
|
304
304
|
* `node_modules`, NOT `cache.inputs.files`. A task that reads its own
|
|
305
305
|
* source in the sandbox says so here.
|
|
306
306
|
*/
|
|
307
|
-
read?: string[]
|
|
307
|
+
read?: readonly string[]
|
|
308
308
|
/**
|
|
309
309
|
* Writable paths or globs. The task can write NOTHING without these:
|
|
310
310
|
* declaring `cache.outputs.files` grants no write, and a task whose
|
|
311
311
|
* only declaration is its outputs writes into a tmpfs that evaporates
|
|
312
312
|
* (vx then warns that `cache.outputs` matched no files).
|
|
313
313
|
*/
|
|
314
|
-
write?: string[]
|
|
314
|
+
write?: readonly string[]
|
|
315
315
|
/**
|
|
316
316
|
* Reachable domains, `['*.example.com']` a pattern. The proxy is the
|
|
317
317
|
* run's: it filters every sandboxed task against the union of the lists
|
|
318
318
|
* the run's tasks declare, so `true` adds no domain and reaches only
|
|
319
319
|
* those (none in a run with no list). Omitted adds none either.
|
|
320
320
|
*/
|
|
321
|
-
network?: true | string[]
|
|
321
|
+
network?: true | readonly string[]
|
|
322
322
|
/**
|
|
323
323
|
* Kernel information the task may query, by info type — the name that
|
|
324
324
|
* appears in a denial, e.g. `vfs.disk-space` (every Bun and git process
|
|
325
325
|
* probes free space at startup).
|
|
326
326
|
*/
|
|
327
|
-
systemInfo?: string[]
|
|
327
|
+
systemInfo?: readonly string[]
|
|
328
328
|
/** Unix socket paths the task may connect to. `true` allows any. */
|
|
329
|
-
unixSockets?: true | string[]
|
|
329
|
+
unixSockets?: true | readonly string[]
|
|
330
330
|
/**
|
|
331
331
|
* Bind and reach localhost ports — a test that boots a server for
|
|
332
332
|
* itself. A port LIST additionally makes those ports reachable from
|
|
@@ -335,9 +335,9 @@ export interface SandboxGrants {
|
|
|
335
335
|
* namespace, each listed port is bridged out to the host's loopback;
|
|
336
336
|
* on macOS the host already sees them and the list means `true`.
|
|
337
337
|
*/
|
|
338
|
-
localBinding?: boolean | number[]
|
|
338
|
+
localBinding?: boolean | readonly number[]
|
|
339
339
|
/** macOS: Mach service names the task may look up. */
|
|
340
|
-
machLookup?: string[]
|
|
340
|
+
machLookup?: readonly string[]
|
|
341
341
|
/** Acquire a TTY. Rare outside interactive tools. */
|
|
342
342
|
pty?: boolean
|
|
343
343
|
/** Write to `.git/config`. Most build tools should not. */
|
|
@@ -346,7 +346,7 @@ export interface SandboxGrants {
|
|
|
346
346
|
|
|
347
347
|
export interface SandboxDenials {
|
|
348
348
|
/** Domains blocked even if `allow.network` would admit them. */
|
|
349
|
-
network?: string[]
|
|
349
|
+
network?: readonly string[]
|
|
350
350
|
}
|
|
351
351
|
|
|
352
352
|
export interface ExecConfig {
|
|
@@ -458,7 +458,7 @@ export interface ExecEnv {
|
|
|
458
458
|
* folded into the cache key — for things like CI flags, regional
|
|
459
459
|
* vars, secrets that change between machines without affecting output.
|
|
460
460
|
*/
|
|
461
|
-
passThrough?: string[]
|
|
461
|
+
passThrough?: readonly string[]
|
|
462
462
|
/**
|
|
463
463
|
* Explicit `name: value` pairs. The values are set on the child AND
|
|
464
464
|
* folded into the cache key (they are literal in your config, captured
|
|
@@ -471,7 +471,7 @@ export interface ExecEnv {
|
|
|
471
471
|
* hold `TOKEN`, `SECRET`, `KEY`, `PASSWORD`, `PASSWD` or `CREDENTIAL`
|
|
472
472
|
* are masked without it; this reaches `GH_PAT`, `NPM_AUTH`.
|
|
473
473
|
*/
|
|
474
|
-
secret?: string[]
|
|
474
|
+
secret?: readonly string[]
|
|
475
475
|
}
|
|
476
476
|
|
|
477
477
|
export interface CacheConfig {
|
|
@@ -488,7 +488,7 @@ export interface CacheOutputs {
|
|
|
488
488
|
* Outputs are not filtered through gitignore — typical artifact dirs
|
|
489
489
|
* like `dist/` and `coverage/` are captured normally even when ignored.
|
|
490
490
|
*/
|
|
491
|
-
files: string[]
|
|
491
|
+
files: readonly string[]
|
|
492
492
|
/**
|
|
493
493
|
* Files produced OUTSIDE the project dir, as workspace-root-relative
|
|
494
494
|
* globs (the Turbo `$TURBO_ROOT$` / Nx `{workspaceRoot}` escape
|
|
@@ -500,7 +500,7 @@ export interface CacheOutputs {
|
|
|
500
500
|
* like overlapping `files` (vx cleans declared outputs before a run
|
|
501
501
|
* and before a restore, so the second would delete the first's).
|
|
502
502
|
*/
|
|
503
|
-
workspaceFiles?: string[]
|
|
503
|
+
workspaceFiles?: readonly string[]
|
|
504
504
|
}
|
|
505
505
|
|
|
506
506
|
export interface CacheInputs {
|
|
@@ -510,11 +510,11 @@ export interface CacheInputs {
|
|
|
510
510
|
* project files" (gitignore-aware), or narrow as needed. An empty
|
|
511
511
|
* array means no file inputs at all.
|
|
512
512
|
*
|
|
513
|
-
* Declared outputs and any nested-project
|
|
514
|
-
* automatically — a task cannot invalidate itself,
|
|
515
|
-
* across project boundaries.
|
|
513
|
+
* Declared outputs (both `outputs` fields) and any nested-project
|
|
514
|
+
* files are excluded automatically — a task cannot invalidate itself,
|
|
515
|
+
* and cannot read across project boundaries.
|
|
516
516
|
*/
|
|
517
|
-
files: string[]
|
|
517
|
+
files: readonly string[]
|
|
518
518
|
/**
|
|
519
519
|
* Workspace-root-relative globs (the Turbo `$TURBO_ROOT$` / Nx
|
|
520
520
|
* `{workspaceRoot}` equivalent), for inputs that live outside the
|
|
@@ -525,10 +525,10 @@ export interface CacheInputs {
|
|
|
525
525
|
* NO project-boundary rule applies: these globs may match files
|
|
526
526
|
* inside other projects' directories. Deliberate escape hatch —
|
|
527
527
|
* prefer project-relative `files` declarations where possible.
|
|
528
|
-
* Declared `outputs
|
|
529
|
-
* task cannot invalidate itself).
|
|
528
|
+
* Declared outputs (both `outputs` fields) are excluded
|
|
529
|
+
* automatically (a task cannot invalidate itself).
|
|
530
530
|
*/
|
|
531
|
-
workspaceFiles?: string[]
|
|
531
|
+
workspaceFiles?: readonly string[]
|
|
532
532
|
/**
|
|
533
533
|
* Env var names whose runtime values from parent `process.env` are
|
|
534
534
|
* folded into the cache key. **Independent of `exec.env`** — declaring
|
|
@@ -536,7 +536,7 @@ export interface CacheInputs {
|
|
|
536
536
|
* invalidation. To both forward AND track, list the name in both
|
|
537
537
|
* `exec.env.passThrough` and here.
|
|
538
538
|
*/
|
|
539
|
-
env?: string[]
|
|
539
|
+
env?: readonly string[]
|
|
540
540
|
/**
|
|
541
541
|
* Which upstream tasks' cache keys participate in this task's key.
|
|
542
542
|
* Uses the same Turbo/Nx micro-syntax as `dependsOn`, plus
|
|
@@ -565,7 +565,7 @@ export interface CacheInputs {
|
|
|
565
565
|
* lock, the OUTPUT is resolved live every run — correct under
|
|
566
566
|
* `--frozen`. Use for project-specific runtime probes.
|
|
567
567
|
*/
|
|
568
|
-
runtime?: string[]
|
|
568
|
+
runtime?: readonly string[]
|
|
569
569
|
/**
|
|
570
570
|
* Like `runtime`, but commands run at the WORKSPACE ROOT and are
|
|
571
571
|
* deduped GLOBALLY per command across the whole run — a `node -v`
|
|
@@ -573,7 +573,7 @@ export interface CacheInputs {
|
|
|
573
573
|
* analog of `workspaceFiles`: per-task, root-anchored. Use for global
|
|
574
574
|
* tool versions, OS info, etc.
|
|
575
575
|
*/
|
|
576
|
-
workspaceRuntime?: string[]
|
|
576
|
+
workspaceRuntime?: readonly string[]
|
|
577
577
|
}
|
|
578
578
|
|
|
579
579
|
/**
|
package/src/exec/env.ts
CHANGED
|
@@ -33,6 +33,11 @@ export const ESSENTIAL_ENV: readonly string[] = [
|
|
|
33
33
|
'NO_COLOR',
|
|
34
34
|
'CI',
|
|
35
35
|
'NODE_OPTIONS',
|
|
36
|
+
// Where corepack and pnpm keep their own binaries, as HOME is: a machine
|
|
37
|
+
// path, never task output. Without them a corepack shim re-downloads its
|
|
38
|
+
// manager (offline or sandboxed: fails) and pnpm loses its global dir.
|
|
39
|
+
'COREPACK_HOME',
|
|
40
|
+
'PNPM_HOME',
|
|
36
41
|
]
|
|
37
42
|
|
|
38
43
|
/**
|
package/src/exec/index.ts
CHANGED
package/src/exec/proc-sample.ts
CHANGED
|
@@ -26,7 +26,7 @@ interface Proc {
|
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* Linux's `/proc/<pid>/stat` counts CPU in USER_HZ ticks, fixed at 100 by
|
|
29
|
-
* the ABI on every architecture; `tests/proc-sample.test.ts` pins it by
|
|
29
|
+
* the ABI on every architecture; `tests/proc-sample.unsafe.test.ts` pins it by
|
|
30
30
|
* burning a known span and reading it back.
|
|
31
31
|
*/
|
|
32
32
|
const TICK_MS = 10
|
|
@@ -134,12 +134,16 @@ async function psTable(): Promise<Map<number, Proc> | undefined> {
|
|
|
134
134
|
return table
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
/**
|
|
137
|
+
/**
|
|
138
|
+
* `ps`'s `time`: `[[dd-]hh:]mm:ss[.cc]`, macOS printing hundredths after
|
|
139
|
+
* the locale's decimal point — `0:01,50` under a German or French locale.
|
|
140
|
+
*/
|
|
138
141
|
export function psTimeMs(s: string): number | undefined {
|
|
139
142
|
const dash = s.indexOf('-')
|
|
140
143
|
const days = dash === -1 ? 0 : Number(s.slice(0, dash))
|
|
141
144
|
const parts = s
|
|
142
145
|
.slice(dash + 1)
|
|
146
|
+
.replace(',', '.')
|
|
143
147
|
.split(':')
|
|
144
148
|
.map(Number)
|
|
145
149
|
if (parts.length < 2 || parts.some((n) => !Number.isFinite(n)) || !Number.isFinite(days)) {
|
package/src/exec/runner.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
isOutOfFds,
|
|
13
13
|
killGraceMs,
|
|
14
14
|
OUT_OF_FDS_HINT,
|
|
15
|
+
type SecretMask,
|
|
15
16
|
} from '../util/index.js'
|
|
16
17
|
import {
|
|
17
18
|
closeSignalChannel,
|
|
@@ -112,30 +113,49 @@ export function shellQuote(arg: string): string {
|
|
|
112
113
|
}
|
|
113
114
|
|
|
114
115
|
/**
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
* `${#x}` are words), and a newline closes
|
|
116
|
+
* One pass over `command` as sh reads it. `comment`: where a comment still
|
|
117
|
+
* open at the end starts, or -1 — a `#` opens one only unquoted and at the
|
|
118
|
+
* start of a word (`a#b`, `$#` and `${#x}` are words), and a newline closes
|
|
119
|
+
* it. `body`: per line, is it a heredoc's body or terminator (`<<X` /
|
|
120
|
+
* `<<-'X'` / `<<\X` … `X`). One scan, because each needs the other: a
|
|
121
|
+
* `<<X` in a comment or quotes opens no heredoc, and a quote in a body
|
|
122
|
+
* opens nothing (X-110).
|
|
118
123
|
*/
|
|
119
|
-
function
|
|
124
|
+
function shellShape(command: string): { comment: number; body: boolean[] } {
|
|
125
|
+
const lines = command.split('\n')
|
|
126
|
+
const body = lines.map(() => false)
|
|
127
|
+
const pending: Array<{ delim: string; tabs: boolean }> = []
|
|
120
128
|
let quote = ''
|
|
121
129
|
let comment = -1
|
|
122
130
|
let atWordStart = true
|
|
131
|
+
let line = 0
|
|
123
132
|
for (let i = 0; i < command.length; i++) {
|
|
124
133
|
const c = command[i]!
|
|
125
|
-
if (
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
134
|
+
if (c === '\n') {
|
|
135
|
+
line++
|
|
136
|
+
comment = -1
|
|
137
|
+
if (quote !== '') continue
|
|
138
|
+
atWordStart = true
|
|
139
|
+
// A heredoc's body starts on the line after its `<<`.
|
|
140
|
+
while (pending.length > 0 && line < lines.length) {
|
|
141
|
+
const text = lines[line]!
|
|
142
|
+
body[line] = true
|
|
143
|
+
const open = pending[0]!
|
|
144
|
+
if ((open.tabs ? text.replace(/^\t+/, '') : text) === open.delim) pending.shift()
|
|
145
|
+
i += text.length + 1
|
|
146
|
+
line++
|
|
129
147
|
}
|
|
130
148
|
continue
|
|
131
149
|
}
|
|
150
|
+
if (comment >= 0) continue
|
|
132
151
|
if (quote === "'") {
|
|
133
152
|
if (c === "'") quote = ''
|
|
134
153
|
continue
|
|
135
154
|
}
|
|
136
155
|
if (c === '\\') {
|
|
156
|
+
if (command[i + 1] === '\n') line++
|
|
157
|
+
else atWordStart = false
|
|
137
158
|
i++
|
|
138
|
-
atWordStart = false
|
|
139
159
|
continue
|
|
140
160
|
}
|
|
141
161
|
if (quote === '"') {
|
|
@@ -151,25 +171,18 @@ function trailingCommentStart(command: string): number {
|
|
|
151
171
|
comment = i
|
|
152
172
|
continue
|
|
153
173
|
}
|
|
174
|
+
if (c === '<' && command[i - 1] !== '<') {
|
|
175
|
+
const m = /^<<(-?)[ \t]*\\?(['"]?)([A-Za-z_]\w*)\2/.exec(command.slice(i, i + 256))
|
|
176
|
+
if (m !== null) {
|
|
177
|
+
pending.push({ delim: m[3]!, tabs: m[1] === '-' })
|
|
178
|
+
i += m[0].length - 1
|
|
179
|
+
atWordStart = false
|
|
180
|
+
continue
|
|
181
|
+
}
|
|
182
|
+
}
|
|
154
183
|
atWordStart = /[\s;&|()<>]/.test(c)
|
|
155
184
|
}
|
|
156
|
-
return comment
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/** Per line: is it a heredoc's body or terminator (`<<X` / `<<-'X'` … `X`)? */
|
|
160
|
-
function heredocLines(lines: readonly string[]): boolean[] {
|
|
161
|
-
const pending: Array<{ delim: string; tabs: boolean }> = []
|
|
162
|
-
return lines.map((line) => {
|
|
163
|
-
const open = pending[0]
|
|
164
|
-
if (open !== undefined) {
|
|
165
|
-
if ((open.tabs ? line.replace(/^\t+/, '') : line) === open.delim) pending.shift()
|
|
166
|
-
return true
|
|
167
|
-
}
|
|
168
|
-
for (const m of line.matchAll(/(?<!<)<<(?!<)(-?)\s*(['"]?)([A-Za-z_]\w*)\2/g)) {
|
|
169
|
-
pending.push({ delim: m[3]!, tabs: m[1] === '-' })
|
|
170
|
-
}
|
|
171
|
-
return false
|
|
172
|
-
})
|
|
185
|
+
return { comment, body }
|
|
173
186
|
}
|
|
174
187
|
|
|
175
188
|
/**
|
|
@@ -183,27 +196,34 @@ export function withForwardArgs(command: string, args: readonly string[] | undef
|
|
|
183
196
|
// `X FWD` closed nothing and the args never reached the command (X-12).
|
|
184
197
|
// They go on the last line that is a command, where `cat <<X FWD` is
|
|
185
198
|
// still `cat FWD` reading the heredoc.
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
199
|
+
const trimmed = command.trimEnd()
|
|
200
|
+
const lines = trimmed.split('\n')
|
|
201
|
+
const shape = shellShape(trimmed)
|
|
202
|
+
if (shape.body[lines.length - 1] === true) {
|
|
203
|
+
const last = shape.body.lastIndexOf(false)
|
|
190
204
|
const tail = lines.slice(last + 1).join('\n')
|
|
191
205
|
return `${withForwardArgs(lines.slice(0, last + 1).join('\n'), args)}\n${tail}`
|
|
192
206
|
}
|
|
193
207
|
const quoted = args.map(shellQuote).join(' ')
|
|
194
|
-
|
|
208
|
+
// Of the trimmed command: the newline closing a template literal's last
|
|
209
|
+
// line closed its comment too, and `echo args: # c\n` took the args into
|
|
210
|
+
// the comment (X-110).
|
|
211
|
+
let comment = shape.comment
|
|
195
212
|
if (comment < 0) {
|
|
196
213
|
// A command that ends in a newline (a template literal's closing line)
|
|
197
214
|
// ran the args as a command of their own: `--watch: not found`. Kept
|
|
198
215
|
// when an odd run of backslashes ends it: that escapes what follows.
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
return `${escaped ? command : body} ${quoted}`
|
|
216
|
+
const escaped = (/\\+$/.exec(trimmed)?.[0].length ?? 0) % 2 === 1
|
|
217
|
+
return `${escaped ? command : trimmed} ${quoted}`
|
|
202
218
|
}
|
|
203
219
|
// A comment-only last line after a commented line: the args go before
|
|
204
220
|
// the earliest comment, or they land inside it (`echo one # c\n# two`).
|
|
205
|
-
for (let c = comment; c >= 0; c =
|
|
206
|
-
|
|
221
|
+
for (let c = comment; c >= 0; c = shellShape(trimmed.slice(0, c).trimEnd()).comment) comment = c
|
|
222
|
+
// What precedes the comment may end in a heredoc: the args go where they
|
|
223
|
+
// would with no comment, never on its terminator.
|
|
224
|
+
const head = trimmed.slice(0, comment).trimEnd()
|
|
225
|
+
const gap = trimmed.slice(head.length, comment) || ' '
|
|
226
|
+
return `${withForwardArgs(head, args)}${gap}${trimmed.slice(comment)}`
|
|
207
227
|
}
|
|
208
228
|
|
|
209
229
|
// Any shell control/expansion character means the wrapping `sh` has real
|
|
@@ -348,6 +368,18 @@ const SIGNAL_ALIASES = new Set(['SIGIOT', 'SIGPOLL', 'SIGCLD'])
|
|
|
348
368
|
*/
|
|
349
369
|
const TIMEOUT_SIGKILL_GRACE_MS = 2000
|
|
350
370
|
|
|
371
|
+
/**
|
|
372
|
+
* Run `fn` one loop turn later, once the child exits already pending are
|
|
373
|
+
* read. A deadline due in the same turn as a child's exit fires BEFORE Bun
|
|
374
|
+
* reaps it: a task that exited 0 inside its timeout, seen late by a busy vx,
|
|
375
|
+
* was SIGTERMed as a zombie and failed as timed out, and a server that died
|
|
376
|
+
* before ready read as a readiness timeout (2026-10-08). A turn later the
|
|
377
|
+
* exit has settled the caller, which clears the returned timer.
|
|
378
|
+
*/
|
|
379
|
+
function afterPendingExits(fn: () => void): ReturnType<typeof setTimeout> {
|
|
380
|
+
return setTimeout(fn, 0)
|
|
381
|
+
}
|
|
382
|
+
|
|
351
383
|
/**
|
|
352
384
|
* Arm a SIGTERM timeout on a spawned child. Returns a handle whose
|
|
353
385
|
* `timedOut()` reports whether the timer fired — so the caller can
|
|
@@ -368,7 +400,10 @@ export function armTimeout(
|
|
|
368
400
|
let firedAt: number | undefined
|
|
369
401
|
let killTimer: ReturnType<typeof setTimeout> | undefined
|
|
370
402
|
const graceMs = killGraceMs(TIMEOUT_SIGKILL_GRACE_MS)
|
|
371
|
-
|
|
403
|
+
let timer = setTimeout(() => {
|
|
404
|
+
timer = afterPendingExits(fire)
|
|
405
|
+
}, timeoutMs)
|
|
406
|
+
function fire(): void {
|
|
372
407
|
firedAt = Date.now()
|
|
373
408
|
killTree(proc, 'SIGTERM')
|
|
374
409
|
// Escalate to SIGKILL after a grace: a child that TRAPS+IGNORES SIGTERM
|
|
@@ -379,7 +414,7 @@ export function armTimeout(
|
|
|
379
414
|
// the CLI alive.
|
|
380
415
|
killTimer = setTimeout(() => killTree(proc, 'SIGKILL'), graceMs)
|
|
381
416
|
killTimer.unref?.()
|
|
382
|
-
}
|
|
417
|
+
}
|
|
383
418
|
return {
|
|
384
419
|
timedOut: () => firedAt !== undefined,
|
|
385
420
|
settle: async () => {
|
|
@@ -680,7 +715,7 @@ export function runPersistent(opts: PersistentOptions): PersistentSpawn {
|
|
|
680
715
|
// a healthy server is never killed by a stale timer.
|
|
681
716
|
let readyTimer: ReturnType<typeof setTimeout> | undefined
|
|
682
717
|
if (readyRe && opts.timeoutMs !== undefined) {
|
|
683
|
-
|
|
718
|
+
const giveUp = (): void => {
|
|
684
719
|
if (readyAt === undefined) {
|
|
685
720
|
gaveUpAt = Date.now()
|
|
686
721
|
rejectReady(
|
|
@@ -705,6 +740,9 @@ export function runPersistent(opts: PersistentOptions): PersistentSpawn {
|
|
|
705
740
|
}, killGraceMs(TIMEOUT_SIGKILL_GRACE_MS))
|
|
706
741
|
killTimer.unref?.()
|
|
707
742
|
}
|
|
743
|
+
}
|
|
744
|
+
readyTimer = setTimeout(() => {
|
|
745
|
+
readyTimer = afterPendingExits(giveUp)
|
|
708
746
|
}, opts.timeoutMs)
|
|
709
747
|
}
|
|
710
748
|
|
|
@@ -883,6 +921,23 @@ export function droppedOutputLine(dropped: number): string {
|
|
|
883
921
|
)
|
|
884
922
|
}
|
|
885
923
|
|
|
924
|
+
/**
|
|
925
|
+
* A captured `text` with `secrets` masked. A value the dropped middle cut
|
|
926
|
+
* left a piece at the head's end and one at the tail's start, which the
|
|
927
|
+
* whole-value mask missed, and the cache entry stored and replayed both.
|
|
928
|
+
*/
|
|
929
|
+
export function maskCaptured(text: string, secrets: SecretMask): string {
|
|
930
|
+
const at = text.indexOf('\n[vx] ', CAPTURE_HEAD_CHARS - 1)
|
|
931
|
+
const end = at === -1 ? -1 : text.indexOf('\n', at + 1) + 1
|
|
932
|
+
if (at === -1 || at > CAPTURE_HEAD_CHARS || !DROPPED_LINE.test(text.slice(at, end))) {
|
|
933
|
+
return secrets.mask(text)
|
|
934
|
+
}
|
|
935
|
+
const [head, tail] = secrets.maskCut(text.slice(0, at), text.slice(end))
|
|
936
|
+
return head + text.slice(at, end) + tail
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
const DROPPED_LINE = /^\n\[vx\] [\d.]+ MiB of output not kept — /
|
|
940
|
+
|
|
886
941
|
const isHighSurrogate = (code: number): boolean => code >= 0xd800 && code <= 0xdbff
|
|
887
942
|
|
|
888
943
|
/**
|
|
@@ -897,7 +952,10 @@ class BoundedCapture {
|
|
|
897
952
|
private dropped = 0
|
|
898
953
|
|
|
899
954
|
push(chunk: string): void {
|
|
900
|
-
|
|
955
|
+
// Once the tail has begun the head is closed, even a unit short of its
|
|
956
|
+
// bound (the surrogate case below): a later short chunk fit there and
|
|
957
|
+
// was retained ahead of the chunk before it.
|
|
958
|
+
if (this.tailLen === 0 && this.head.length < CAPTURE_HEAD_CHARS) {
|
|
901
959
|
let room = CAPTURE_HEAD_CHARS - this.head.length
|
|
902
960
|
if (chunk.length <= room) {
|
|
903
961
|
this.head += chunk
|
|
@@ -78,14 +78,16 @@ export function localBindingOn(c: { localBinding?: boolean | readonly number[] }
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
/**
|
|
81
|
-
* Whether a sandboxed task may read `file`: it lies under
|
|
82
|
-
*
|
|
83
|
-
* grant
|
|
84
|
-
*
|
|
81
|
+
* Whether a sandboxed task may read `file`: it lies under no denial (the
|
|
82
|
+
* rest of the host is mounted read-only), or under a read grant, a write
|
|
83
|
+
* grant or a baseline read, by its canonical path. A path no grant holds
|
|
84
|
+
* under a denial is not there inside the sandbox, which the shell reports
|
|
85
|
+
* as "not found": a PATH search is a stat, which the trace does not stop on.
|
|
85
86
|
*/
|
|
86
87
|
export function sandboxReads(
|
|
87
88
|
sandbox: {
|
|
88
89
|
readonly baseAllowRead: readonly string[]
|
|
90
|
+
readonly baseDenyRead: readonly string[]
|
|
89
91
|
readonly config: {
|
|
90
92
|
readonly allowRead: readonly string[]
|
|
91
93
|
readonly allowWrite: readonly string[]
|
|
@@ -94,7 +96,11 @@ export function sandboxReads(
|
|
|
94
96
|
file: string,
|
|
95
97
|
): boolean {
|
|
96
98
|
const real = toRealPath(file)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
+
const under = (g: string): boolean => atOrUnder(real, toRealPath(g))
|
|
100
|
+
return (
|
|
101
|
+
!sandbox.baseDenyRead.some(under) ||
|
|
102
|
+
[...sandbox.baseAllowRead, ...sandbox.config.allowRead, ...sandbox.config.allowWrite].some(
|
|
103
|
+
under,
|
|
104
|
+
)
|
|
99
105
|
)
|
|
100
106
|
}
|