@vzn/vx 0.0.635 → 0.0.637
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 +47 -19
- package/docs/caching.md +222 -210
- package/docs/cli.md +699 -664
- package/docs/execution.md +67 -75
- package/docs/features.md +44 -40
- package/docs/patterns.md +31 -32
- package/docs/schema.md +418 -202
- package/docs/security.md +76 -39
- package/launcher.sh +43 -0
- package/package.json +5 -5
- package/schemas/cache.json +41 -19
- package/schemas/info.json +23 -11
- package/schemas/last.json +48 -2
- package/schemas/lock.json +1 -1
- package/schemas/plan.json +2 -2
- package/schemas/show.json +13 -4
- package/schemas/why.json +15 -1
- package/skills/vx/SKILL.md +16 -12
- package/src/bin.ts +17 -2
- package/src/cache/archive.ts +11 -0
- package/src/cache/cache.ts +105 -53
- package/src/cache/git-inputs.ts +277 -63
- package/src/cache/index.ts +7 -1
- package/src/cache/inputs.ts +125 -35
- package/src/cache/key-fold.ts +8 -7
- package/src/cache/layer.ts +8 -17
- package/src/cache/layered-cache.ts +16 -102
- package/src/cache/policy.ts +25 -29
- package/src/cache/run-history.ts +36 -14
- package/src/cache/schema.ts +21 -23
- package/src/cache/scoped-cache.ts +224 -0
- package/src/cli/cache.ts +150 -28
- package/src/cli/completions.ts +2 -2
- package/src/cli/core-alias.ts +1 -1
- package/src/cli/foreign-flags.ts +13 -12
- package/src/cli/help.ts +94 -61
- package/src/cli/index.ts +42 -13
- package/src/cli/info.ts +38 -23
- package/src/cli/init.ts +115 -406
- package/src/cli/last.ts +145 -4
- package/src/cli/lock.ts +333 -111
- package/src/cli/plan-format.ts +2 -2
- package/src/cli/plugin-commands.ts +5 -9
- package/src/cli/plugin-templates.ts +10 -10
- package/src/cli/run.ts +123 -144
- package/src/cli/select.ts +33 -39
- package/src/cli/show.ts +17 -19
- package/src/cli/task-verb.ts +2 -9
- package/src/cli/upgrade.ts +402 -145
- package/src/cli/watch-cycle.ts +9 -2
- package/src/cli/watch-filter.ts +33 -60
- package/src/cli/watch-fs.ts +29 -16
- package/src/cli/watch-judge.ts +12 -3
- package/src/cli/watch-set.ts +16 -42
- package/src/cli/watch.ts +188 -215
- package/src/cli/why.ts +28 -17
- package/src/cli/workspace-config.ts +31 -68
- package/src/config.ts +191 -71
- package/src/exec/env.ts +1 -31
- package/src/exec/executor.ts +1 -5
- package/src/exec/kill-tree.ts +2 -1
- package/src/exec/sandbox-runtime.ts +12 -1
- package/src/exec/sandbox-violations.ts +9 -1
- package/src/graph/dependency-spec.ts +1 -1
- package/src/graph/index.ts +1 -0
- package/src/graph/priorities.ts +1 -1
- package/src/graph/scheduler.ts +19 -7
- package/src/graph/task-graph.ts +44 -20
- package/src/handoff.ts +169 -0
- package/src/index.ts +25 -57
- package/src/orchestrator/affected-tasks.ts +7 -5
- package/src/orchestrator/command-windows.ts +44 -0
- package/src/orchestrator/deferred-outputs.ts +12 -10
- package/src/orchestrator/doctor.ts +98 -110
- package/src/orchestrator/download-policy.ts +3 -3
- package/src/orchestrator/events.ts +19 -3
- package/src/orchestrator/excluded-keys.ts +0 -2
- package/src/orchestrator/execute-task.ts +192 -70
- package/src/orchestrator/fingerprint-watch.ts +18 -15
- package/src/orchestrator/forecast.ts +10 -2
- package/src/orchestrator/framed-output.ts +8 -3
- package/src/orchestrator/graph-stage.ts +5 -5
- package/src/orchestrator/history.ts +70 -30
- package/src/orchestrator/hit-restore.ts +15 -3
- package/src/orchestrator/index.ts +18 -4
- package/src/orchestrator/local-shortcircuit.ts +2 -1
- package/src/orchestrator/lockfile-claim.ts +79 -20
- package/src/orchestrator/logger.ts +71 -39
- package/src/orchestrator/metrics.ts +71 -6
- package/src/orchestrator/miss-save.ts +3 -0
- package/src/orchestrator/options.ts +43 -28
- package/src/orchestrator/output-host.ts +72 -0
- package/src/orchestrator/plan.ts +6 -8
- package/src/orchestrator/plugin-host.ts +87 -63
- package/src/orchestrator/plugin.ts +94 -60
- package/src/orchestrator/prepare.ts +114 -73
- package/src/orchestrator/projects.ts +102 -99
- package/src/orchestrator/remote-prefetch.ts +2 -1
- package/src/orchestrator/run-context.ts +25 -6
- package/src/orchestrator/run-failures.ts +17 -8
- package/src/orchestrator/run-lock.ts +1 -1
- package/src/orchestrator/run-report.ts +9 -9
- package/src/orchestrator/run-settings.ts +48 -0
- package/src/orchestrator/run.ts +127 -88
- package/src/orchestrator/sandbox-fix.ts +39 -20
- package/src/orchestrator/sandbox-request.ts +84 -13
- package/src/orchestrator/signals.ts +1 -1
- package/src/orchestrator/stable-keys.ts +13 -11
- package/src/orchestrator/status-line.ts +48 -6
- package/src/orchestrator/summary.ts +46 -62
- package/src/orchestrator/task-hash.ts +12 -83
- package/src/orchestrator/telemetry.ts +3 -3
- package/src/settings.ts +417 -0
- package/src/util/errors.ts +1 -1
- package/src/util/essential-env.ts +31 -0
- package/src/util/hide-env.ts +107 -0
- package/src/util/index.ts +8 -2
- package/src/util/paths.ts +10 -0
- package/src/util/settle.ts +49 -30
- package/src/util/size.ts +23 -4
- package/src/util/timing.ts +3 -2
- package/src/util/ulid.ts +1 -1
- package/src/util/verbs.ts +0 -15
- package/src/workspace/affected.ts +77 -222
- package/src/workspace/config-eval.ts +15 -20
- package/src/workspace/config-imports.ts +78 -3
- package/src/workspace/config-schema.ts +251 -241
- package/src/workspace/dependency-rules.ts +57 -0
- package/src/workspace/filter.ts +2 -2
- package/src/workspace/fingerprint.ts +11 -121
- package/src/workspace/index.ts +22 -27
- package/src/workspace/load-reads.ts +4 -4
- package/src/workspace/lockfile.ts +96 -140
- package/src/workspace/migration.ts +242 -146
- package/src/workspace/package-graph.ts +30 -216
- package/src/workspace/project-loader.ts +80 -285
- package/src/workspace/repo-id.ts +2 -1
- package/src/workspace/workspace.ts +362 -831
- package/src/cache/config-evals.ts +0 -130
- package/src/workspace/config-cache.ts +0 -661
- package/src/workspace/migrate-scripts.ts +0 -1121
package/src/orchestrator/run.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// so the layers can be swapped without touching the others.
|
|
4
4
|
|
|
5
5
|
import type { ProjectEntry } from '../workspace/index.js'
|
|
6
|
-
import {
|
|
6
|
+
import { inputStabilityEnv, LOCK_INVALID, lockEntryBroken } from '../workspace/index.js'
|
|
7
7
|
import { realpathSync } from 'node:fs'
|
|
8
8
|
import path from 'node:path'
|
|
9
9
|
import {
|
|
@@ -37,8 +37,6 @@ import {
|
|
|
37
37
|
stageTimes,
|
|
38
38
|
killGraceMs,
|
|
39
39
|
MAX_TIMEOUT_MS,
|
|
40
|
-
parseDuration,
|
|
41
|
-
parseSize,
|
|
42
40
|
printTimings,
|
|
43
41
|
ulid,
|
|
44
42
|
isLiteralPattern,
|
|
@@ -49,10 +47,11 @@ import {
|
|
|
49
47
|
teardownTimeoutMs,
|
|
50
48
|
secretMask,
|
|
51
49
|
isFsRefusal,
|
|
50
|
+
isUserError,
|
|
52
51
|
} from '../util/index.js'
|
|
53
52
|
import { keyedProjects } from './keyed-projects.js'
|
|
54
53
|
import { isDefaultBuild } from './projects.js'
|
|
55
|
-
import { prepareSandbox } from './sandbox-request.js'
|
|
54
|
+
import { commandWriteReach, prepareSandbox } from './sandbox-request.js'
|
|
56
55
|
import type { OutputDirSnapshot, SaveFacts } from './miss-save.js'
|
|
57
56
|
import { admitTasks, taintTracker } from './admission.js'
|
|
58
57
|
import { unkeyedGroupOutcome, type ExecuteArgs } from './execute-task.js'
|
|
@@ -62,6 +61,7 @@ import { busLogger, createEventBus, terminalSubscriber, type EventBus } from './
|
|
|
62
61
|
import { installPlugins, PluginSetupError } from './plugin.js'
|
|
63
62
|
import { buildAdmission, executorLabel, resolveExecutors, teardownPlugins } from './plugin-host.js'
|
|
64
63
|
import { subscribeTelemetry, type TelemetryHandle } from './telemetry-host.js'
|
|
64
|
+
import { claimOutput, guardedOutput } from './output-host.js'
|
|
65
65
|
import { assembleRunSummary, isPassStatus } from './telemetry.js'
|
|
66
66
|
import type { RunContextRecord } from './telemetry.js'
|
|
67
67
|
import { defaultLogger, type DefaultLogger, resolveOutputView, type Logger } from './logger.js'
|
|
@@ -121,10 +121,13 @@ import {
|
|
|
121
121
|
type RunSummaryJson,
|
|
122
122
|
} from './run-artifacts.js'
|
|
123
123
|
import { createSaveLane } from './save-lane.js'
|
|
124
|
+
import { commandWindows } from './command-windows.js'
|
|
124
125
|
import { formatOutcomeTable, formatRunSummary } from './summary.js'
|
|
125
126
|
import { detectFlaky, type FlakyCandidate, type FlakyFinding } from './failure-mode.js'
|
|
126
127
|
import { createMissExplainer } from './miss-reason.js'
|
|
127
128
|
import type { HeldServer, RunOptions, RunSummary } from './options.js'
|
|
129
|
+
import { withRunSettings } from './run-settings.js'
|
|
130
|
+
import { resolveSetting } from '../settings.js'
|
|
128
131
|
|
|
129
132
|
// Per run, never shared: a `vx watch` process runs many, and a shared map
|
|
130
133
|
// is one `preProbed.set` away from leaking a hit across cycles.
|
|
@@ -135,28 +138,6 @@ const emptyShortCircuit = (): ShortCircuit => ({
|
|
|
135
138
|
groupKeys: new Map(),
|
|
136
139
|
})
|
|
137
140
|
|
|
138
|
-
/**
|
|
139
|
-
* Parse the `VX_TASK_TIMEOUT` env var (ms) — the "global" run-level task
|
|
140
|
-
* timeout default. A missing/empty/non-positive-integer value yields
|
|
141
|
-
* `undefined` (ignored), so a typo never silently disables a task's own
|
|
142
|
-
* `exec.timeout`. A value past `MAX_TIMEOUT_MS` is clamped to it — see below.
|
|
143
|
-
*
|
|
144
|
-
* Exported for `tests/options-resolve.test.ts`, which pins every accepted
|
|
145
|
-
* and ignored form of this rung; it has no other caller outside this file.
|
|
146
|
-
*/
|
|
147
|
-
export function readTaskTimeoutEnv(): number | undefined {
|
|
148
|
-
const raw = process.env['VX_TASK_TIMEOUT']
|
|
149
|
-
if (raw === undefined || raw === '') return undefined
|
|
150
|
-
const n = Number(raw)
|
|
151
|
-
if (!Number.isInteger(n) || n <= 0) return undefined
|
|
152
|
-
// CLAMPED, not refused — unlike `exec.timeout` and `--timeout`, this rung's
|
|
153
|
-
// contract is already "unusable value → fall back", so it never throws. But
|
|
154
|
-
// falling back is wrong here: someone who typed a huge number wants a long
|
|
155
|
-
// timeout, and the largest expressible one IS long (~24.8 days). Handing it
|
|
156
|
-
// to `setTimeout` unbounded would mean 1 ms — killing every task instantly.
|
|
157
|
-
return Math.min(n, MAX_TIMEOUT_MS)
|
|
158
|
-
}
|
|
159
|
-
|
|
160
141
|
/**
|
|
161
142
|
* Compact the 4-axis cache policy into the `invocations.cache_policy`
|
|
162
143
|
* column string. Each enabled axis contributes its flag; a fully-on
|
|
@@ -177,7 +158,7 @@ function compactCachePolicy(p: CachePolicy): string {
|
|
|
177
158
|
* local cache writes NOWHERE, yet the write axis reads as on. Normalising
|
|
178
159
|
* here — the ONE place both `run()` and `planRun()` derive it from — is
|
|
179
160
|
* what keeps `--dry` describing the run you are about to get: reading the
|
|
180
|
-
* raw request made the plan label `--cache=local:,remote:rw` "cache miss —
|
|
161
|
+
* raw request made the plan label `--cache-policy=local:,remote:rw` "cache miss —
|
|
181
162
|
* would exec" (a result that will be stored) for a run in which caching is
|
|
182
163
|
* entirely off.
|
|
183
164
|
*/
|
|
@@ -196,7 +177,7 @@ function effectiveCachePolicy(requested: CachePolicy, hasRemoteLayer: boolean):
|
|
|
196
177
|
* ingests hits into local for execute-task's lazy probe. The gate asks
|
|
197
178
|
* the layer (`hasRemote`), not `instanceof LayeredCache` — a
|
|
198
179
|
* third-party remote layer stalls on the classify just as hard.
|
|
199
|
-
* - the policy reads locally (a `--no-cache`/`--force`/`--cache=local:`
|
|
180
|
+
* - the policy reads locally (a `--no-cache`/`--force`/`--cache-policy=local:`
|
|
200
181
|
* run reads nothing locally → nothing to restore → skip);
|
|
201
182
|
* - there is at least one task.
|
|
202
183
|
* A flat graph has no ordering to bypass, but the classify pass is worth
|
|
@@ -289,6 +270,12 @@ function refuseRunOptions(options: RunOptions): void {
|
|
|
289
270
|
oneOf('outputLogs', options.outputLogs, ['full', 'errors-only', 'none', 'hash-only'])
|
|
290
271
|
oneOf('download', options.download, ['all', 'toplevel', 'none'])
|
|
291
272
|
oneOf('flow', options.flow, ['focused', 'broad'])
|
|
273
|
+
oneOf('schedule', options.schedule, [
|
|
274
|
+
'most-work',
|
|
275
|
+
'critical-path',
|
|
276
|
+
'direct-dependents',
|
|
277
|
+
'ready-order',
|
|
278
|
+
])
|
|
292
279
|
const exclude = options.excludeDependencies
|
|
293
280
|
if (exclude !== undefined && exclude !== 'all' && !names(exclude))
|
|
294
281
|
refuse('excludeDependencies', exclude, "'all' or an array of task names")
|
|
@@ -299,11 +286,15 @@ function refuseRunOptions(options: RunOptions): void {
|
|
|
299
286
|
refuse('concurrency', concurrency, 'a positive integer')
|
|
300
287
|
if (retries !== undefined && !(Number.isInteger(retries) && retries >= 0))
|
|
301
288
|
refuse('retries', retries, 'a non-negative integer')
|
|
302
|
-
|
|
303
|
-
timeout
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
289
|
+
for (const [name, ms] of [
|
|
290
|
+
['timeout', timeout],
|
|
291
|
+
['killGrace', options.killGrace],
|
|
292
|
+
['teardownTimeout', options.teardownTimeout],
|
|
293
|
+
['readyNotice', options.readyNotice],
|
|
294
|
+
] as const) {
|
|
295
|
+
if (ms !== undefined && !(Number.isInteger(ms) && ms > 0 && ms <= MAX_TIMEOUT_MS))
|
|
296
|
+
refuse(name, ms, `a positive integer of ms, at most ${MAX_TIMEOUT_MS}`)
|
|
297
|
+
}
|
|
307
298
|
}
|
|
308
299
|
|
|
309
300
|
export async function run(options: RunOptions): Promise<RunSummary> {
|
|
@@ -332,6 +323,7 @@ export async function run(options: RunOptions): Promise<RunSummary> {
|
|
|
332
323
|
coalesce: true,
|
|
333
324
|
...(options.tty === true ? { tty: true } : {}),
|
|
334
325
|
...(options.forwardArgs !== undefined ? { forwardArgs: options.forwardArgs } : {}),
|
|
326
|
+
...(options.footer === false ? { footer: false } : {}),
|
|
335
327
|
},
|
|
336
328
|
)
|
|
337
329
|
: null
|
|
@@ -344,16 +336,26 @@ export async function run(options: RunOptions): Promise<RunSummary> {
|
|
|
344
336
|
// Tasks run with colour forced (exec/env.ts); where the terminal prints
|
|
345
337
|
// plain, their escapes are stripped. A custom logger gets the task's
|
|
346
338
|
// bytes as written, as the cache holds them: it formats its own output.
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
)
|
|
339
|
+
const render = terminalSubscriber(terminal === null || colors.enabled ? sink : plainOutput(sink))
|
|
340
|
+
let unsubscribeTerminal = bus.subscribe(render)
|
|
350
341
|
const detach = (): void => {
|
|
351
342
|
unsubscribeTerminal()
|
|
352
343
|
terminal?.settle()
|
|
353
344
|
}
|
|
345
|
+
// A plugin's `output` renders the run from its start; core's renderer
|
|
346
|
+
// stays the fallback. Only core's own renderer is replaceable: an
|
|
347
|
+
// embedder's `log` is never consulted away.
|
|
348
|
+
const takeOutput =
|
|
349
|
+
terminal === null
|
|
350
|
+
? undefined
|
|
351
|
+
: (claimed: { logger: Logger; owner: string }): void => {
|
|
352
|
+
terminal.settle()
|
|
353
|
+
unsubscribeTerminal()
|
|
354
|
+
unsubscribeTerminal = bus.subscribe(guardedOutput(claimed, render))
|
|
355
|
+
}
|
|
354
356
|
let held = false
|
|
355
357
|
try {
|
|
356
|
-
const summary = await runOnBus(options, bus, colors, terminal)
|
|
358
|
+
const summary = await runOnBus(options, bus, colors, terminal, takeOutput)
|
|
357
359
|
if (summary.persistent === undefined) return summary
|
|
358
360
|
// Servers handed back still running still write: `vx watch dev` printed
|
|
359
361
|
// none of its server's log while it idled, the renderer gone with this
|
|
@@ -377,6 +379,7 @@ async function runOnBus(
|
|
|
377
379
|
bus: EventBus,
|
|
378
380
|
colors: ColorSupport,
|
|
379
381
|
terminal: DefaultLogger | null,
|
|
382
|
+
takeOutput?: (claimed: { logger: Logger; owner: string }) => void,
|
|
380
383
|
): Promise<RunSummary> {
|
|
381
384
|
const log = busLogger(bus)
|
|
382
385
|
// A server's output goes through a cell of its own: a later watch cycle
|
|
@@ -393,6 +396,7 @@ async function runOnBus(
|
|
|
393
396
|
}
|
|
394
397
|
|
|
395
398
|
const prepared = await prepareRun(options, log)
|
|
399
|
+
options = withRunSettings(options, prepared.workspaceConfig)
|
|
396
400
|
mark('plugin stages')
|
|
397
401
|
// Every exit from here tears the plugins down: their capability
|
|
398
402
|
// factories ran in prepareRun, and teardown is what releases what those
|
|
@@ -494,14 +498,13 @@ async function runOnBus(
|
|
|
494
498
|
cacheDir,
|
|
495
499
|
cache,
|
|
496
500
|
nodes,
|
|
497
|
-
workspaceFingerprint,
|
|
498
501
|
fingerprintWatch,
|
|
499
502
|
nestedDirsByProject,
|
|
500
503
|
gitFilesCache,
|
|
501
504
|
hashCache,
|
|
502
505
|
workspaceProjectCount,
|
|
503
506
|
} = prepared
|
|
504
|
-
const concurrency = options.concurrency ??
|
|
507
|
+
const concurrency = options.concurrency ?? machineParallelism()
|
|
505
508
|
// An earlier cycle's servers: the ones this graph starts again unchanged
|
|
506
509
|
// stay up; the others go now, before anything binds their ports. A stop
|
|
507
510
|
// that lands meanwhile ends the run here, having run nothing: an aborted
|
|
@@ -593,7 +596,7 @@ async function runOnBus(
|
|
|
593
596
|
}
|
|
594
597
|
}
|
|
595
598
|
|
|
596
|
-
//
|
|
599
|
+
// `cache.download` (default `all`) decides ONCE per task, here, whether a
|
|
597
600
|
// remote execution's outputs come home.
|
|
598
601
|
const downloadPolicy = options.download ?? 'all'
|
|
599
602
|
const localPlaced = locallyPlaced(placements, nodes.keys())
|
|
@@ -604,12 +607,19 @@ async function runOnBus(
|
|
|
604
607
|
remoteOnly: placements.remoteOnly,
|
|
605
608
|
})
|
|
606
609
|
|
|
607
|
-
//
|
|
608
|
-
|
|
609
|
-
//
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
610
|
+
// `run.timeout` (ms), for any task WITHOUT its own `exec.timeout`.
|
|
611
|
+
const taskTimeoutDefault = options.timeout
|
|
612
|
+
// Read once, before any task: a bad value is refused here, not mid-run.
|
|
613
|
+
const inputStability = {
|
|
614
|
+
run: inputStabilityEnv(),
|
|
615
|
+
...(workspaceConfig?.cache?.inputStability !== undefined
|
|
616
|
+
? { workspace: workspaceConfig.cache.inputStability }
|
|
617
|
+
: {}),
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const cleanOutputs = resolveSetting('cache.outputs.clean', workspaceConfig).value as
|
|
621
|
+
| boolean
|
|
622
|
+
| undefined
|
|
613
623
|
|
|
614
624
|
// Run-scoped registries of live subprocesses:
|
|
615
625
|
// - `liveChildren`: in-flight children. The runner adds/removes
|
|
@@ -698,7 +708,7 @@ async function runOnBus(
|
|
|
698
708
|
// bumps are flushed, so a throw between opening the cache and the
|
|
699
709
|
// normal close leaked the SQLite handle (it matters in a long-lived
|
|
700
710
|
// host) AND lost this run's touch record, after which an LRU
|
|
701
|
-
// `vx cache
|
|
711
|
+
// `vx cache clean` can evict entries the run just hit. Once-only
|
|
702
712
|
// because the normal path closes before the persistent-task wait —
|
|
703
713
|
// holding the handle open for a dev server's whole lifetime would be
|
|
704
714
|
// worse — and because close() re-runs its retention DELETEs.
|
|
@@ -739,7 +749,7 @@ async function runOnBus(
|
|
|
739
749
|
const policy: CachePolicy = effectiveCachePolicy(prepared.cachePolicy, prepared.hasRemoteLayer)
|
|
740
750
|
if (options.remoteRequested === true && !prepared.hasRemoteLayer) {
|
|
741
751
|
log.status(
|
|
742
|
-
'vx: --cache named the remote cache, but no cache plugin in vx
|
|
752
|
+
'vx: --cache-policy named the remote cache, but no cache plugin in .vx/workspace.* supplies one — the remote axes are off for this run',
|
|
743
753
|
)
|
|
744
754
|
}
|
|
745
755
|
const deferredOutputs = new DeferredOutputs({
|
|
@@ -756,7 +766,11 @@ async function runOnBus(
|
|
|
756
766
|
// fails a run). `dirty` reuses the `git status --porcelain` the
|
|
757
767
|
// GitFilesCache populate already ran for input enumeration — no
|
|
758
768
|
// second status spawn.
|
|
759
|
-
|
|
769
|
+
// `run.branch` names the branch over git's (`prepared.branch`).
|
|
770
|
+
const gitContext = {
|
|
771
|
+
...captureGitContext(workspaceRoot, gitFilesCache.worktreeDirty),
|
|
772
|
+
branch: prepared.branch(),
|
|
773
|
+
}
|
|
760
774
|
const ciContext = detectCi(process.env)
|
|
761
775
|
const hostContext = captureHostContext()
|
|
762
776
|
|
|
@@ -772,7 +786,7 @@ async function runOnBus(
|
|
|
772
786
|
let runContextRecord: RunContextRecord | undefined
|
|
773
787
|
// Gate on a plugin that can actually CONTRIBUTE a sink, not on declaring
|
|
774
788
|
// any plugin at all: a cache-only plugin has no telemetry hook to
|
|
775
|
-
// consult, so paying for the record (2 git spawns + a `.vx/workspace-id`
|
|
789
|
+
// consult, so paying for the record (2 git spawns + a `.vx/cache/workspace-id`
|
|
776
790
|
// write on a remote-less repo) buys nothing. A plugin that has the hook
|
|
777
791
|
// but declines still pays — its answer is only knowable by asking.
|
|
778
792
|
const hasTelemetryPlugin = prepared.plugins.some((p) => p.telemetry !== undefined)
|
|
@@ -857,6 +871,13 @@ async function runOnBus(
|
|
|
857
871
|
log.status(`[vx] cache save failed: ${err instanceof Error ? err.message : String(err)}`),
|
|
858
872
|
)
|
|
859
873
|
const deferredSaves = new Map<string, Promise<SaveFacts>>()
|
|
874
|
+
const windows = commandWindows()
|
|
875
|
+
// A server held from an earlier run (`vx watch`) runs through this one
|
|
876
|
+
// too, so it may write a mutable input while it is described.
|
|
877
|
+
for (const id of heldBefore.kept.keys()) {
|
|
878
|
+
const node = nodes.get(id)!
|
|
879
|
+
windows.open(id, node.projectDir, commandWriteReach(node, workspaceRoot))
|
|
880
|
+
}
|
|
860
881
|
|
|
861
882
|
// Focused flow: a requested GROUP has no output of its own, so
|
|
862
883
|
// surface the same-project, non-group tasks it chains (one level)
|
|
@@ -895,6 +916,20 @@ async function runOnBus(
|
|
|
895
916
|
workspaceProjectCount,
|
|
896
917
|
}
|
|
897
918
|
|
|
919
|
+
// Asked only when a plugin declares `output`: a plain run pays nothing.
|
|
920
|
+
if (takeOutput !== undefined && prepared.plugins.some((p) => p.output !== undefined)) {
|
|
921
|
+
const json = options.json === true
|
|
922
|
+
const claimed = await claimOutput(prepared.plugins, {
|
|
923
|
+
workspaceRoot,
|
|
924
|
+
cacheDir,
|
|
925
|
+
warn: (m: string) => log.status(m),
|
|
926
|
+
isTTY: (json ? process.stderr : process.stdout).isTTY === true,
|
|
927
|
+
ci: ciContext.ci,
|
|
928
|
+
json,
|
|
929
|
+
})
|
|
930
|
+
if (claimed !== undefined) takeOutput(claimed)
|
|
931
|
+
}
|
|
932
|
+
|
|
898
933
|
// Lifecycle hooks drive the default logger's dynamic status line
|
|
899
934
|
// (TTY-only); custom loggers may ignore them.
|
|
900
935
|
log.runStart?.({
|
|
@@ -909,7 +944,11 @@ async function runOnBus(
|
|
|
909
944
|
terminal?.forecast(async () => {
|
|
910
945
|
const ids: string[] = []
|
|
911
946
|
for (const n of nodes.values()) if (!isGroupTask(n)) ids.push(n.id)
|
|
912
|
-
const p50s = await new LocalHistoryProvider(
|
|
947
|
+
const p50s = await new LocalHistoryProvider(
|
|
948
|
+
prepared.localCache.dbHandle(),
|
|
949
|
+
undefined,
|
|
950
|
+
prepared.branch(),
|
|
951
|
+
).p50sFor(ids)
|
|
913
952
|
return createForecast(nodes, p50s, concurrency)
|
|
914
953
|
})
|
|
915
954
|
|
|
@@ -926,7 +965,7 @@ async function runOnBus(
|
|
|
926
965
|
nodes,
|
|
927
966
|
cache,
|
|
928
967
|
workspaceRoot,
|
|
929
|
-
|
|
968
|
+
claimedFiles: fingerprintWatch.names,
|
|
930
969
|
forwardArgs: options.forwardArgs,
|
|
931
970
|
nestedDirsByProject,
|
|
932
971
|
gitFilesCache,
|
|
@@ -958,7 +997,7 @@ async function runOnBus(
|
|
|
958
997
|
nodes,
|
|
959
998
|
cache,
|
|
960
999
|
workspaceRoot,
|
|
961
|
-
|
|
1000
|
+
claimedFiles: fingerprintWatch.names,
|
|
962
1001
|
forwardArgs: options.forwardArgs,
|
|
963
1002
|
nestedDirsByProject,
|
|
964
1003
|
gitFilesCache,
|
|
@@ -1031,7 +1070,6 @@ async function runOnBus(
|
|
|
1031
1070
|
node,
|
|
1032
1071
|
upstream,
|
|
1033
1072
|
workspaceRoot,
|
|
1034
|
-
workspaceFingerprint,
|
|
1035
1073
|
cache,
|
|
1036
1074
|
cachePolicy: policy,
|
|
1037
1075
|
forwardArgs: options.forwardArgs,
|
|
@@ -1051,11 +1089,15 @@ async function runOnBus(
|
|
|
1051
1089
|
outputDirSnapshots,
|
|
1052
1090
|
deferSave: saveLane.defer,
|
|
1053
1091
|
deferredSaves,
|
|
1092
|
+
commandWindows: windows,
|
|
1054
1093
|
stopSignal: stopRun.signal,
|
|
1055
1094
|
failFast: failFast.signal,
|
|
1056
1095
|
}
|
|
1057
1096
|
if (options.retries !== undefined) a.retries = options.retries
|
|
1058
1097
|
if (taskTimeoutDefault !== undefined) a.timeout = taskTimeoutDefault
|
|
1098
|
+
if (options.readyNotice !== undefined) a.readyNotice = options.readyNotice
|
|
1099
|
+
a.inputStability = inputStability
|
|
1100
|
+
if (cleanOutputs !== undefined) a.cleanOutputs = cleanOutputs
|
|
1059
1101
|
if (download.modeOf.get(node.id) === 'deferred') a.download = 'deferred'
|
|
1060
1102
|
if (placements.remoteOnlyNoop.has(node.id)) a.remoteOnlyNoop = true
|
|
1061
1103
|
if (placements.remoteOnly.has(node.id)) a.remoteOnly = true
|
|
@@ -1077,7 +1119,6 @@ async function runOnBus(
|
|
|
1077
1119
|
shortCircuit,
|
|
1078
1120
|
hashArgs: {
|
|
1079
1121
|
workspaceRoot,
|
|
1080
|
-
workspaceFingerprint,
|
|
1081
1122
|
cache,
|
|
1082
1123
|
forwardArgs: options.forwardArgs,
|
|
1083
1124
|
nestedDirsByProject,
|
|
@@ -1135,6 +1176,7 @@ async function runOnBus(
|
|
|
1135
1176
|
wallclockEndNs: at,
|
|
1136
1177
|
}
|
|
1137
1178
|
}
|
|
1179
|
+
let lockBroken: boolean | undefined
|
|
1138
1180
|
const outcomes = await runGraph({
|
|
1139
1181
|
nodes,
|
|
1140
1182
|
concurrency,
|
|
@@ -1156,7 +1198,15 @@ async function runOnBus(
|
|
|
1156
1198
|
log.taskComplete(o.node, o)
|
|
1157
1199
|
narrowDemand(o.node.id)
|
|
1158
1200
|
},
|
|
1159
|
-
|
|
1201
|
+
// A crash a hand-broken lock entry caused is named as that; asked
|
|
1202
|
+
// only here, so a run that does not crash pays nothing.
|
|
1203
|
+
onError: (node, line, err) =>
|
|
1204
|
+
log.taskStderr(
|
|
1205
|
+
node,
|
|
1206
|
+
!isUserError(err) && !isFsRefusal(err) && (lockBroken ??= lockEntryBroken(workspaceRoot))
|
|
1207
|
+
? `[vx] ${node.id}: ${LOCK_INVALID}\n`
|
|
1208
|
+
: line,
|
|
1209
|
+
),
|
|
1160
1210
|
execute: (node, upstream) => {
|
|
1161
1211
|
const kept = keepServer(node)
|
|
1162
1212
|
return kept !== undefined
|
|
@@ -1182,9 +1232,7 @@ async function runOnBus(
|
|
|
1182
1232
|
// baseline as the tie-break. Empty map → baseline only.
|
|
1183
1233
|
...(priorities.size > 0 ? { priorities } : {}),
|
|
1184
1234
|
// The baseline under them; `most-work` when the workspace names none.
|
|
1185
|
-
...(
|
|
1186
|
-
? { strategy: prepared.workspaceConfig.schedule }
|
|
1187
|
-
: {}),
|
|
1235
|
+
...(options.schedule !== undefined ? { strategy: options.schedule } : {}),
|
|
1188
1236
|
// Local short-circuit: confirmed stable local hits the scheduler
|
|
1189
1237
|
// runs ahead of their deps as low-priority worker-slot backfill.
|
|
1190
1238
|
// Empty when the short-circuit didn't fire → byte-identical.
|
|
@@ -1276,7 +1324,7 @@ async function runOnBus(
|
|
|
1276
1324
|
const stillDeferred = deferredOutputs.pending()
|
|
1277
1325
|
if (stillDeferred.length > 0) {
|
|
1278
1326
|
log.status(
|
|
1279
|
-
` Deferred: ${stillDeferred.length} task(s) left outputs remote (--download=none): ${stillDeferred.join(', ')}`,
|
|
1327
|
+
` Deferred: ${stillDeferred.length} task(s) left outputs remote (--cache-download=none): ${stillDeferred.join(', ')}`,
|
|
1280
1328
|
)
|
|
1281
1329
|
}
|
|
1282
1330
|
|
|
@@ -1506,7 +1554,8 @@ async function runOnBus(
|
|
|
1506
1554
|
if (options.summaryTable === true) for (const line of formatOutcomeTable(list)) log.status(line)
|
|
1507
1555
|
const above = options.beforeFooter?.(list, ok)
|
|
1508
1556
|
if (above) log.status(above.replace(/\n$/, ''))
|
|
1509
|
-
|
|
1557
|
+
if (options.footer !== false)
|
|
1558
|
+
for (const line of formatRunSummary(list, totalMs, colors, runContext)) log.status(line)
|
|
1510
1559
|
|
|
1511
1560
|
// Edge case the summary already reported: the user requested a
|
|
1512
1561
|
// persistent task (dev server / watcher). The run is "done" in every
|
|
@@ -1652,18 +1701,16 @@ async function runOnBus(
|
|
|
1652
1701
|
}
|
|
1653
1702
|
|
|
1654
1703
|
/**
|
|
1655
|
-
*
|
|
1704
|
+
* `cache.retention`, after every save and upload of this run
|
|
1656
1705
|
* has landed (so nothing this run wrote is mid-flight) and before the cache
|
|
1657
1706
|
* closes. Housekeeping, not the run's work: silent, and a failure is never
|
|
1658
1707
|
* a failed run. Declared nowhere → one property read.
|
|
1659
1708
|
*/
|
|
1660
1709
|
async function applyCacheRetention(prepared: PreparedRun): Promise<void> {
|
|
1661
|
-
const
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
retention.olderThan === undefined ? undefined : parseDuration(retention.olderThan)!
|
|
1666
|
-
const maxBytes = retention.maxSize === undefined ? undefined : parseSize(retention.maxSize)!
|
|
1710
|
+
const ws = prepared.workspaceConfig
|
|
1711
|
+
const maxAgeMs = resolveSetting('cache.retention.olderThan', ws).value as number | undefined
|
|
1712
|
+
const maxBytes = resolveSetting('cache.retention.maxSize', ws).value as number | undefined
|
|
1713
|
+
if (maxAgeMs === undefined && maxBytes === undefined) return
|
|
1667
1714
|
try {
|
|
1668
1715
|
await prepared.localCache.evictIfDue({
|
|
1669
1716
|
...(maxAgeMs !== undefined ? { maxAgeMs } : {}),
|
|
@@ -1708,6 +1755,7 @@ export async function planRun(options: RunOptions): Promise<RunPlan> {
|
|
|
1708
1755
|
const log = options.log ?? defaultLogger(undefined, undefined, process.stderr)
|
|
1709
1756
|
const reasons = new Map<string, AffectedReason>()
|
|
1710
1757
|
const prepared = await prepareRun({ ...options, affectedReasons: reasons }, log)
|
|
1758
|
+
options = withRunSettings(options, prepared.workspaceConfig)
|
|
1711
1759
|
try {
|
|
1712
1760
|
if (prepared.unresolvedTasks.length > 0) {
|
|
1713
1761
|
return {
|
|
@@ -1735,7 +1783,6 @@ export async function planRun(options: RunOptions): Promise<RunPlan> {
|
|
|
1735
1783
|
const planned = await plan({
|
|
1736
1784
|
nodes: prepared.nodes,
|
|
1737
1785
|
workspaceRoot: prepared.workspaceRoot,
|
|
1738
|
-
workspaceFingerprint: prepared.workspaceFingerprint,
|
|
1739
1786
|
cache: prepared.cache,
|
|
1740
1787
|
cachePolicy: policy,
|
|
1741
1788
|
forwardArgs: options.forwardArgs,
|
|
@@ -1747,7 +1794,11 @@ export async function planRun(options: RunOptions): Promise<RunPlan> {
|
|
|
1747
1794
|
// per-task p50s + a predicted wall-clock. `--dry` is an explicit
|
|
1748
1795
|
// inspection command, so the history read's cost is fine here even
|
|
1749
1796
|
// though it stays OFF the default run path.
|
|
1750
|
-
history: new LocalHistoryProvider(
|
|
1797
|
+
history: new LocalHistoryProvider(
|
|
1798
|
+
prepared.localCache.dbHandle(),
|
|
1799
|
+
undefined,
|
|
1800
|
+
prepared.branch(),
|
|
1801
|
+
),
|
|
1751
1802
|
// Placement, by the same rules the run applies. Only worth showing
|
|
1752
1803
|
// when there is a choice to show — with one declared executor every
|
|
1753
1804
|
// line would carry the same label. Resolving the executors here is
|
|
@@ -1797,7 +1848,7 @@ async function planRestorable(
|
|
|
1797
1848
|
nodes,
|
|
1798
1849
|
cache: prepared.cache,
|
|
1799
1850
|
workspaceRoot: prepared.workspaceRoot,
|
|
1800
|
-
|
|
1851
|
+
claimedFiles: prepared.fingerprintWatch.names,
|
|
1801
1852
|
forwardArgs,
|
|
1802
1853
|
nestedDirsByProject: prepared.nestedDirsByProject,
|
|
1803
1854
|
gitFilesCache: prepared.gitFilesCache,
|
|
@@ -1815,25 +1866,13 @@ function elsewhereHint(names: readonly string[]): string {
|
|
|
1815
1866
|
}
|
|
1816
1867
|
|
|
1817
1868
|
/**
|
|
1818
|
-
* The first-run case, told apart from a typo: no
|
|
1869
|
+
* The first-run case, told apart from a typo: no project in the workspace
|
|
1819
1870
|
* has a `vx.config.*` at all, so no name could have resolved. Reached only
|
|
1820
|
-
* on the error path.
|
|
1821
|
-
* told that instead — `vx init` would find no package to write for.
|
|
1871
|
+
* on the error path.
|
|
1822
1872
|
*/
|
|
1823
|
-
async function initHint(prepared: {
|
|
1824
|
-
anyProjectConfig: boolean
|
|
1825
|
-
workspaceProjectCount: number
|
|
1826
|
-
workspaceRoot: string
|
|
1827
|
-
}): Promise<string> {
|
|
1873
|
+
async function initHint(prepared: { anyProjectConfig: boolean }): Promise<string> {
|
|
1828
1874
|
if (prepared.anyProjectConfig) return ''
|
|
1829
|
-
|
|
1830
|
-
return " No package matched the workspace's package globs — check `workspaces` in package.json (or pnpm-workspace.yaml)."
|
|
1831
|
-
}
|
|
1832
|
-
// Single-project mode with packages the root's missing `workspaces`
|
|
1833
|
-
// never reaches: `vx init` would find no scripts either (item 248).
|
|
1834
|
-
const unreached = await unreachedPackages(await loadWorkspace(prepared.workspaceRoot))
|
|
1835
|
-
if (unreached.length > 0) return ` ${unreachedHint(unreached, prepared.workspaceRoot)}`
|
|
1836
|
-
return ' No package declares a vx.config — run `vx init` to write one per package from its package.json scripts.'
|
|
1875
|
+
return ' No directory holds a vx.config — run `vx init` to write them, then `vx lock`.'
|
|
1837
1876
|
}
|
|
1838
1877
|
|
|
1839
1878
|
/**
|
|
@@ -1879,7 +1918,7 @@ function didYouMean(
|
|
|
1879
1918
|
// `//` is two edits from any two-letter name: it hinted `ui#build` (X-13).
|
|
1880
1919
|
if (proj === '//') {
|
|
1881
1920
|
lists.add(
|
|
1882
|
-
|
|
1921
|
+
'`//` is the workspace root, which is no project here: add a vx.config there with that task and run `vx lock`.',
|
|
1883
1922
|
)
|
|
1884
1923
|
continue
|
|
1885
1924
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// The `exec.sandbox.allow` entries that would have granted what a failed
|
|
2
|
-
// task was denied, so the report names the line to add (CLI, JSON
|
|
2
|
+
// task was denied, so the report names the line to add (CLI, JSON)
|
|
3
3
|
// rather than leaving the user to turn absolute paths into grants. Built on
|
|
4
4
|
// the failure path only: a passing run never calls it.
|
|
5
5
|
|
|
@@ -9,26 +9,22 @@ import path from 'node:path'
|
|
|
9
9
|
import type { SandboxViolation } from '../exec/index.js'
|
|
10
10
|
import type { TaskOutcome } from '../graph/index.js'
|
|
11
11
|
import { isLiteralPattern } from '../util/index.js'
|
|
12
|
+
import { CREDENTIAL_STORES } from './sandbox-request.js'
|
|
12
13
|
|
|
13
14
|
/** Grants to add under `exec.sandbox.allow`, each list sorted and unique. */
|
|
14
15
|
type SandboxFix = NonNullable<TaskOutcome['sandboxFix']>
|
|
15
|
-
type Kind = Exclude<keyof SandboxFix, 'withheld'>
|
|
16
|
-
|
|
17
|
-
// Where credentials live. A denial here is never turned into a grant: an
|
|
18
|
-
// agent applies `sandboxFix` as given, so a dependency probing ~/.ssh would
|
|
19
|
-
// get its grant suggested and applied (Security, 2026-10-12).
|
|
20
|
-
const CREDENTIAL_DIRS = ['.ssh', '.aws', '.gnupg', '.kube', '.config/gh', '.config/gcloud']
|
|
21
|
-
const CREDENTIAL_FILES = ['.netrc', '.npmrc', '.docker/config.json', '.git-credentials', '.pypirc']
|
|
16
|
+
type Kind = Exclude<keyof SandboxFix, 'withheld' | 'inputs'>
|
|
22
17
|
|
|
18
|
+
// A denial in a credential store is never turned into a grant: an agent
|
|
19
|
+
// applies `sandboxFix` as given, so a dependency probing ~/.ssh would get
|
|
20
|
+
// its grant suggested and applied (Security, 2026-10-12). The stores are
|
|
21
|
+
// the ones the sandbox denies: a hand copy here missed half of them.
|
|
23
22
|
function isCredential(abs: string): boolean {
|
|
24
23
|
const base = path.basename(abs)
|
|
25
24
|
if (base === '.env' || base.startsWith('.env.')) return true
|
|
26
25
|
const rel = path.relative(os.homedir(), abs)
|
|
27
26
|
if (rel === '' || rel.startsWith('..') || path.isAbsolute(rel)) return false
|
|
28
|
-
return (
|
|
29
|
-
CREDENTIAL_FILES.includes(rel) ||
|
|
30
|
-
CREDENTIAL_DIRS.some((d) => rel === d || rel.startsWith(d + path.sep))
|
|
31
|
-
)
|
|
27
|
+
return CREDENTIAL_STORES.some((d) => rel === d || rel.startsWith(d + path.sep))
|
|
32
28
|
}
|
|
33
29
|
|
|
34
30
|
/**
|
|
@@ -63,6 +59,16 @@ function readsDir(g: string, top: string): boolean {
|
|
|
63
59
|
return first === top || !isLiteralPattern(first)
|
|
64
60
|
}
|
|
65
61
|
|
|
62
|
+
const inProject = (grant: string): boolean => !grant.startsWith('~/') && !path.isAbsolute(grant)
|
|
63
|
+
|
|
64
|
+
/** Whether a project-relative path is one the task's input globs match. */
|
|
65
|
+
function keyedBy(globs: readonly string[]): (rel: string) => boolean {
|
|
66
|
+
const strip = (g: string) => g.replace(/^\.\//, '')
|
|
67
|
+
const take = globs.filter((g) => !g.startsWith('!')).map((g) => new Bun.Glob(strip(g)))
|
|
68
|
+
const drop = globs.filter((g) => g.startsWith('!')).map((g) => new Bun.Glob(strip(g.slice(1))))
|
|
69
|
+
return (rel) => take.some((g) => g.match(rel)) && !drop.some((g) => g.match(rel))
|
|
70
|
+
}
|
|
71
|
+
|
|
66
72
|
function kindOf(v: SandboxViolation): Kind | undefined {
|
|
67
73
|
const can = v.ignorable ?? []
|
|
68
74
|
if (can.length === 1) return can[0]
|
|
@@ -78,16 +84,22 @@ export function sandboxFix(
|
|
|
78
84
|
violations: readonly SandboxViolation[],
|
|
79
85
|
projectDir: string,
|
|
80
86
|
mask: (text: string) => string = (t) => t,
|
|
81
|
-
/**
|
|
82
|
-
|
|
87
|
+
/**
|
|
88
|
+
* The task's `cache.inputs.files`, undefined when it does not cache: a
|
|
89
|
+
* write under one is granted exactly, and a read of the project no glob
|
|
90
|
+
* matches is named for the key too.
|
|
91
|
+
*/
|
|
92
|
+
inputs?: readonly string[],
|
|
83
93
|
): SandboxFix | undefined {
|
|
84
|
-
const fix: Record<Kind | 'withheld', Set<string>> = {
|
|
94
|
+
const fix: Record<Kind | 'withheld' | 'inputs', Set<string>> = {
|
|
85
95
|
read: new Set(),
|
|
86
96
|
write: new Set(),
|
|
87
97
|
network: new Set(),
|
|
88
98
|
systemInfo: new Set(),
|
|
89
99
|
withheld: new Set(),
|
|
100
|
+
inputs: new Set(),
|
|
90
101
|
}
|
|
102
|
+
const keyed = inputs === undefined ? undefined : keyedBy(inputs)
|
|
91
103
|
for (const v of violations) {
|
|
92
104
|
if (v.hint === true || v.target === undefined) continue
|
|
93
105
|
const kind = kindOf(v)
|
|
@@ -96,16 +108,19 @@ export function sandboxFix(
|
|
|
96
108
|
else if (kind === 'systemInfo') fix.systemInfo.add(v.target)
|
|
97
109
|
else if (v.path === undefined) continue
|
|
98
110
|
else if (isCredential(v.path)) fix.withheld.add(grantPath(v.path, projectDir))
|
|
99
|
-
else if (kind === 'read')
|
|
100
|
-
|
|
111
|
+
else if (kind === 'read') {
|
|
112
|
+
const grant = grantPath(v.path, projectDir)
|
|
113
|
+
fix.read.add(grant)
|
|
114
|
+
if (keyed !== undefined && inProject(grant) && !keyed(grant)) fix.inputs.add(grant)
|
|
115
|
+
} else {
|
|
101
116
|
const isDir =
|
|
102
117
|
v.line.startsWith('mkdir') ||
|
|
103
118
|
lstatSync(v.path, { throwIfNoEntry: false })?.isDirectory() === true
|
|
104
|
-
fix.write.add(writeGrant(grantPath(v.path, projectDir), isDir, inputs))
|
|
119
|
+
fix.write.add(writeGrant(grantPath(v.path, projectDir), isDir, inputs ?? []))
|
|
105
120
|
}
|
|
106
121
|
}
|
|
107
122
|
const out: SandboxFix = {}
|
|
108
|
-
for (const k of ['read', 'write', 'network', 'systemInfo', 'withheld'] as const)
|
|
123
|
+
for (const k of ['read', 'write', 'network', 'systemInfo', 'withheld', 'inputs'] as const)
|
|
109
124
|
if (fix[k].size > 0) out[k] = [...new Set([...fix[k]].map(mask))].sort()
|
|
110
125
|
return Object.keys(out).length > 0 ? out : undefined
|
|
111
126
|
}
|
|
@@ -114,7 +129,11 @@ export function sandboxFix(
|
|
|
114
129
|
* The fix as the config line it is: `exec.sandbox.allow: { read: ['a'] }`,
|
|
115
130
|
* or undefined when every denial was a withheld credential path.
|
|
116
131
|
*/
|
|
117
|
-
export function sandboxFixLine({
|
|
132
|
+
export function sandboxFixLine({
|
|
133
|
+
withheld: _,
|
|
134
|
+
inputs: __,
|
|
135
|
+
...fix
|
|
136
|
+
}: SandboxFix): string | undefined {
|
|
118
137
|
if (Object.keys(fix).length === 0) return undefined
|
|
119
138
|
const parts = Object.entries(fix).map(
|
|
120
139
|
([k, list]: [string, string[]]) => `${k}: [${list.map((p) => `'${p}'`).join(', ')}]`,
|