@vzn/vx 0.0.16 → 0.0.21

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.
Files changed (138) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +135 -124
  3. package/index.ts +10 -0
  4. package/launcher.mjs +10 -11
  5. package/package.json +11 -7
  6. package/src/bin.ts +14 -4
  7. package/src/cache/archive.ts +659 -0
  8. package/src/cache/cache.ts +769 -1101
  9. package/src/cache/chained-cache.ts +194 -0
  10. package/src/cache/config-evals.ts +89 -0
  11. package/src/cache/file-hashes.ts +262 -0
  12. package/src/cache/git-inputs.ts +637 -0
  13. package/src/cache/index.ts +24 -5
  14. package/src/cache/inputs.ts +373 -456
  15. package/src/cache/layer.ts +604 -0
  16. package/src/cache/layered-cache.ts +96 -43
  17. package/src/cache/output-index.ts +224 -0
  18. package/src/cache/policy.ts +84 -0
  19. package/src/cache/run-history.ts +147 -0
  20. package/src/cache/tar-stream.ts +358 -0
  21. package/src/cache/zstd.ts +160 -0
  22. package/src/cli/cache.ts +75 -21
  23. package/src/cli/completions.ts +104 -0
  24. package/src/cli/core-alias.ts +25 -0
  25. package/src/cli/help.ts +195 -128
  26. package/src/cli/index.ts +116 -46
  27. package/src/cli/info.ts +122 -63
  28. package/src/cli/init.ts +75 -0
  29. package/src/cli/last.ts +172 -0
  30. package/src/cli/lock.ts +2 -1
  31. package/src/cli/plan-format.ts +72 -6
  32. package/src/cli/plugin-commands.ts +103 -0
  33. package/src/cli/run.ts +245 -271
  34. package/src/cli/select.ts +332 -0
  35. package/src/cli/show.ts +144 -69
  36. package/src/cli/upgrade.ts +9 -7
  37. package/src/cli/watch.ts +858 -110
  38. package/src/cli/why.ts +223 -0
  39. package/src/cli/workspace-config.ts +126 -0
  40. package/src/config.ts +187 -150
  41. package/src/exec/env.ts +18 -1
  42. package/src/exec/executor.ts +224 -0
  43. package/src/exec/index.ts +24 -2
  44. package/src/exec/local-executor.ts +41 -0
  45. package/src/exec/runner.ts +166 -40
  46. package/src/exec/sandbox-binds.ts +182 -0
  47. package/src/exec/sandbox-paths.ts +50 -0
  48. package/src/exec/sandbox-runtime.ts +642 -260
  49. package/src/exec/sandbox-violations.ts +280 -0
  50. package/src/graph/index.ts +4 -10
  51. package/src/graph/scheduler.ts +232 -203
  52. package/src/graph/task-graph.ts +222 -8
  53. package/src/index.ts +146 -153
  54. package/src/orchestrator/admission.ts +137 -0
  55. package/src/orchestrator/deferred-outputs.ts +147 -0
  56. package/src/orchestrator/doctor.ts +255 -0
  57. package/src/orchestrator/download-policy.ts +171 -0
  58. package/src/orchestrator/events.ts +72 -8
  59. package/src/orchestrator/execute-task.ts +374 -549
  60. package/src/orchestrator/failure-mode.ts +202 -0
  61. package/src/orchestrator/framed-output.ts +126 -69
  62. package/src/orchestrator/history.ts +126 -59
  63. package/src/orchestrator/hit-restore.ts +221 -0
  64. package/src/orchestrator/index.ts +77 -93
  65. package/src/orchestrator/local-shortcircuit.ts +22 -0
  66. package/src/orchestrator/lockfile-claim.ts +281 -0
  67. package/src/orchestrator/logger.ts +208 -35
  68. package/src/orchestrator/metrics.ts +102 -1388
  69. package/src/orchestrator/miss-save.ts +169 -0
  70. package/src/orchestrator/options.ts +55 -34
  71. package/src/orchestrator/persistent.ts +79 -0
  72. package/src/orchestrator/placement.ts +182 -0
  73. package/src/orchestrator/plan.ts +144 -1
  74. package/src/orchestrator/plugin-host.ts +357 -120
  75. package/src/orchestrator/plugin.ts +298 -58
  76. package/src/orchestrator/prepare.ts +202 -106
  77. package/src/orchestrator/projects.ts +259 -0
  78. package/src/orchestrator/remote-prefetch.ts +12 -8
  79. package/src/orchestrator/run-artifacts.ts +63 -16
  80. package/src/orchestrator/run-context.ts +167 -29
  81. package/src/orchestrator/run-records.ts +149 -0
  82. package/src/orchestrator/run-report.ts +74 -47
  83. package/src/orchestrator/run.ts +534 -417
  84. package/src/orchestrator/sandbox-request.ts +243 -0
  85. package/src/orchestrator/save-lane.ts +47 -0
  86. package/src/orchestrator/signals.ts +111 -0
  87. package/src/orchestrator/stable-keys.ts +129 -31
  88. package/src/orchestrator/status-line.ts +49 -9
  89. package/src/orchestrator/summary.ts +90 -26
  90. package/src/orchestrator/tally.ts +78 -35
  91. package/src/orchestrator/task-hash.ts +111 -15
  92. package/src/orchestrator/task-log-buffer.ts +288 -0
  93. package/src/orchestrator/telemetry-host.ts +34 -6
  94. package/src/orchestrator/telemetry.ts +182 -36
  95. package/src/orchestrator/upstream.ts +72 -31
  96. package/src/util/cgroup.ts +160 -0
  97. package/src/util/edit-distance.ts +70 -0
  98. package/src/util/errors.ts +15 -1
  99. package/src/util/index.ts +15 -2
  100. package/src/util/num.ts +37 -0
  101. package/src/util/paths.ts +64 -0
  102. package/src/util/settle.ts +67 -0
  103. package/src/util/size.ts +5 -1
  104. package/src/util/tail.ts +66 -0
  105. package/src/util/timing.ts +63 -0
  106. package/src/util/verbs.ts +39 -0
  107. package/src/workspace/affected.ts +250 -23
  108. package/src/workspace/config-cache.ts +313 -0
  109. package/src/workspace/config-eval.ts +217 -0
  110. package/src/workspace/config-imports.ts +162 -0
  111. package/src/workspace/config-schema.ts +837 -0
  112. package/src/workspace/filter.ts +65 -10
  113. package/src/workspace/fingerprint.ts +53 -4
  114. package/src/workspace/index.ts +42 -3
  115. package/src/workspace/lockfile.ts +25 -9
  116. package/src/workspace/migrate-scripts.ts +134 -0
  117. package/src/workspace/migration.ts +314 -0
  118. package/src/workspace/nested-dirs.ts +21 -7
  119. package/src/workspace/package-graph.ts +114 -41
  120. package/src/workspace/project-loader.ts +224 -450
  121. package/src/workspace/workspace.ts +298 -72
  122. package/src/cache/cas-backend.ts +0 -97
  123. package/src/cache/digest.ts +0 -47
  124. package/src/cache/tar.ts +0 -343
  125. package/src/cli/backend.ts +0 -23
  126. package/src/cli/mcp-rpc.ts +0 -292
  127. package/src/cli/mcp.ts +0 -92
  128. package/src/cli/migrate-nx.ts +0 -450
  129. package/src/cli/migrate-turbo.ts +0 -364
  130. package/src/cli/migrate.ts +0 -219
  131. package/src/orchestrator/devframe-surface.ts +0 -74
  132. package/src/orchestrator/predict.ts +0 -106
  133. package/src/orchestrator/protocol.ts +0 -156
  134. package/src/orchestrator/resources.ts +0 -59
  135. package/src/orchestrator/run-state.ts +0 -145
  136. package/src/orchestrator/verify.ts +0 -164
  137. package/src/orchestrator/wire-render.ts +0 -92
  138. package/src/orchestrator/wire.ts +0 -212
package/src/config.ts CHANGED
@@ -14,23 +14,29 @@ export interface WorkspaceConfig {
14
14
  */
15
15
  timeout?: number
16
16
  /**
17
- * Plugins registered for this workspace. Each plugin is an
18
- * in-process subscriber on the run event bus, installed once per
19
- * `vx run`. See `docs/design/extension-protocol-2026-06.md` §5.
17
+ * Plugins registered for this workspace, consulted in this order once
18
+ * per `vx run`: each fills any of the pipeline stages and capabilities
19
+ * below, and core's own executor and cache store sit at the tail of
20
+ * every list. See `docs/design/pipeline-2026-09.md`.
20
21
  */
21
22
  plugins?: readonly Plugin[]
22
- /**
23
- * Opt in to history-aware predictive scheduling. When `true` and
24
- * `cache.db` has prior runs, the scheduler picks the next ready
25
- * task by expected remaining critical-path duration (HistoryTable
26
- * p50) instead of the static reverse-deps count.
27
- */
28
- predictive?: boolean
29
23
  }
30
24
 
31
25
  /**
32
- * Structural plugin shape — any subset of the run-level capabilities
33
- * (`backend`/`cache`/`telemetry`/`eventSink`) plus optional
26
+ * The runtime brand `definePlugin` stamps on a plugin: the name of the
27
+ * package it was defined in. The workspace loader accepts a plugin only
28
+ * when this is present and equals `name`, so a plugin's name is its
29
+ * package name and nothing else — no field to set, no override. A
30
+ * registry symbol on purpose: a plugin package imports its own copy of
31
+ * `@vzn/vx` beside a compiled binary's, and the two copies must stamp
32
+ * and check the same key.
33
+ */
34
+ export const PLUGIN_PACKAGE: unique symbol = Symbol.for('vx.plugin.package')
35
+
36
+ /**
37
+ * Structural plugin shape — any subset of the pipeline stages
38
+ * (`config`/`project`/`graph`/`key`/`schedule`), run-level capabilities
39
+ * (`cache`/`executor`/`telemetry`) and CLI `commands`, plus optional
34
40
  * `setup`/`teardown`. The fully-typed `VxPlugin` lives in
35
41
  * `src/orchestrator/plugin.ts` and is what users import; this is a
36
42
  * re-declaration with opaque function types so `config.ts` stays a leaf
@@ -38,15 +44,60 @@ export interface WorkspaceConfig {
38
44
  * before consulting capabilities.
39
45
  */
40
46
  export interface Plugin {
47
+ /** The package name — set by `definePlugin`, never by the plugin. */
41
48
  readonly name: string
42
- backend?(ctx: unknown): unknown
49
+ config?(workspace: unknown, ctx: unknown): unknown
50
+ project?(config: unknown, ctx: unknown): unknown
51
+ graph?(nodes: unknown, ctx: unknown): unknown
52
+ key?(task: unknown, ctx: unknown): unknown
53
+ schedule?(nodes: unknown, ctx: unknown): unknown
54
+ admit?(task: unknown, ctx: unknown): unknown
55
+ readonly commands?: Readonly<Record<string, unknown>>
43
56
  cache?(ctx: unknown): unknown
57
+ executor?(ctx: unknown): unknown
44
58
  telemetry?(ctx: unknown): unknown
45
- eventSink?(ctx: unknown): unknown
46
59
  setup?(ctx: unknown): void | Promise<void>
47
60
  teardown?(): void | Promise<void>
61
+ readonly fingerprint?: unknown
48
62
  }
49
63
 
64
+ /**
65
+ * Every hook a plugin may fill, in pipeline order — THE list. The loader's
66
+ * "must be a function" and "at least one of" checks, the host's stage gate
67
+ * and `vx info`'s seam column all read it, so a stage added here is a stage
68
+ * everywhere: `admit` reached `vx info` a day late (2026-09-12) because
69
+ * that column kept its own copy, and the loader kept a third. The type
70
+ * pin below refuses a list that drifts from `Plugin`'s keys either way.
71
+ */
72
+ export const PLUGIN_HOOKS = [
73
+ 'config',
74
+ 'project',
75
+ 'graph',
76
+ 'key',
77
+ 'fingerprint',
78
+ 'schedule',
79
+ 'admit',
80
+ 'executor',
81
+ 'cache',
82
+ 'telemetry',
83
+ 'setup',
84
+ 'commands',
85
+ 'teardown',
86
+ ] as const
87
+ export type PluginHook = (typeof PLUGIN_HOOKS)[number]
88
+ /** The hooks that are functions; `commands` and `fingerprint` are objects. */
89
+ export const PLUGIN_FUNCTION_HOOKS = PLUGIN_HOOKS.filter(
90
+ (h): h is Exclude<PluginHook, 'commands' | 'fingerprint'> =>
91
+ h !== 'commands' && h !== 'fingerprint',
92
+ )
93
+ type PluginHookKeys = Exclude<keyof Plugin, 'name'>
94
+ type HooksNotListed = Exclude<PluginHookKeys, PluginHook>
95
+ type HooksNotOnPlugin = Exclude<PluginHook, PluginHookKeys>
96
+ // Both `never`, or this line does not compile: the list and the type agree.
97
+ const _pluginHooksMatch: [HooksNotListed, HooksNotOnPlugin] extends [never, never] ? true : never =
98
+ true
99
+ void _pluginHooksMatch
100
+
50
101
  export interface ProjectConfig {
51
102
  /** Tasks declared by this project, keyed by task name. */
52
103
  tasks?: Record<string, TaskConfig>
@@ -57,7 +108,13 @@ export interface TaskConfig {
57
108
  * Optional one-line description of what this task does. Surfaced in
58
109
  * the interactive task picker and the `--dry` text view so users
59
110
  * can grep for what each task does without opening every config.
60
- * Pure metadata — no effect on caching, scheduling, or execution.
111
+ *
112
+ * No effect on scheduling or execution. It DOES participate in the
113
+ * cache key, because the key hashes the whole resolved task config
114
+ * (`docs/caching.md`, step 5) — deliberate: editing a description
115
+ * isn't a correctness change, but a one-off re-run costs little and
116
+ * carving exceptions out of the resolved object is what invites
117
+ * stale hits.
61
118
  */
62
119
  description?: string
63
120
  /**
@@ -89,128 +146,120 @@ export interface TaskConfig {
89
146
  * runs.
90
147
  */
91
148
  cache?: CacheConfig
92
- /**
93
- * Sandbox configuration. **Sandbox is opt-in per task.**
94
- * - omitted entirely → task runs unsandboxed.
95
- * - `sandbox: {}` → opts in with the minimum baseline (only
96
- * resolved inputs readable, only static-prefix of outputs
97
- * writable, network blocked).
98
- * - `sandbox: { ... }` → opts in with the explicit options below.
99
- *
100
- * No inheritance, no workspace defaults, no built-in escapes. The
101
- * sandbox sees STRICTLY the union of resolved `cache.inputs.files`
102
- * + this block's `allowRead` (for reads) and the static prefix of
103
- * `cache.outputs.files` + this block's `allowWrite` (for writes).
104
- * If a task needs `node_modules` or `/tmp`, declare them here
105
- * explicitly.
106
- *
107
- * **Policy: fail on violation.** If the sandbox detects an
108
- * undeclared read/write (macOS log monitor) or the child returns
109
- * non-zero because it couldn't reach a path it expected (Linux
110
- * structural deny), the task fails. No cache is written for a
111
- * failed task.
112
- *
113
- * Activation requires `exec` (group tasks have no command to wrap)
114
- * and is silently skipped for persistent tasks (dev servers need
115
- * unrestricted network + an indefinite process).
116
- */
117
- sandbox?: SandboxConfig
118
149
  }
119
150
 
120
151
  /**
121
- * Per-task sandbox config. Mirrors the user-facing surface of
122
- * `@anthropic-ai/sandbox-runtime`'s `SandboxRuntimeConfig`, minus
123
- * deployment-only fields (binary paths, proxy ports, ripgrep config).
152
+ * What a sandboxed task may do, declared as CAPABILITIES.
153
+ *
154
+ * One shape for the whole policy. You say what the task is allowed to
155
+ * touch; vx decides how each capability is realised — some become sandbox
156
+ * runtime config, some become rules in the OS policy, and one the platform
157
+ * cannot express is an error rather than a silent no-op. None of that
158
+ * reaches the config.
124
159
  *
125
- * Path-list fields (`allowRead`, `allowWrite`, `network.allowUnixSockets`)
126
- * accept:
127
- * - relative paths → resolved against the project dir
128
- * - absolute paths → used as-is (`/etc/passwd`, `/tmp`)
129
- * - tilde paths → expanded against the user's home (`~/.npmrc`)
160
+ * The baseline is always the task's own declarations: it may read its
161
+ * resolved `cache.inputs.files`, write the prefixes of its
162
+ * `cache.outputs.files`, and reach nothing else. Everything here is on top
163
+ * of that.
130
164
  *
131
- * No globs in path lists — bwrap on Linux only accepts path prefixes.
165
+ * Paths are project-relative, absolute, or `~`-expanded — and are path
166
+ * PREFIXES, never globs (a glob cannot be enforced by a mount).
132
167
  */
133
168
  export interface SandboxConfig {
134
- // === Filesystem ===
135
- /**
136
- * Additional read-allowed paths beyond resolved `cache.inputs.files`.
137
- * These are unioned with the declared inputs to form the complete
138
- * allowRead set passed to the sandbox.
139
- */
140
- allowRead?: string[]
141
- /**
142
- * Additional write-allowed paths beyond the static prefix of
143
- * `cache.outputs.files`. Tasks with no declared outputs and no extra
144
- * `allowWrite` can write to nowhere — typical for `lint`/`typecheck`.
145
- */
146
- allowWrite?: string[]
147
- /**
148
- * Permit writes to `.git/config` files. Default `false` — most build
149
- * tools should not be reconfiguring git.
150
- */
151
- allowGitConfig?: boolean
152
-
153
- // === Network ===
154
- /**
155
- * Network policy.
156
- * - `false` (default) — block all outbound traffic.
157
- * - `true` — allow all outbound traffic.
158
- * - object — fine-grained control (domains / sockets / etc.).
159
- */
160
- network?: boolean | SandboxNetworkConfig
161
-
162
- // === Process behavior ===
169
+ /** Capabilities granted on top of the task's own declarations. */
170
+ allow?: SandboxGrants
163
171
  /**
164
- * Allow the task to acquire a pseudo-terminal. Default `false`.
165
- * Needed by tasks that interact with a TTY (rare in CI).
172
+ * Capabilities taken away. Evaluated before `allow`, so a domain in both
173
+ * is denied.
166
174
  */
167
- allowPty?: boolean
175
+ deny?: SandboxDenials
168
176
  /**
169
- * Linux only. Allow nested sandboxes (a sandboxed task spawning
170
- * another sandboxed task). Default `false`.
177
+ * Denials to leave out of the report, in the same shape as `allow`.
178
+ *
179
+ * This does NOT permit anything: the OS still denies the operation, and
180
+ * a command that cannot proceed without it still fails. It says "I know,
181
+ * and it does not affect this task's output" — for a denial you cannot
182
+ * grant away and do not want failing the run.
183
+ *
184
+ * Unlike `allow`, the values are GLOB PATTERNS, and they work the same
185
+ * on every platform: `allow` has to be expressible by the OS sandbox
186
+ * (bwrap mounts a path; it cannot mount a pattern), while `ignore` only
187
+ * has to match a line vx already holds. Relative patterns anchor at the
188
+ * project dir.
189
+ *
190
+ * ```ts
191
+ * ignore: { write: ['*.bun-build'] } // bun --compile stages a random
192
+ * // temp name in the cwd
193
+ * ```
171
194
  */
172
- enableWeakerNestedSandbox?: boolean
195
+ ignore?: SandboxGrants
173
196
  /**
174
- * macOS only. Skip the network namespace; routes traffic via the
175
- * host proxy instead of unsharing networking. Lower overhead but
176
- * weaker isolation. Default `false`.
197
+ * Accept a weaker sandbox when vx itself already runs inside one
198
+ * (a container, another sandbox). Without it, nesting fails fast rather
199
+ * than pretending to confine.
177
200
  */
178
- enableWeakerNetworkIsolation?: boolean
201
+ weakerWhenNested?: boolean
202
+ /** Accept weaker network isolation (host proxy) for lower overhead. */
203
+ weakerNetworkIsolation?: boolean
204
+ }
179
205
 
180
- // === Violation policy ===
181
- /**
182
- * Map of command-substring → list of paths to ignore violations on
183
- * for that command. Lets you silence known-noisy probes (e.g. a
184
- * compiler that statx's many candidate header paths). Per SRT's
185
- * own ignoreViolations field.
186
- */
187
- ignoreViolations?: Record<string, string[]>
206
+ export interface SandboxGrants {
207
+ /** Readable path prefixes, beyond the resolved `cache.inputs.files`. */
208
+ read?: string[]
209
+ /** Writable path prefixes, beyond the `cache.outputs.files` prefixes. */
210
+ write?: string[]
211
+ /**
212
+ * Reachable domains. `true` allows all, `['*.example.com']` a pattern.
213
+ * Omitted means no network at all.
214
+ */
215
+ network?: true | string[]
216
+ /**
217
+ * Kernel information the task may query, by info type — the name that
218
+ * appears in a denial, e.g. `vfs.disk-space` (every Bun and git process
219
+ * probes free space at startup).
220
+ */
221
+ systemInfo?: string[]
222
+ /** Unix socket paths the task may connect to. `true` allows any. */
223
+ unixSockets?: true | string[]
224
+ /**
225
+ * Bind and reach localhost ports — a test that boots a server for
226
+ * itself. A port LIST additionally makes those ports reachable from
227
+ * outside the sandbox (a dev server another task or the developer
228
+ * talks to): on Linux, where a sandboxed task lives in its own network
229
+ * namespace, each listed port is bridged out to the host's loopback;
230
+ * on macOS the host already sees them and the list means `true`.
231
+ */
232
+ localBinding?: boolean | number[]
233
+ /** macOS: Mach service names the task may look up. */
234
+ machLookup?: string[]
235
+ /** Acquire a TTY. Rare outside interactive tools. */
236
+ pty?: boolean
237
+ /** Write to `.git/config`. Most build tools should not. */
238
+ gitConfig?: boolean
188
239
  }
189
240
 
190
- export interface SandboxNetworkConfig {
191
- /**
192
- * Domain patterns the task may reach. Wildcards: `*.example.com`,
193
- * `*` (allow all).
194
- */
195
- allowedDomains?: string[]
196
- /** Domains explicitly blocked, evaluated before allowedDomains. */
197
- deniedDomains?: string[]
198
- /** Unix socket paths the task may connect to. */
199
- allowUnixSockets?: string[]
200
- /** Allow any Unix socket. Use with care. */
201
- allowAllUnixSockets?: boolean
202
- /**
203
- * Allow binding to local ports (e.g. a test that boots a server on
204
- * localhost for itself to query).
205
- */
206
- allowLocalBinding?: boolean
207
- /** macOS only. Mach service names the task may look up. */
208
- allowMachLookup?: string[]
241
+ export interface SandboxDenials {
242
+ /** Domains blocked even if `allow.network` would admit them. */
243
+ network?: string[]
209
244
  }
210
245
 
211
246
  export interface ExecConfig {
212
247
  /** Shell command to run, from the project's directory. */
213
248
  command: string
249
+ /**
250
+ * `false` pins the task to this machine: it is never offered to a remote
251
+ * executor (it talks to a local daemon, the network, Docker, …). Default
252
+ * `true`. Tasks that are persistent, or depend on one, are pinned
253
+ * regardless — a worker cannot reach a port on the submitter.
254
+ *
255
+ * `'only'` is the inverse pin: the task exists to produce a REMOTE input
256
+ * tree (the canonical case is `pnpm install` feeding remote workers) and is
257
+ * a NO-OP on this machine — never executed locally, its declared outputs
258
+ * never cleaned or restored on this disk. With no remote executor declared
259
+ * the task succeeds without running and dependents use whatever ambient
260
+ * state the machine has, exactly as they did before the field existed.
261
+ */
262
+ remote?: boolean | 'only'
214
263
  /** Environment exposed to the child process. */
215
264
  env?: ExecEnv
216
265
  /**
@@ -239,16 +288,6 @@ export interface ExecConfig {
239
288
  * default, including `retries: 0`.
240
289
  */
241
290
  retries?: number
242
- /**
243
- * Resource RESERVATIONS for admission control — NOT enforcement: vx
244
- * does not cgroup-limit, nice, or kill the task; it only decides what
245
- * to co-schedule so concurrent reservations never exceed the CPU /
246
- * memory budget. Each axis defaults to 0 = reserve nothing: the task
247
- * runs subject only to the concurrency-count limit. A pure scheduling
248
- * hint — the whole object is stripped from the cache key, so tuning a
249
- * reservation never invalidates a cached result.
250
- */
251
- resources?: ResourcesConfig
252
291
  /**
253
292
  * Long-running / continuous task (dev server, watcher, daemon).
254
293
  * When present, the task is spawned but the runner does NOT wait
@@ -258,32 +297,29 @@ export interface ExecConfig {
258
297
  * on ready, not on exit. The orchestrator SIGTERMs every persistent
259
298
  * subprocess once the rest of the graph has finished.
260
299
  *
261
- * `cache` is silently ignored for persistent tasks — there's no
262
- * exit code to cache and the work continues after vx's run notion
263
- * of "done".
300
+ * `cache` is rejected on a persistent task — there's no exit code
301
+ * to cache and the work continues after vx's run notion of "done".
264
302
  */
265
303
  persistent?: PersistentConfig
266
- }
267
-
268
- /**
269
- * Per-task resource reservation (see `ExecConfig.resources`). Grouped so
270
- * a future axis slots in without new top-level `exec` fields; the loader
271
- * rejects unknown keys.
272
- */
273
- export interface ResourcesConfig {
274
304
  /**
275
- * CPU units (fractional ok, e.g. `0.5`), or a `"<n>%"` string of the
276
- * CPU budget (the run's `concurrency`). `cpus: "50%"` on a budget of 8
277
- * reserves 4 units.
278
- */
279
- cpus?: number | string
280
- /**
281
- * Bytes, a size string (`"2GB"`, `"512MB"` — K/M/G/T, powers of 1024),
282
- * or a `"<n>%"` string of the memory budget (`os.totalmem()` unless
283
- * overridden with `--memory` — pass `--memory` in cgroup-limited
284
- * containers, where `os.totalmem()` reports the HOST's RAM).
305
+ * Confine this task's process: what it may read, write and reach.
306
+ * Nothing is confined unless this is present.
307
+ *
308
+ * It sits in `exec` because it constrains the same spawn `env` and
309
+ * `resources` do — the environment the child gets, the CPU and memory it
310
+ * gets, the filesystem and network it gets. A group task has no process,
311
+ * so the field cannot be reached from one.
312
+ *
313
+ * **Policy: fail on violation.** An undeclared touch of the project's own
314
+ * files fails the task, and a failed task is never cached. Reaching
315
+ * OUTSIDE the project is denied but not reported — that is the wall
316
+ * doing its job, not a finding.
317
+ *
318
+ * A persistent task is confined the same way but never REPORTED on: the
319
+ * violation report reads the trace after the process exits, and a server
320
+ * exits at teardown. Enforced, not reported.
285
321
  */
286
- memory?: number | string
322
+ sandbox?: SandboxConfig
287
323
  }
288
324
 
289
325
  export interface PersistentConfig {
@@ -337,8 +373,9 @@ export interface CacheOutputs {
337
373
  * files anywhere in the workspace, including inside other projects'
338
374
  * directories. Deliberate escape hatch — prefer project-relative
339
375
  * `files` whenever the task can write inside its own dir. Two tasks
340
- * declaring overlapping workspace outputs is user responsibility;
341
- * vx does not police it.
376
+ * whose workspace outputs provably overlap are refused at graph build,
377
+ * like overlapping `files` (vx cleans declared outputs before a run
378
+ * and before a restore, so the second would delete the first's).
342
379
  */
343
380
  workspaceFiles?: string[]
344
381
  }
package/src/exec/env.ts CHANGED
@@ -8,7 +8,13 @@ import path from 'node:path'
8
8
  // 3. define: explicit name=value pairs from the task config.
9
9
  // 4. binPaths prepended to PATH (after all the above resolve PATH).
10
10
 
11
- const ESSENTIAL_ENV: readonly string[] = [
11
+ /**
12
+ * Exported so the schema doc's enumeration of it can be PINNED against the
13
+ * real list: a hand-copied allowlist is how the two drift, and a reader
14
+ * asking "what does my build script actually see?" is asking a
15
+ * security-shaped question that a stale list answers wrongly.
16
+ */
17
+ export const ESSENTIAL_ENV: readonly string[] = [
12
18
  'PATH',
13
19
  'HOME',
14
20
  'SHELL',
@@ -36,6 +42,17 @@ const ESSENTIAL_ENV: readonly string[] = [
36
42
  'PATHEXT',
37
43
  ]
38
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
+
39
56
  export interface BuildEnvOptions {
40
57
  passThrough: readonly string[]
41
58
  define: Readonly<Record<string, string>>