@vzn/vx 0.0.18 → 0.0.22
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/LICENSE +1 -1
- package/README.md +107 -70
- package/package.json +10 -20
- package/src/bin.ts +64 -5
- package/src/cache/archive.ts +279 -28
- package/src/cache/cache.ts +952 -1584
- package/src/cache/chained-cache.ts +14 -10
- package/src/cache/config-evals.ts +112 -0
- package/src/cache/file-hashes.ts +290 -0
- package/src/cache/git-inputs.ts +966 -0
- package/src/cache/index.ts +23 -7
- package/src/cache/inputs.ts +553 -739
- package/src/cache/key-fold.ts +232 -0
- package/src/cache/layer.ts +674 -0
- package/src/cache/layered-cache.ts +211 -49
- package/src/cache/output-index.ts +323 -0
- package/src/cache/policy.ts +84 -0
- package/src/cache/run-history.ts +152 -0
- package/src/cache/zstd.ts +183 -0
- package/src/cli/cache.ts +88 -30
- package/src/cli/completions.ts +104 -0
- package/src/cli/core-alias.ts +25 -0
- package/src/cli/format.ts +4 -15
- package/src/cli/help.ts +76 -38
- package/src/cli/index.ts +82 -17
- package/src/cli/info.ts +162 -91
- package/src/cli/init.ts +87 -0
- package/src/cli/last.ts +130 -23
- package/src/cli/lock.ts +36 -18
- package/src/cli/plan-format.ts +18 -7
- package/src/cli/plugin-commands.ts +34 -12
- package/src/cli/run.ts +138 -313
- package/src/cli/select.ts +342 -0
- package/src/cli/show.ts +151 -71
- package/src/cli/upgrade.ts +136 -15
- package/src/cli/watch.ts +1175 -116
- package/src/cli/why.ts +51 -29
- package/src/cli/workspace-config.ts +138 -0
- package/src/config.ts +183 -126
- package/src/exec/env.ts +11 -0
- package/src/exec/executor.ts +62 -31
- package/src/exec/index.ts +16 -2
- package/src/exec/kill-tree.ts +281 -0
- package/src/exec/local-executor.ts +41 -0
- package/src/exec/runner.ts +361 -77
- package/src/exec/sandbox-binds.ts +211 -0
- package/src/exec/sandbox-paths.ts +70 -0
- package/src/exec/sandbox-runtime.ts +1152 -415
- package/src/exec/sandbox-violations.ts +293 -0
- package/src/graph/dependency-spec.ts +14 -14
- package/src/graph/index.ts +6 -4
- package/src/graph/priorities.ts +130 -0
- package/src/graph/scheduler.ts +332 -329
- package/src/graph/task-graph.ts +552 -188
- package/src/index.ts +111 -102
- package/src/orchestrator/admission.ts +161 -0
- package/src/orchestrator/colors.ts +8 -3
- package/src/orchestrator/deferred-outputs.ts +25 -7
- package/src/orchestrator/doctor.ts +363 -0
- package/src/orchestrator/events.ts +111 -8
- package/src/orchestrator/excluded-keys.ts +155 -0
- package/src/orchestrator/execute-task.ts +450 -565
- package/src/orchestrator/failure-mode.ts +169 -28
- package/src/orchestrator/failure-recap.ts +121 -0
- package/src/orchestrator/fingerprint-watch.ts +89 -0
- package/src/orchestrator/framed-output.ts +121 -49
- package/src/orchestrator/history.ts +123 -104
- package/src/orchestrator/hit-restore.ts +285 -0
- package/src/orchestrator/index.ts +42 -5
- package/src/orchestrator/keyed-projects.ts +89 -0
- package/src/orchestrator/local-shortcircuit.ts +83 -23
- package/src/orchestrator/lockfile-claim.ts +401 -0
- package/src/orchestrator/logger.ts +128 -41
- package/src/orchestrator/metrics.ts +201 -72
- package/src/orchestrator/miss-save.ts +232 -0
- package/src/orchestrator/options.ts +63 -34
- package/src/orchestrator/persistent.ts +99 -0
- package/src/orchestrator/placement.ts +212 -0
- package/src/orchestrator/plan.ts +24 -11
- package/src/orchestrator/plugin-host.ts +200 -49
- package/src/orchestrator/plugin.ts +190 -14
- package/src/orchestrator/prepare.ts +168 -137
- package/src/orchestrator/projects.ts +270 -0
- package/src/orchestrator/remote-prefetch.ts +22 -2
- package/src/orchestrator/run-artifacts.ts +34 -3
- package/src/orchestrator/run-context.ts +20 -5
- package/src/orchestrator/run-lock.ts +349 -0
- package/src/orchestrator/run-records.ts +132 -0
- package/src/orchestrator/run-report.ts +6 -4
- package/src/orchestrator/run.ts +467 -611
- package/src/orchestrator/sandbox-request.ts +569 -0
- package/src/orchestrator/save-lane.ts +44 -0
- package/src/orchestrator/shell-verdict.ts +117 -0
- package/src/orchestrator/signals.ts +167 -0
- package/src/orchestrator/stable-keys.ts +259 -10
- package/src/orchestrator/status-line.ts +35 -2
- package/src/orchestrator/summary.ts +115 -25
- package/src/orchestrator/task-hash.ts +103 -21
- package/src/orchestrator/task-log-buffer.ts +15 -18
- package/src/orchestrator/telemetry-host.ts +27 -8
- package/src/orchestrator/telemetry.ts +71 -44
- package/src/orchestrator/upstream.ts +91 -45
- package/src/util/bun-version.ts +43 -0
- package/src/util/cgroup.ts +160 -0
- package/src/util/edit-distance.ts +49 -0
- package/src/util/errors.ts +86 -0
- package/src/util/hash.ts +10 -1
- package/src/util/index.ts +42 -5
- package/src/util/paths.ts +168 -5
- package/src/util/procfs.ts +20 -0
- package/src/util/settle.ts +23 -2
- package/src/util/size.ts +33 -0
- package/src/util/task-id.ts +20 -0
- package/src/util/timing.ts +11 -1
- package/src/util/verbs.ts +39 -0
- package/src/util/which.ts +48 -0
- package/src/workspace/affected.ts +368 -60
- package/src/workspace/config-cache.ts +271 -32
- package/src/workspace/config-eval.ts +60 -16
- package/src/workspace/config-imports.ts +131 -9
- package/src/workspace/config-schema.ts +1033 -0
- package/src/workspace/filter.ts +69 -17
- package/src/workspace/fingerprint.ts +59 -7
- package/src/workspace/index.ts +37 -3
- package/src/workspace/json-data.ts +90 -0
- package/src/workspace/load-reads.ts +32 -0
- package/src/workspace/lockfile.ts +41 -14
- package/src/workspace/migrate-scripts.ts +187 -0
- package/src/workspace/migration.ts +353 -0
- package/src/workspace/package-graph.ts +239 -31
- package/src/workspace/project-loader.ts +295 -815
- package/src/workspace/workspace.ts +266 -31
- package/plugins/local-cache/index.ts +0 -2
- package/plugins/local-executor/index.ts +0 -2
- package/plugins/schedule-history/index.ts +0 -2
- package/src/cache/cas-backend.ts +0 -115
- package/src/cache/digest.ts +0 -32
- package/src/cli/migrate-emit.ts +0 -21
- package/src/cli/migrate-nx.ts +0 -473
- package/src/cli/migrate-scripts.ts +0 -127
- package/src/cli/migrate-turbo.ts +0 -401
- package/src/cli/migrate.ts +0 -305
- package/src/cli/prune.ts +0 -291
- package/src/orchestrator/missing-plugin.ts +0 -9
- package/src/orchestrator/resources.ts +0 -45
- package/src/orchestrator/verify.ts +0 -174
- package/src/plugins/local-cache/index.ts +0 -11
- package/src/plugins/local-executor/index.ts +0 -45
- package/src/plugins/schedule-history/index.ts +0 -109
package/src/config.ts
CHANGED
|
@@ -14,13 +14,34 @@ export interface WorkspaceConfig {
|
|
|
14
14
|
*/
|
|
15
15
|
timeout?: number
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* `
|
|
17
|
+
* Evict from the local cache at the end of every run that writes to it,
|
|
18
|
+
* by the same policy `vx cache prune` takes as flags: entries not used
|
|
19
|
+
* for `olderThan` (`30d`, `12h`), then least-recently-used entries
|
|
20
|
+
* until the cache is under `maxSize` (`10G`, `500MB`). Either or both.
|
|
21
|
+
* Omitted → nothing is evicted unless you run `vx cache prune`. Never
|
|
22
|
+
* folded into a cache key.
|
|
23
|
+
*/
|
|
24
|
+
cacheRetention?: { olderThan?: string; maxSize?: string }
|
|
25
|
+
/**
|
|
26
|
+
* Plugins registered for this workspace, consulted in this order once
|
|
27
|
+
* per `vx run`: each fills any of the pipeline stages and capabilities
|
|
28
|
+
* below, and core's own executor and cache store sit at the tail of
|
|
29
|
+
* every list. See `docs/design/pipeline-2026-09.md`.
|
|
20
30
|
*/
|
|
21
31
|
plugins?: readonly Plugin[]
|
|
22
32
|
}
|
|
23
33
|
|
|
34
|
+
/**
|
|
35
|
+
* The runtime brand `definePlugin` stamps on a plugin: the name of the
|
|
36
|
+
* package it was defined in. The workspace loader accepts a plugin only
|
|
37
|
+
* when this is present and equals `name`, so a plugin's name is its
|
|
38
|
+
* package name and nothing else — no field to set, no override. A
|
|
39
|
+
* registry symbol on purpose: a plugin package imports its own copy of
|
|
40
|
+
* `@vzn/vx` beside a compiled binary's, and the two copies must stamp
|
|
41
|
+
* and check the same key.
|
|
42
|
+
*/
|
|
43
|
+
export const PLUGIN_PACKAGE: unique symbol = Symbol.for('vx.plugin.package')
|
|
44
|
+
|
|
24
45
|
/**
|
|
25
46
|
* Structural plugin shape — any subset of the pipeline stages
|
|
26
47
|
* (`config`/`project`/`graph`/`key`/`schedule`), run-level capabilities
|
|
@@ -32,20 +53,60 @@ export interface WorkspaceConfig {
|
|
|
32
53
|
* before consulting capabilities.
|
|
33
54
|
*/
|
|
34
55
|
export interface Plugin {
|
|
56
|
+
/** The package name — set by `definePlugin`, never by the plugin. */
|
|
35
57
|
readonly name: string
|
|
36
58
|
config?(workspace: unknown, ctx: unknown): unknown
|
|
37
59
|
project?(config: unknown, ctx: unknown): unknown
|
|
38
60
|
graph?(nodes: unknown, ctx: unknown): unknown
|
|
39
61
|
key?(task: unknown, ctx: unknown): unknown
|
|
40
62
|
schedule?(nodes: unknown, ctx: unknown): unknown
|
|
63
|
+
admit?(task: unknown, ctx: unknown): unknown
|
|
41
64
|
readonly commands?: Readonly<Record<string, unknown>>
|
|
42
65
|
cache?(ctx: unknown): unknown
|
|
43
66
|
executor?(ctx: unknown): unknown
|
|
44
67
|
telemetry?(ctx: unknown): unknown
|
|
45
68
|
setup?(ctx: unknown): void | Promise<void>
|
|
46
69
|
teardown?(): void | Promise<void>
|
|
70
|
+
readonly fingerprint?: unknown
|
|
47
71
|
}
|
|
48
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Every hook a plugin may fill, in pipeline order — THE list. The loader's
|
|
75
|
+
* "must be a function" and "at least one of" checks, the host's stage gate
|
|
76
|
+
* and `vx info`'s seam column all read it, so a stage added here is a stage
|
|
77
|
+
* everywhere: `admit` reached `vx info` a day late (2026-09-12) because
|
|
78
|
+
* that column kept its own copy, and the loader kept a third. The type
|
|
79
|
+
* pin below refuses a list that drifts from `Plugin`'s keys either way.
|
|
80
|
+
*/
|
|
81
|
+
export const PLUGIN_HOOKS = [
|
|
82
|
+
'config',
|
|
83
|
+
'project',
|
|
84
|
+
'graph',
|
|
85
|
+
'key',
|
|
86
|
+
'fingerprint',
|
|
87
|
+
'schedule',
|
|
88
|
+
'admit',
|
|
89
|
+
'executor',
|
|
90
|
+
'cache',
|
|
91
|
+
'telemetry',
|
|
92
|
+
'setup',
|
|
93
|
+
'commands',
|
|
94
|
+
'teardown',
|
|
95
|
+
] as const
|
|
96
|
+
export type PluginHook = (typeof PLUGIN_HOOKS)[number]
|
|
97
|
+
/** The hooks that are functions; `commands` and `fingerprint` are objects. */
|
|
98
|
+
export const PLUGIN_FUNCTION_HOOKS = PLUGIN_HOOKS.filter(
|
|
99
|
+
(h): h is Exclude<PluginHook, 'commands' | 'fingerprint'> =>
|
|
100
|
+
h !== 'commands' && h !== 'fingerprint',
|
|
101
|
+
)
|
|
102
|
+
type PluginHookKeys = Exclude<keyof Plugin, 'name'>
|
|
103
|
+
type HooksNotListed = Exclude<PluginHookKeys, PluginHook>
|
|
104
|
+
type HooksNotOnPlugin = Exclude<PluginHook, PluginHookKeys>
|
|
105
|
+
// Both `never`, or this line does not compile: the list and the type agree.
|
|
106
|
+
const _pluginHooksMatch: [HooksNotListed, HooksNotOnPlugin] extends [never, never] ? true : never =
|
|
107
|
+
true
|
|
108
|
+
void _pluginHooksMatch
|
|
109
|
+
|
|
49
110
|
export interface ProjectConfig {
|
|
50
111
|
/** Tasks declared by this project, keyed by task name. */
|
|
51
112
|
tasks?: Record<string, TaskConfig>
|
|
@@ -94,123 +155,129 @@ export interface TaskConfig {
|
|
|
94
155
|
* runs.
|
|
95
156
|
*/
|
|
96
157
|
cache?: CacheConfig
|
|
97
|
-
/**
|
|
98
|
-
* Sandbox configuration. **Sandbox is opt-in per task.**
|
|
99
|
-
* - omitted entirely → task runs unsandboxed.
|
|
100
|
-
* - `sandbox: {}` → opts in with the minimum baseline (only
|
|
101
|
-
* resolved inputs readable, only static-prefix of outputs
|
|
102
|
-
* writable, network blocked).
|
|
103
|
-
* - `sandbox: { ... }` → opts in with the explicit options below.
|
|
104
|
-
*
|
|
105
|
-
* No inheritance, no workspace defaults, no built-in escapes. The
|
|
106
|
-
* sandbox sees STRICTLY the union of resolved `cache.inputs.files`
|
|
107
|
-
* + this block's `allowRead` (for reads) and the static prefix of
|
|
108
|
-
* `cache.outputs.files` + this block's `allowWrite` (for writes).
|
|
109
|
-
* If a task needs `node_modules` or `/tmp`, declare them here
|
|
110
|
-
* explicitly.
|
|
111
|
-
*
|
|
112
|
-
* **Policy: fail on violation.** If the sandbox detects an
|
|
113
|
-
* undeclared read/write (macOS log monitor) or the child returns
|
|
114
|
-
* non-zero because it couldn't reach a path it expected (Linux
|
|
115
|
-
* structural deny), the task fails. No cache is written for a
|
|
116
|
-
* failed task.
|
|
117
|
-
*
|
|
118
|
-
* Activation requires `exec` (group tasks have no command to wrap)
|
|
119
|
-
* and is silently skipped for persistent tasks (dev servers need
|
|
120
|
-
* unrestricted network + an indefinite process).
|
|
121
|
-
*/
|
|
122
|
-
sandbox?: SandboxConfig
|
|
123
158
|
}
|
|
124
159
|
|
|
125
160
|
/**
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
161
|
+
* What a sandboxed task may do, declared as CAPABILITIES.
|
|
162
|
+
*
|
|
163
|
+
* One shape for the whole policy. You say what the task is allowed to
|
|
164
|
+
* touch; vx decides how each capability is realised — some become sandbox
|
|
165
|
+
* runtime config, some become rules in the OS policy, and one the platform
|
|
166
|
+
* cannot express is an error rather than a silent no-op. None of that
|
|
167
|
+
* reaches the config.
|
|
129
168
|
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
169
|
+
* The baseline (`sandbox: {}`) grants almost nothing: the task reads
|
|
170
|
+
* nothing, writes nothing and reaches no network — NOT EVEN ITS OWN
|
|
171
|
+
* PROJECT DIRECTORY, which is why `allow: { read: ['.'] }` opens almost
|
|
172
|
+
* every real block. The one grant vx makes for you is dependencies:
|
|
173
|
+
* `node_modules`, and through it the real path of every workspace
|
|
174
|
+
* package linked there.
|
|
135
175
|
*
|
|
136
|
-
*
|
|
176
|
+
* `cache` grants no access in either direction: `cache.inputs` says what
|
|
177
|
+
* INVALIDATES a task, `allow` says what it may TOUCH, and deriving one
|
|
178
|
+
* from the other coupled them both ways — a declaration added for
|
|
179
|
+
* caching silently widened the sandbox, and a path the task needed had
|
|
180
|
+
* to be laundered through the cache key to get it (owner, 2026-09-05).
|
|
181
|
+
* A declared `cache.outputs` is NOT a write grant; the request
|
|
182
|
+
* `sandbox-request.ts` builds carries no write of its own and binds
|
|
183
|
+
* `allow.write` alone. This comment claimed the opposite — here and on
|
|
184
|
+
* both grant fields — while the file beside it and `schema.md` both said
|
|
185
|
+
* the truth, and no test read the bare baseline it described (item 443).
|
|
186
|
+
* That case has a test of its own now.
|
|
187
|
+
*
|
|
188
|
+
* Enforcement anchors at the WORKSPACE ROOT, so a task cannot leave its
|
|
189
|
+
* project whatever it declares here. The full account, including what a
|
|
190
|
+
* write grant costs on Linux, is in `docs/schema.md` under
|
|
191
|
+
* `exec.sandbox`.
|
|
192
|
+
*
|
|
193
|
+
* Paths are project-relative, absolute, or `~`-expanded — and are path
|
|
194
|
+
* PREFIXES, never globs (a glob cannot be enforced by a mount).
|
|
137
195
|
*/
|
|
138
196
|
export interface SandboxConfig {
|
|
139
|
-
|
|
197
|
+
/** Capabilities granted on top of the task's own declarations. */
|
|
198
|
+
allow?: SandboxGrants
|
|
140
199
|
/**
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
* allowRead set passed to the sandbox.
|
|
200
|
+
* Capabilities taken away. Evaluated before `allow`, so a domain in both
|
|
201
|
+
* is denied.
|
|
144
202
|
*/
|
|
145
|
-
|
|
203
|
+
deny?: SandboxDenials
|
|
146
204
|
/**
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
205
|
+
* Denials to leave out of the report, in the same shape as `allow`.
|
|
206
|
+
*
|
|
207
|
+
* This does NOT permit anything: the OS still denies the operation, and
|
|
208
|
+
* a command that cannot proceed without it still fails. It says "I know,
|
|
209
|
+
* and it does not affect this task's output" — for a denial you cannot
|
|
210
|
+
* grant away and do not want failing the run.
|
|
211
|
+
*
|
|
212
|
+
* Unlike `allow`, the values are GLOB PATTERNS, and they work the same
|
|
213
|
+
* on every platform: `allow` has to be expressible by the OS sandbox
|
|
214
|
+
* (bwrap mounts a path; it cannot mount a pattern), while `ignore` only
|
|
215
|
+
* has to match a line vx already holds. Relative patterns anchor at the
|
|
216
|
+
* project dir.
|
|
217
|
+
*
|
|
218
|
+
* ```ts
|
|
219
|
+
* ignore: { write: ['*.bun-build'] } // bun --compile stages a random
|
|
220
|
+
* // temp name in the cwd
|
|
221
|
+
* ```
|
|
150
222
|
*/
|
|
151
|
-
|
|
223
|
+
ignore?: SandboxGrants
|
|
152
224
|
/**
|
|
153
|
-
*
|
|
154
|
-
*
|
|
225
|
+
* Accept a weaker sandbox when vx itself already runs inside one
|
|
226
|
+
* (a container, another sandbox). Without it, nesting fails fast rather
|
|
227
|
+
* than pretending to confine.
|
|
155
228
|
*/
|
|
156
|
-
|
|
229
|
+
weakerWhenNested?: boolean
|
|
230
|
+
/** Accept weaker network isolation (host proxy) for lower overhead. */
|
|
231
|
+
weakerNetworkIsolation?: boolean
|
|
232
|
+
}
|
|
157
233
|
|
|
158
|
-
|
|
234
|
+
export interface SandboxGrants {
|
|
159
235
|
/**
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
* - object — fine-grained control (domains / sockets / etc.).
|
|
236
|
+
* Readable path prefixes, added to the baseline — which is
|
|
237
|
+
* `node_modules`, NOT `cache.inputs.files`. A task that reads its own
|
|
238
|
+
* source in the sandbox says so here.
|
|
164
239
|
*/
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
// === Process behavior ===
|
|
240
|
+
read?: string[]
|
|
168
241
|
/**
|
|
169
|
-
*
|
|
170
|
-
*
|
|
242
|
+
* Writable path prefixes. The task can write NOTHING without these:
|
|
243
|
+
* declaring `cache.outputs.files` grants no write, and a task whose
|
|
244
|
+
* only declaration is its outputs writes into a tmpfs that evaporates
|
|
245
|
+
* (vx then warns that `cache.outputs` matched no files).
|
|
171
246
|
*/
|
|
172
|
-
|
|
247
|
+
write?: string[]
|
|
173
248
|
/**
|
|
174
|
-
*
|
|
175
|
-
*
|
|
249
|
+
* Reachable domains. `true` allows all, `['*.example.com']` a pattern.
|
|
250
|
+
* Omitted means no network at all.
|
|
176
251
|
*/
|
|
177
|
-
|
|
252
|
+
network?: true | string[]
|
|
178
253
|
/**
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
254
|
+
* Kernel information the task may query, by info type — the name that
|
|
255
|
+
* appears in a denial, e.g. `vfs.disk-space` (every Bun and git process
|
|
256
|
+
* probes free space at startup).
|
|
182
257
|
*/
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
258
|
+
systemInfo?: string[]
|
|
259
|
+
/** Unix socket paths the task may connect to. `true` allows any. */
|
|
260
|
+
unixSockets?: true | string[]
|
|
186
261
|
/**
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
* own
|
|
262
|
+
* Bind and reach localhost ports — a test that boots a server for
|
|
263
|
+
* itself. A port LIST additionally makes those ports reachable from
|
|
264
|
+
* outside the sandbox (a dev server another task or the developer
|
|
265
|
+
* talks to): on Linux, where a sandboxed task lives in its own network
|
|
266
|
+
* namespace, each listed port is bridged out to the host's loopback;
|
|
267
|
+
* on macOS the host already sees them and the list means `true`.
|
|
191
268
|
*/
|
|
192
|
-
|
|
269
|
+
localBinding?: boolean | number[]
|
|
270
|
+
/** macOS: Mach service names the task may look up. */
|
|
271
|
+
machLookup?: string[]
|
|
272
|
+
/** Acquire a TTY. Rare outside interactive tools. */
|
|
273
|
+
pty?: boolean
|
|
274
|
+
/** Write to `.git/config`. Most build tools should not. */
|
|
275
|
+
gitConfig?: boolean
|
|
193
276
|
}
|
|
194
277
|
|
|
195
|
-
export interface
|
|
196
|
-
/**
|
|
197
|
-
|
|
198
|
-
* `*` (allow all).
|
|
199
|
-
*/
|
|
200
|
-
allowedDomains?: string[]
|
|
201
|
-
/** Domains explicitly blocked, evaluated before allowedDomains. */
|
|
202
|
-
deniedDomains?: string[]
|
|
203
|
-
/** Unix socket paths the task may connect to. */
|
|
204
|
-
allowUnixSockets?: string[]
|
|
205
|
-
/** Allow any Unix socket. Use with care. */
|
|
206
|
-
allowAllUnixSockets?: boolean
|
|
207
|
-
/**
|
|
208
|
-
* Allow binding to local ports (e.g. a test that boots a server on
|
|
209
|
-
* localhost for itself to query).
|
|
210
|
-
*/
|
|
211
|
-
allowLocalBinding?: boolean
|
|
212
|
-
/** macOS only. Mach service names the task may look up. */
|
|
213
|
-
allowMachLookup?: string[]
|
|
278
|
+
export interface SandboxDenials {
|
|
279
|
+
/** Domains blocked even if `allow.network` would admit them. */
|
|
280
|
+
network?: string[]
|
|
214
281
|
}
|
|
215
282
|
|
|
216
283
|
export interface ExecConfig {
|
|
@@ -258,16 +325,6 @@ export interface ExecConfig {
|
|
|
258
325
|
* default, including `retries: 0`.
|
|
259
326
|
*/
|
|
260
327
|
retries?: number
|
|
261
|
-
/**
|
|
262
|
-
* Resource RESERVATIONS for admission control — NOT enforcement: vx
|
|
263
|
-
* does not cgroup-limit, nice, or kill the task; it only decides what
|
|
264
|
-
* to co-schedule so concurrent reservations never exceed the CPU /
|
|
265
|
-
* memory budget. Each axis defaults to 0 = reserve nothing: the task
|
|
266
|
-
* runs subject only to the concurrency-count limit. A pure scheduling
|
|
267
|
-
* hint — the whole object is stripped from the cache key, so tuning a
|
|
268
|
-
* reservation never invalidates a cached result.
|
|
269
|
-
*/
|
|
270
|
-
resources?: ResourcesConfig
|
|
271
328
|
/**
|
|
272
329
|
* Long-running / continuous task (dev server, watcher, daemon).
|
|
273
330
|
* When present, the task is spawned but the runner does NOT wait
|
|
@@ -277,30 +334,29 @@ export interface ExecConfig {
|
|
|
277
334
|
* on ready, not on exit. The orchestrator SIGTERMs every persistent
|
|
278
335
|
* subprocess once the rest of the graph has finished.
|
|
279
336
|
*
|
|
280
|
-
* `cache` is
|
|
281
|
-
*
|
|
282
|
-
* of "done".
|
|
337
|
+
* `cache` is rejected on a persistent task — there's no exit code
|
|
338
|
+
* to cache and the work continues after vx's run notion of "done".
|
|
283
339
|
*/
|
|
284
340
|
persistent?: PersistentConfig
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* Per-task resource reservation (see `ExecConfig.resources`). Grouped so
|
|
289
|
-
* a future axis slots in without new top-level `exec` fields; the loader
|
|
290
|
-
* rejects unknown keys.
|
|
291
|
-
*/
|
|
292
|
-
export interface ResourcesConfig {
|
|
293
|
-
/** CPU cores; fractional is allowed (`0.5`). */
|
|
294
|
-
cpus?: number
|
|
295
|
-
/** Megabytes. `memory: 4096` is four gigabytes. */
|
|
296
|
-
memory?: number
|
|
297
341
|
/**
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
342
|
+
* Confine this task's process: what it may read, write and reach.
|
|
343
|
+
* Nothing is confined unless this is present.
|
|
344
|
+
*
|
|
345
|
+
* It sits in `exec` because it constrains the same spawn `env` does —
|
|
346
|
+
* the environment the child gets, the filesystem and network it gets.
|
|
347
|
+
* A group task has no process,
|
|
348
|
+
* so the field cannot be reached from one.
|
|
349
|
+
*
|
|
350
|
+
* **Policy: fail on violation.** An undeclared touch of the project's own
|
|
351
|
+
* files fails the task, and a failed task is never cached. Reaching
|
|
352
|
+
* OUTSIDE the project is denied but not reported — that is the wall
|
|
353
|
+
* doing its job, not a finding.
|
|
354
|
+
*
|
|
355
|
+
* A persistent task is confined the same way but never REPORTED on: the
|
|
356
|
+
* violation report reads the trace after the process exits, and a server
|
|
357
|
+
* exits at teardown. Enforced, not reported.
|
|
302
358
|
*/
|
|
303
|
-
|
|
359
|
+
sandbox?: SandboxConfig
|
|
304
360
|
}
|
|
305
361
|
|
|
306
362
|
export interface PersistentConfig {
|
|
@@ -354,8 +410,9 @@ export interface CacheOutputs {
|
|
|
354
410
|
* files anywhere in the workspace, including inside other projects'
|
|
355
411
|
* directories. Deliberate escape hatch — prefer project-relative
|
|
356
412
|
* `files` whenever the task can write inside its own dir. Two tasks
|
|
357
|
-
*
|
|
358
|
-
* vx
|
|
413
|
+
* whose workspace outputs provably overlap are refused at graph build,
|
|
414
|
+
* like overlapping `files` (vx cleans declared outputs before a run
|
|
415
|
+
* and before a restore, so the second would delete the first's).
|
|
359
416
|
*/
|
|
360
417
|
workspaceFiles?: string[]
|
|
361
418
|
}
|
package/src/exec/env.ts
CHANGED
|
@@ -42,6 +42,17 @@ export const ESSENTIAL_ENV: readonly string[] = [
|
|
|
42
42
|
'PATHEXT',
|
|
43
43
|
]
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* The two variables vx sets on every task it spawns, over the layers
|
|
47
|
+
* above: the workspace root the run is in and the `project#task` running.
|
|
48
|
+
* `run()` reads them back so a task that shells out to `vx run` in its OWN
|
|
49
|
+
* workspace is refused before it forks without bound (a different
|
|
50
|
+
* workspace — a fixture, a benchmark — is fine). Not in the allowlist:
|
|
51
|
+
* they come from this run, never from the parent environment.
|
|
52
|
+
*/
|
|
53
|
+
export const VX_RUN_WORKSPACE_ENV = 'VX_RUN_WORKSPACE'
|
|
54
|
+
export const VX_RUN_TASK_ENV = 'VX_RUN_TASK'
|
|
55
|
+
|
|
45
56
|
export interface BuildEnvOptions {
|
|
46
57
|
passThrough: readonly string[]
|
|
47
58
|
define: Readonly<Record<string, string>>
|
package/src/exec/executor.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// The per-task execution contract. `execute-task.ts` decides WHAT to run
|
|
2
2
|
// (command, env, sandbox baselines, capture) and hands a fully-resolved
|
|
3
|
-
// request here; an executor decides WHERE/HOW the process runs. Core
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// tasks (`exec.persistent`) never reach an executor: they are local by
|
|
3
|
+
// request here; an executor decides WHERE/HOW the process runs. Core's own
|
|
4
|
+
// `local-executor.ts` is the in-process spawn, and it sits at the TAIL of
|
|
5
|
+
// every executor list — what a plugin executor declines runs here.
|
|
6
|
+
// Persistent tasks (`exec.persistent`) never reach an executor: they are local by
|
|
7
7
|
// construction (a worker cannot hand the submitter a listening port) and
|
|
8
8
|
// stay on `runPersistent`.
|
|
9
9
|
//
|
|
@@ -13,29 +13,33 @@
|
|
|
13
13
|
|
|
14
14
|
import type { CaptureConfig, RunResult } from './runner.js'
|
|
15
15
|
import type { ResolvedSandboxConfig, SandboxViolation } from './sandbox-runtime.js'
|
|
16
|
+
import { UserError } from '../util/index.js'
|
|
16
17
|
|
|
17
18
|
/** Sandbox baselines + the user's resolved sandbox block, when the task is sandboxed. */
|
|
18
19
|
export interface ExecuteSandbox {
|
|
19
20
|
readonly baseAllowRead: readonly string[]
|
|
20
|
-
readonly baseAllowWrite: readonly string[]
|
|
21
21
|
readonly baseDenyRead: readonly string[]
|
|
22
|
-
readonly config: ResolvedSandboxConfig
|
|
23
22
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
* Only denials on a path under this directory are reported. A denial
|
|
24
|
+
* outside it is still ENFORCED — the task cannot leave its project — but
|
|
25
|
+
* that is the wall doing its job, not a finding to fail a run over.
|
|
26
|
+
*/
|
|
27
|
+
readonly reportWithin: string
|
|
28
|
+
/**
|
|
29
|
+
* Canonical directories of the workspace packages core withheld from
|
|
30
|
+
* this task's `node_modules` grant (its key does not answer for them).
|
|
31
|
+
* A denial under one is reported too: the task reached for a dependency,
|
|
32
|
+
* which is the finding, not the wall.
|
|
30
33
|
*/
|
|
31
|
-
readonly
|
|
34
|
+
readonly reportLinked: readonly string[]
|
|
35
|
+
readonly config: ResolvedSandboxConfig
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
/** One declared input file, as the cache key saw it. */
|
|
35
39
|
export interface InputFile {
|
|
36
40
|
/** Workspace-relative POSIX path. */
|
|
37
41
|
readonly path: string
|
|
38
|
-
/** Git blob OID of the WORKTREE bytes
|
|
42
|
+
/** Git blob OID of the WORKTREE bytes — for a symlink, of its target string — the same digest the key folds. */
|
|
39
43
|
readonly digest: string
|
|
40
44
|
}
|
|
41
45
|
|
|
@@ -50,8 +54,8 @@ export interface InputFile {
|
|
|
50
54
|
export interface TaskInputs {
|
|
51
55
|
/** Declared `cache.inputs.files` + `workspaceFiles` (own outputs excluded), sorted by path. */
|
|
52
56
|
readonly files: readonly InputFile[]
|
|
53
|
-
/** Declared `cache.inputs.env` names with their resolved values. */
|
|
54
|
-
readonly env: ReadonlyArray<{ readonly name: string; readonly value: string }>
|
|
57
|
+
/** Declared `cache.inputs.env` names with their resolved values; `undefined` when unset. */
|
|
58
|
+
readonly env: ReadonlyArray<{ readonly name: string; readonly value: string | undefined }>
|
|
55
59
|
/** `cache.inputs.runtime` commands and the output that was folded — a toolchain expectation. */
|
|
56
60
|
readonly runtime: ReadonlyArray<{ readonly command: string; readonly output: string }>
|
|
57
61
|
/** `cache.inputs.workspaceRuntime`, same shape. */
|
|
@@ -169,24 +173,12 @@ export interface TaskPlacement {
|
|
|
169
173
|
readonly command: string
|
|
170
174
|
/**
|
|
171
175
|
* Must run on this machine: the task is persistent, depends (transitively)
|
|
172
|
-
* on a persistent task,
|
|
173
|
-
* executor is never offered such a task.
|
|
176
|
+
* on a persistent or sandboxed task, is sandboxed itself, or declares
|
|
177
|
+
* `exec.remote: false`. A `remote` executor is never offered such a task.
|
|
174
178
|
*/
|
|
175
179
|
readonly pinnedLocal: boolean
|
|
176
180
|
/** Declares `cache` — the only tasks whose input set is described, and so the only ones that can ship. */
|
|
177
181
|
readonly cacheable: boolean
|
|
178
|
-
/**
|
|
179
|
-
* `exec.resources` VERBATIM — CPU cores, megabytes, and the image a
|
|
180
|
-
* worker must be running for this task to be routed to it. All three are
|
|
181
|
-
* requirements MATCHED against what an executor has, never instructions
|
|
182
|
-
* to build a machine: a distributed executor's workers belong to whoever
|
|
183
|
-
* runs the fleet.
|
|
184
|
-
*
|
|
185
|
-
* Placement is where this belongs: `exec.resources` is stripped from the
|
|
186
|
-
* cache key precisely because it decides WHERE a task fits, never what
|
|
187
|
-
* it produces.
|
|
188
|
-
*/
|
|
189
|
-
readonly resources?: Readonly<{ cpus?: number; memory?: number; image?: string }>
|
|
190
182
|
}
|
|
191
183
|
|
|
192
184
|
export interface TaskExecutor {
|
|
@@ -219,6 +211,45 @@ export interface TaskExecutor {
|
|
|
219
211
|
execute(req: ExecuteRequest): Promise<ExecuteResult>
|
|
220
212
|
}
|
|
221
213
|
|
|
214
|
+
/**
|
|
215
|
+
* The result an executor resolves with is a plugin's, so its shape is a
|
|
216
|
+
* boundary: a plugin that resolved `{}` met `res.violations` in core and
|
|
217
|
+
* became "internal error in <task>: TypeError …" — vx's crash, the plugin's
|
|
218
|
+
* bug (2026-09-16). Name the executor and the field instead. The factory's
|
|
219
|
+
* output is checked the same way in `resolveExecutors`.
|
|
220
|
+
*/
|
|
221
|
+
export function assertExecuteResult(
|
|
222
|
+
executor: string,
|
|
223
|
+
taskId: string,
|
|
224
|
+
res: unknown,
|
|
225
|
+
): asserts res is ExecuteResult {
|
|
226
|
+
const fail = (what: string): never => {
|
|
227
|
+
throw new UserError(
|
|
228
|
+
`executor '${executor}' returned an invalid result for ${taskId}: ${what} — a plugin bug, not a task failure`,
|
|
229
|
+
)
|
|
230
|
+
}
|
|
231
|
+
if (typeof res !== 'object' || res === null)
|
|
232
|
+
fail(`${res === null ? 'null' : typeof res} (expected an object)`)
|
|
233
|
+
const r = res as Record<string, unknown>
|
|
234
|
+
for (const key of ['exitCode', 'durationMs']) {
|
|
235
|
+
if (typeof r[key] !== 'number') fail(`${key} is ${typeof r[key]} (expected a number)`)
|
|
236
|
+
}
|
|
237
|
+
for (const key of ['stdout', 'stderr']) {
|
|
238
|
+
if (typeof r[key] !== 'string') fail(`${key} is ${typeof r[key]} (expected a string)`)
|
|
239
|
+
}
|
|
240
|
+
if (!Array.isArray(r['violations']))
|
|
241
|
+
fail(`violations is ${typeof r['violations']} (expected an array)`)
|
|
242
|
+
const outputs = r['outputs'] as { kind?: unknown; materialize?: unknown } | undefined
|
|
243
|
+
if (outputs !== undefined) {
|
|
244
|
+
if (typeof outputs !== 'object' || outputs === null) fail('outputs is not an object')
|
|
245
|
+
else if (outputs.kind === 'deferred') {
|
|
246
|
+
if (typeof outputs.materialize !== 'function')
|
|
247
|
+
fail('outputs.kind is deferred without a materialize()')
|
|
248
|
+
} else if (outputs.kind !== 'disk')
|
|
249
|
+
fail(`outputs.kind is ${String(outputs.kind)} (expected disk or deferred)`)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
222
253
|
/**
|
|
223
254
|
* Place one task: the first executor, in declaration order, that may take
|
|
224
255
|
* it — a `remote` executor is skipped for a `pinnedLocal` task, then
|
|
@@ -234,6 +265,6 @@ export function selectExecutor(
|
|
|
234
265
|
if (executor.accepts === undefined || executor.accepts(task)) return executor
|
|
235
266
|
}
|
|
236
267
|
throw new Error(
|
|
237
|
-
`no executor accepted ${task.taskId} (declared: ${executors.map((e) => e.name).join(', ')}).
|
|
268
|
+
`no executor accepted ${task.taskId} (declared: ${executors.map((e) => e.name).join(', ')}). Core's local executor accepts every task, so this means it is missing from the list.`,
|
|
238
269
|
)
|
|
239
270
|
}
|
package/src/exec/index.ts
CHANGED
|
@@ -1,25 +1,39 @@
|
|
|
1
1
|
// Module contract. Cross-module imports must come through here; see
|
|
2
2
|
// docs/design/module-isolation-2026-06.md and tests/module-boundaries.test.ts.
|
|
3
3
|
|
|
4
|
-
export { buildIsolatedEnv } from './env.js'
|
|
4
|
+
export { buildIsolatedEnv, VX_RUN_TASK_ENV, VX_RUN_WORKSPACE_ENV } from './env.js'
|
|
5
5
|
export {
|
|
6
6
|
runCommand,
|
|
7
7
|
runPersistent,
|
|
8
|
+
PersistentReadyError,
|
|
8
9
|
shellQuote,
|
|
9
10
|
signalExitCode,
|
|
11
|
+
exitSignal,
|
|
10
12
|
type CaptureConfig,
|
|
13
|
+
execWord,
|
|
11
14
|
} from './runner.js'
|
|
12
15
|
export {
|
|
13
16
|
initSandbox,
|
|
14
17
|
probeSandbox,
|
|
18
|
+
unavailableReason,
|
|
15
19
|
resetSandbox,
|
|
16
20
|
resolveSandboxConfig,
|
|
17
21
|
runSandboxed,
|
|
18
|
-
|
|
22
|
+
releaseBridges,
|
|
23
|
+
wrapSandboxedCommand,
|
|
19
24
|
type ResolvedSandboxConfig,
|
|
20
25
|
type SandboxViolation,
|
|
26
|
+
dependencyReason,
|
|
27
|
+
socketPathRefusal,
|
|
28
|
+
thrownReason,
|
|
21
29
|
} from './sandbox-runtime.js'
|
|
30
|
+
export { type DeniedCall } from './sandbox-violations.js'
|
|
31
|
+
export { isMountableLiteral, MOUNT_WILDCARDS, toRealPath } from './sandbox-paths.js'
|
|
32
|
+
export { bindableWrites, punchWalls } from './sandbox-binds.js'
|
|
33
|
+
export { localExecutor } from './local-executor.js'
|
|
34
|
+
export { holdGroups, killTree, untilGroupsGone } from './kill-tree.js'
|
|
22
35
|
export {
|
|
36
|
+
assertExecuteResult,
|
|
23
37
|
selectExecutor,
|
|
24
38
|
type ExecuteRequest,
|
|
25
39
|
type ExecuteResult,
|