@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
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
// Signal a task and everything it forked. Every task child is spawned
|
|
2
|
+
// `detached` — its own session and process group — so the group IS the
|
|
3
|
+
// task's tree: the shell, what the shell backgrounded (`server &`), a
|
|
4
|
+
// test runner's workers. Signalling the child alone left all of that
|
|
5
|
+
// alive: a timed-out `sh -c "x & y"` killed the shell and orphaned `x`,
|
|
6
|
+
// and a Ctrl-C did the same for every compound command (2026-09-16). A
|
|
7
|
+
// group signal reaches a member whose parent already died, too — the
|
|
8
|
+
// group outlives its leader — so the order of death does not matter.
|
|
9
|
+
// What still escapes is a daemon that calls setsid itself (the residual
|
|
10
|
+
// every non-cgroup runner shares); under a sandbox the pid namespace
|
|
11
|
+
// takes even that.
|
|
12
|
+
|
|
13
|
+
import { closeSync, readdirSync, readFileSync, writeSync } from 'node:fs'
|
|
14
|
+
import { executablePath, procfsIsOwn } from '../util/index.js'
|
|
15
|
+
|
|
16
|
+
export type Child = ReturnType<typeof Bun.spawn>
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A child whose polite signals go down a pipe instead of to its group: a
|
|
20
|
+
* Linux sandboxed task. The group there is bwrap's, whose monitor dies of
|
|
21
|
+
* a SIGTERM and takes the namespace down with SIGKILL (`--die-with-parent`),
|
|
22
|
+
* so the command's own cleanup never ran; a shell inside reads the
|
|
23
|
+
* signal's name off the pipe and signals the command's group (item 752).
|
|
24
|
+
* SIGKILL still goes to the group: bwrap's death is the hard stop.
|
|
25
|
+
*/
|
|
26
|
+
const channels = new Map<Child, number>()
|
|
27
|
+
|
|
28
|
+
/** Route `child`'s SIGINT and SIGTERM through `fd`, an end vx owns. */
|
|
29
|
+
export function signalThrough(child: Child, fd: number): void {
|
|
30
|
+
channels.set(child, fd)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Close `child`'s channel once it has exited. The entry goes before the
|
|
35
|
+
* descriptor, so a later kill never writes to a number the process has
|
|
36
|
+
* since reused.
|
|
37
|
+
*/
|
|
38
|
+
export function closeSignalChannel(child: Child): void {
|
|
39
|
+
const fd = channels.get(child)
|
|
40
|
+
if (fd === undefined) return
|
|
41
|
+
channels.delete(child)
|
|
42
|
+
try {
|
|
43
|
+
closeSync(fd)
|
|
44
|
+
} catch {
|
|
45
|
+
// already closed
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The groups vx must take down if it dies without running a line: a
|
|
51
|
+
* `kill -9`, the OOM killer. Nothing in vx runs then, so the kill has to
|
|
52
|
+
* live outside it — one `sh` per vx process, its own group, reading a
|
|
53
|
+
* pipe only vx holds the write end of. `+<pgid>` and `-<pgid>` lines keep
|
|
54
|
+
* its list; when vx dies the kernel closes the pipe, the read hits EOF,
|
|
55
|
+
* and every group still listed is SIGKILLed — what bwrap's
|
|
56
|
+
* `--die-with-parent` does for a Linux sandboxed task, extended to the
|
|
57
|
+
* rest. A group signal reaches what the task forked, which
|
|
58
|
+
* `PR_SET_PDEATHSIG` never did (kill-tree.md). Started just before the
|
|
59
|
+
* first spawn, so a run that spawns nothing (a warm run) never starts
|
|
60
|
+
* it; a normal exit leaves the list empty, so the EOF then kills nothing.
|
|
61
|
+
* `undefined` until then, `null` once it could not start or write: the
|
|
62
|
+
* guard is best-effort, and its failure is the documented limit, never a
|
|
63
|
+
* failed task.
|
|
64
|
+
*/
|
|
65
|
+
let guardFd: number | null | undefined
|
|
66
|
+
|
|
67
|
+
const GUARD_SCRIPT = [
|
|
68
|
+
"g=' '",
|
|
69
|
+
'while IFS= read -r l; do',
|
|
70
|
+
' case $l in',
|
|
71
|
+
' +*) g="$g${l#+} " ;;',
|
|
72
|
+
' -*) p=${l#-}; case $g in *" $p "*) g="${g%% $p *} ${g#* $p }" ;; esac ;;',
|
|
73
|
+
' esac',
|
|
74
|
+
'done <&3',
|
|
75
|
+
'for p in $g; do kill -s KILL -- "-$p"; done 2>/dev/null',
|
|
76
|
+
].join('\n')
|
|
77
|
+
|
|
78
|
+
function startGuard(): void {
|
|
79
|
+
if (guardFd !== undefined) return
|
|
80
|
+
guardFd = null
|
|
81
|
+
try {
|
|
82
|
+
const guard = Bun.spawn([executablePath('sh'), '-c', GUARD_SCRIPT], {
|
|
83
|
+
argv0: 'vx-group-guard',
|
|
84
|
+
stdio: ['ignore', 'ignore', 'ignore', 'pipe'],
|
|
85
|
+
// Out of vx's group, so a Ctrl-C at the terminal leaves it to the
|
|
86
|
+
// pipe; vx's own teardown owns that path.
|
|
87
|
+
detached: true,
|
|
88
|
+
})
|
|
89
|
+
guard.unref()
|
|
90
|
+
guardFd = guard.stdio[3] as number
|
|
91
|
+
} catch {
|
|
92
|
+
// The limit as it was: nothing takes the groups down.
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function guardWrite(line: string): void {
|
|
97
|
+
if (typeof guardFd !== 'number') return
|
|
98
|
+
try {
|
|
99
|
+
writeSync(guardFd, line)
|
|
100
|
+
} catch {
|
|
101
|
+
guardFd = null
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Spawn a task child and list its group for the guard to kill if vx dies
|
|
107
|
+
* holding it. The guard starts BEFORE the spawn: started after it, the
|
|
108
|
+
* guard's own spawn was a window in which the task ran and a `kill -9`
|
|
109
|
+
* of vx found its group unlisted — under a traced sandbox, a third of
|
|
110
|
+
* the kills landed there. What is left is the step from the spawn's
|
|
111
|
+
* return to one pipe write.
|
|
112
|
+
*/
|
|
113
|
+
export function spawnGuarded(spawn: () => Child): Child {
|
|
114
|
+
startGuard()
|
|
115
|
+
const child = spawn()
|
|
116
|
+
if (child.pid > 0) guardWrite(`+${child.pid}\n`)
|
|
117
|
+
return child
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Strike `child`'s group from the guard's list: vx is done with it. What
|
|
122
|
+
* it left running is left as before, and a pid the kernel reuses is
|
|
123
|
+
* never killed on the old task's account. A group a teardown holds is
|
|
124
|
+
* struck when the teardown lets it go.
|
|
125
|
+
*/
|
|
126
|
+
export function releaseGroup(child: Child): void {
|
|
127
|
+
if (!(child.pid > 0)) return
|
|
128
|
+
const hold = holds.get(child.pid)
|
|
129
|
+
if (hold !== undefined) hold.released = true
|
|
130
|
+
else guardWrite(`-${child.pid}\n`)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Groups a teardown is still taking down: how many hold each, and whether its runner let it go. */
|
|
134
|
+
const holds = new Map<number, { count: number; released: boolean }>()
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Keep `children`'s groups on the guard's list until the returned
|
|
138
|
+
* function runs. A teardown signals a group and waits out a grace for it,
|
|
139
|
+
* and the runner lets a group go when its LEADER exits: a shell that died
|
|
140
|
+
* on the signal while a child that ignores it ran on released the group
|
|
141
|
+
* mid-grace, and a `kill -9` of vx there left that child to nobody (item
|
|
142
|
+
* 865). The caller lets go once its SIGKILL sweep has settled.
|
|
143
|
+
*/
|
|
144
|
+
export function holdGroups(children: readonly Child[]): () => void {
|
|
145
|
+
const pids = children.map((c) => c.pid).filter((pid) => pid > 0)
|
|
146
|
+
for (const pid of pids) {
|
|
147
|
+
const hold = holds.get(pid)
|
|
148
|
+
if (hold === undefined) holds.set(pid, { count: 1, released: false })
|
|
149
|
+
else hold.count++
|
|
150
|
+
}
|
|
151
|
+
return () => {
|
|
152
|
+
for (const pid of pids) {
|
|
153
|
+
const hold = holds.get(pid)
|
|
154
|
+
if (hold === undefined) continue
|
|
155
|
+
if (--hold.count > 0) continue
|
|
156
|
+
holds.delete(pid)
|
|
157
|
+
if (hold.released) guardWrite(`-${pid}\n`)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Children whose group was empty when their leader exited. Its number is
|
|
164
|
+
* free from then on, and the kernel hands it to the next process that
|
|
165
|
+
* needs it: a persistent server stays in the run's registry after it
|
|
166
|
+
* exits (keep-alive reports it), and a teardown that signalled `-pid`
|
|
167
|
+
* there could reach whatever group holds the number now (item 874). A
|
|
168
|
+
* group that still had a member keeps its number reserved, and is
|
|
169
|
+
* signalled as before.
|
|
170
|
+
*/
|
|
171
|
+
const goneGroups = new WeakSet<Child>()
|
|
172
|
+
|
|
173
|
+
/** Note, as `child`'s leader exits, whether its group went with it. */
|
|
174
|
+
export function markGroupIfGone(child: Child): void {
|
|
175
|
+
if (!groupAlive(child)) goneGroups.add(child)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function killTree(child: Child, signal: 'SIGINT' | 'SIGTERM' | 'SIGKILL'): void {
|
|
179
|
+
// A pid of 0 would name OUR group (kill(0)): a child that never
|
|
180
|
+
// spawned has nothing to kill.
|
|
181
|
+
if (!(child.pid > 0) || goneGroups.has(child)) return
|
|
182
|
+
const fd = signal === 'SIGKILL' ? undefined : channels.get(child)
|
|
183
|
+
if (fd !== undefined) {
|
|
184
|
+
try {
|
|
185
|
+
writeSync(fd, `${signal.slice(3)}\n`)
|
|
186
|
+
return
|
|
187
|
+
} catch {
|
|
188
|
+
// The reader is gone (EPIPE): so is the command. The group signal
|
|
189
|
+
// below finds what is left, or nothing.
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
process.kill(-child.pid, signal)
|
|
194
|
+
} catch (err) {
|
|
195
|
+
// ESRCH: the whole group is gone. Anything else (a group that is not
|
|
196
|
+
// ours to signal): the child itself, as before.
|
|
197
|
+
if ((err as NodeJS.ErrnoException).code === 'ESRCH') return
|
|
198
|
+
child.kill(signal)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* How often a group whose leader has exited is asked again whether any
|
|
204
|
+
* member is left. Only a group that outlived its leader is polled.
|
|
205
|
+
*/
|
|
206
|
+
const GROUP_POLL_MS = 20
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Wait until every child's process group is gone, or `graceMs` passes.
|
|
210
|
+
* Resolves with the children whose group still has a live member: the
|
|
211
|
+
* ones to SIGKILL. The leader's exit is not the end of the tree — a
|
|
212
|
+
* `server & wait` shell dies on SIGTERM while a server that ignores it
|
|
213
|
+
* keeps the group, and the task's pipe, alive — so after the leaders the
|
|
214
|
+
* groups themselves are polled, and only while one is left.
|
|
215
|
+
*/
|
|
216
|
+
export async function untilGroupsGone(
|
|
217
|
+
children: readonly Child[],
|
|
218
|
+
graceMs: number,
|
|
219
|
+
): Promise<Child[]> {
|
|
220
|
+
const deadline = Date.now() + graceMs
|
|
221
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
222
|
+
// Not unref'd: it is what guarantees progress when every other handle
|
|
223
|
+
// has drained, and the grace is bounded either way.
|
|
224
|
+
await Promise.race([
|
|
225
|
+
Promise.allSettled(children.map((c) => c.exited)),
|
|
226
|
+
new Promise<void>((resolve) => {
|
|
227
|
+
timer = setTimeout(resolve, graceMs)
|
|
228
|
+
}),
|
|
229
|
+
])
|
|
230
|
+
if (timer !== undefined) clearTimeout(timer)
|
|
231
|
+
let left = children.filter(groupAlive)
|
|
232
|
+
while (left.length > 0 && Date.now() < deadline) {
|
|
233
|
+
await Bun.sleep(Math.min(GROUP_POLL_MS, deadline - Date.now()))
|
|
234
|
+
left = left.filter(groupAlive)
|
|
235
|
+
}
|
|
236
|
+
return left
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Does any process of the child's group still run? `kill(-pgid, 0)`
|
|
241
|
+
* answers for the group but counts a zombie, and an orphaned member that
|
|
242
|
+
* died on the SIGTERM waits as one until init reaps it — 1 to 2 s under a
|
|
243
|
+
* container's init (measured 2026-09-24) — so on Linux a group the kernel
|
|
244
|
+
* still knows is read from /proc, where a zombie says so — when /proc is
|
|
245
|
+
* this namespace's. One mounted for another reads as no member at all,
|
|
246
|
+
* and a group that ignored the SIGTERM went un-killed; there the group
|
|
247
|
+
* signal is the answer and the grace ends the wait.
|
|
248
|
+
*/
|
|
249
|
+
function groupAlive(child: Child): boolean {
|
|
250
|
+
if (!(child.pid > 0) || goneGroups.has(child)) return false
|
|
251
|
+
try {
|
|
252
|
+
process.kill(-child.pid, 0)
|
|
253
|
+
} catch (err) {
|
|
254
|
+
return (err as NodeJS.ErrnoException).code === 'EPERM'
|
|
255
|
+
}
|
|
256
|
+
return !procfsIsOwn() || groupHasLiveMember(child.pid)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function groupHasLiveMember(pgid: number): boolean {
|
|
260
|
+
let entries: string[]
|
|
261
|
+
try {
|
|
262
|
+
entries = readdirSync('/proc')
|
|
263
|
+
} catch {
|
|
264
|
+
return true
|
|
265
|
+
}
|
|
266
|
+
for (const entry of entries) {
|
|
267
|
+
const first = entry.charCodeAt(0)
|
|
268
|
+
if (first < 48 || first > 57) continue
|
|
269
|
+
let stat: string
|
|
270
|
+
try {
|
|
271
|
+
stat = readFileSync(`/proc/${entry}/stat`, 'utf8')
|
|
272
|
+
} catch {
|
|
273
|
+
continue
|
|
274
|
+
}
|
|
275
|
+
// `<pid> (<comm>) <state> <ppid> <pgrp> …` — comm may hold spaces and
|
|
276
|
+
// parens, so the fields start after the LAST ')'.
|
|
277
|
+
const fields = stat.slice(stat.lastIndexOf(')') + 2).split(' ', 3)
|
|
278
|
+
if (Number(fields[2]) === pgid && fields[0] !== 'Z' && fields[0] !== 'X') return true
|
|
279
|
+
}
|
|
280
|
+
return false
|
|
281
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Core's own local executor: spawn the task's command here, in this
|
|
2
|
+
// process. Not a plugin — "run it on this machine" is not a capability
|
|
3
|
+
// someone else supplies differently, it is what running MEANS when no
|
|
4
|
+
// plugin claims the task, so it sits at the TAIL of every executor list
|
|
5
|
+
// (owner, 2026-09-05). An executor plugin places work ELSEWHERE; its
|
|
6
|
+
// absence is "here".
|
|
7
|
+
import { runCommand } from './runner.js'
|
|
8
|
+
import { runSandboxed } from './sandbox-runtime.js'
|
|
9
|
+
import type { TaskExecutor } from './executor.js'
|
|
10
|
+
|
|
11
|
+
/** The local executor. Accepts every task. */
|
|
12
|
+
export function localExecutor(): TaskExecutor {
|
|
13
|
+
return {
|
|
14
|
+
name: 'local',
|
|
15
|
+
async execute(req) {
|
|
16
|
+
const common = {
|
|
17
|
+
command: req.command,
|
|
18
|
+
cwd: req.cwd,
|
|
19
|
+
env: req.env,
|
|
20
|
+
forwardArgs: req.forwardArgs,
|
|
21
|
+
onStdout: req.onStdout,
|
|
22
|
+
onStderr: req.onStderr,
|
|
23
|
+
capture: req.capture,
|
|
24
|
+
...(req.liveChildren !== undefined ? { liveChildren: req.liveChildren } : {}),
|
|
25
|
+
...(req.timeoutMs !== undefined ? { timeoutMs: req.timeoutMs } : {}),
|
|
26
|
+
}
|
|
27
|
+
if (req.sandbox === undefined) {
|
|
28
|
+
const res = await runCommand(common)
|
|
29
|
+
return { ...res, violations: [] }
|
|
30
|
+
}
|
|
31
|
+
return runSandboxed({
|
|
32
|
+
...common,
|
|
33
|
+
baseAllowRead: req.sandbox.baseAllowRead,
|
|
34
|
+
baseDenyRead: req.sandbox.baseDenyRead,
|
|
35
|
+
reportWithin: req.sandbox.reportWithin,
|
|
36
|
+
reportLinked: req.sandbox.reportLinked,
|
|
37
|
+
config: req.sandbox.config,
|
|
38
|
+
})
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
}
|