@bakers-build/conductor 0.4.1-next.499 → 0.4.1-next.536
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/dist/src/commands/conductor.js +25 -0
- package/dist/src/contract.d.ts +1 -0
- package/dist/src/contract.js +1 -0
- package/dist/src/core/conductor.js +17 -3
- package/dist/src/plugins/agent.d.ts +8 -0
- package/dist/src/plugins/remote.d.ts +2 -0
- package/dist/src/plugins/sandbox-preflight.d.ts +35 -0
- package/dist/src/plugins/sandbox-preflight.js +56 -0
- package/package.json +3 -3
|
@@ -9,6 +9,7 @@ import { composeConductorConfig, loadConductorConfig } from '../config.js';
|
|
|
9
9
|
import { Conductor } from '../core/conductor.js';
|
|
10
10
|
import { hostKeyOf } from '../core/host-key.js';
|
|
11
11
|
import { selectAgents, selectExecutor, selectRemote, selectWorkspace, } from '../plugins/plugins.js';
|
|
12
|
+
import { sandboxFailures } from '../plugins/sandbox-preflight.js';
|
|
12
13
|
/** Default host bases for connection/plugin resolution when none is injected. */
|
|
13
14
|
function defaultHost() {
|
|
14
15
|
return { resolveBases: [import.meta.url, `${process.cwd()}/`] };
|
|
@@ -202,6 +203,16 @@ async function cmdPoll(deps, log = {}) {
|
|
|
202
203
|
await conductor.start();
|
|
203
204
|
await conductor.tick();
|
|
204
205
|
}
|
|
206
|
+
/** `bakers oven check`: whether each enabled agent can start its sandbox. */
|
|
207
|
+
async function cmdCheck(deps) {
|
|
208
|
+
const config = await resolveConfig(deps);
|
|
209
|
+
for (const agent of deps.agents ?? (await selectAgents(config))) {
|
|
210
|
+
const [failure] = await sandboxFailures([agent]);
|
|
211
|
+
console.log(failure ?? `${agent.alias ?? agent.id}: sandbox ok`);
|
|
212
|
+
if (failure)
|
|
213
|
+
process.exitCode = 1;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
205
216
|
async function cmdReap(deps, log = {}) {
|
|
206
217
|
const config = await resolveConfig(deps, undefined, true);
|
|
207
218
|
const checkoutRoot = await checkoutRootOf(config);
|
|
@@ -562,6 +573,14 @@ async function cmdStartBody(deps, log, force = false) {
|
|
|
562
573
|
const logger = loggerFor(checkoutRoot, log);
|
|
563
574
|
if (!(await ensureAuthenticated(config, logger)))
|
|
564
575
|
return;
|
|
576
|
+
// The loop withholds claims while this fails; say so before it starts.
|
|
577
|
+
// Best-effort: an agent config the loop cannot load fails there, as before.
|
|
578
|
+
try {
|
|
579
|
+
for (const line of await sandboxFailures(deps.agents ?? (await selectAgents(config)))) {
|
|
580
|
+
console.warn(`warning: ${line}`);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
catch { }
|
|
565
584
|
const remote = await resolveRemote(deps, config);
|
|
566
585
|
const id = conductorIdOf(config);
|
|
567
586
|
const thisHostKey = hostKeyOf(config);
|
|
@@ -783,6 +802,12 @@ Examples:
|
|
|
783
802
|
.action(async function () {
|
|
784
803
|
await cmdPoll(deps, logOptsOf(this));
|
|
785
804
|
});
|
|
805
|
+
conductor
|
|
806
|
+
.command('check')
|
|
807
|
+
.description('check that every enabled agent can start its sandbox on this machine (exit 1 if one cannot)')
|
|
808
|
+
.action(async () => {
|
|
809
|
+
await cmdCheck(deps);
|
|
810
|
+
});
|
|
786
811
|
conductor
|
|
787
812
|
.command('clean')
|
|
788
813
|
.description('tear down herdr worktrees of finished-but-uncleaned tickets (recovers orphans a live tick missed: conductor was down at done, restarted, or reassigned)')
|
package/dist/src/contract.d.ts
CHANGED
|
@@ -9,5 +9,6 @@ export { resolveAgent, selectAgent, selectAgents, selectExecutor, selectRemote,
|
|
|
9
9
|
export { type ConductorAddonEntry, type ConductorContributions, narrowConductorContributions, type Preset, } from './plugins/preset.js';
|
|
10
10
|
export type { HerdrHost } from './plugins/process-host.js';
|
|
11
11
|
export type { ActiveRun, AddonDefinitionV2, AddonRegistration, BbRemote, ClaimedRun, ClaimOptions, ClaimWithheld, ConductorRegistration, ConductorStatus, FinalizableRun, FrozenTicket, InboxMessage, InboxSnapshot, RunMetrics, RunWriteAttributes, Ticket, TicketMetric, UncleanTicket, } from './plugins/remote.js';
|
|
12
|
+
export { installCommand, lastLine, type SandboxHost, type SandboxPreflight, sandboxFailures, systemSandboxHost, } from './plugins/sandbox-preflight.js';
|
|
12
13
|
export type { BbWorkspace, EnsuredWorkspace, } from './plugins/workspace.js';
|
|
13
14
|
export type { ConductorEngineConfig, ConductorFileConfig, ConductorSettings, } from './types.js';
|
package/dist/src/contract.js
CHANGED
|
@@ -17,3 +17,4 @@ export { detectKnownOverrides, resolveAgentCliOptions, resolveSecurityOptions, S
|
|
|
17
17
|
export { loadInbox, validateRequestPayload, } from './plugins/message.js';
|
|
18
18
|
export { resolveAgent, selectAgent, selectAgents, selectExecutor, selectRemote, selectWorkspace, } from './plugins/plugins.js';
|
|
19
19
|
export { narrowConductorContributions, } from './plugins/preset.js';
|
|
20
|
+
export { installCommand, lastLine, sandboxFailures, systemSandboxHost, } from './plugins/sandbox-preflight.js';
|
|
@@ -3,6 +3,7 @@ import { basename, dirname, join } from 'node:path';
|
|
|
3
3
|
import { shellQuote, stemForConfigFile, } from '@bakers-build/core';
|
|
4
4
|
import { DEFAULT_ENV_DENY, matchesEnvDeny, } from '../plugins/agent.js';
|
|
5
5
|
import { resolveAgent } from '../plugins/plugins.js';
|
|
6
|
+
import { sandboxFailures } from '../plugins/sandbox-preflight.js';
|
|
6
7
|
import { ActionQueue } from './action-queue.js';
|
|
7
8
|
import { attemptFamily } from './attempt.js';
|
|
8
9
|
import { hostKeyOf } from './host-key.js';
|
|
@@ -278,6 +279,9 @@ export class Conductor {
|
|
|
278
279
|
const log = this.inboxAddons.empty ? this.logger.warn : this.logger.error;
|
|
279
280
|
log.call(this.logger, { conductorId: this.id, err: String(err) }, 'addon registration failed');
|
|
280
281
|
}
|
|
282
|
+
for (const line of await sandboxFailures(this.agents)) {
|
|
283
|
+
this.logger.error({ conductorId: this.id }, line);
|
|
284
|
+
}
|
|
281
285
|
}
|
|
282
286
|
/** One tick that also waits for its actions: `bakers oven once`, tests. */
|
|
283
287
|
async tick() {
|
|
@@ -335,9 +339,15 @@ export class Conductor {
|
|
|
335
339
|
await this.step('reap', () => this.enqueueReap());
|
|
336
340
|
await this.step('freeze', () => this.observeFreezes());
|
|
337
341
|
await runInbox(this.remote, this.inboxAddons, this.id, this.logger, this.queue);
|
|
342
|
+
// A run that cannot start its sandbox would only fail and count against
|
|
343
|
+
// the retry cap; claim nothing until every enabled agent can start.
|
|
344
|
+
const unavailable = machineLoad < maxParallel ? await sandboxFailures(this.agents) : [];
|
|
345
|
+
for (const line of unavailable) {
|
|
346
|
+
this.logger.warn({ conductorId: this.id }, `claims withheld: ${line}`);
|
|
347
|
+
}
|
|
338
348
|
let claimed = 0;
|
|
339
|
-
let withheld =
|
|
340
|
-
while (machineLoad < maxParallel) {
|
|
349
|
+
let withheld = unavailable.length > 0;
|
|
350
|
+
while (!withheld && machineLoad < maxParallel) {
|
|
341
351
|
const run = await this.remote.claimNext({
|
|
342
352
|
leaseSeconds: this.config.lease_seconds,
|
|
343
353
|
conductorId: this.id,
|
|
@@ -441,7 +451,7 @@ export class Conductor {
|
|
|
441
451
|
// server change is needed and no empty log is written.
|
|
442
452
|
const uploadedLog = (this.config.capture_logs ?? true) ? captured.log : '';
|
|
443
453
|
await this.remote.finalizeRun(r.runUuid, uploadedLog, captured.metrics);
|
|
444
|
-
this.logger.info({ run: r.runUuid, footprint: captured.metrics }, 'run finalised');
|
|
454
|
+
this.logger.info({ run: r.runUuid, state: r.state, footprint: captured.metrics }, 'run finalised');
|
|
445
455
|
}
|
|
446
456
|
catch (err) {
|
|
447
457
|
this.logger.warn({ run: r.runUuid, err: String(err) }, 'run finalise failed');
|
|
@@ -1041,6 +1051,10 @@ export class Conductor {
|
|
|
1041
1051
|
defaultAgent: this.config.default_agent,
|
|
1042
1052
|
warn: (message) => this.logger.warn({ ticket: t.identifier }, message),
|
|
1043
1053
|
});
|
|
1054
|
+
// The tick's gate may be stale by now; spawn nothing that cannot start.
|
|
1055
|
+
const [unavailable] = await sandboxFailures([chosen]);
|
|
1056
|
+
if (unavailable)
|
|
1057
|
+
throw new Error(unavailable);
|
|
1044
1058
|
const baseRef = t.baseBranch ? `origin/${t.baseBranch}` : undefined;
|
|
1045
1059
|
// Resolve the run env once (per-ticket env_vars + core BB_* vars) and
|
|
1046
1060
|
// inject it into BOTH the executor-owned lifecycle hooks AND the agent run
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SandboxHost, SandboxPreflight } from './sandbox-preflight.js';
|
|
1
2
|
/**
|
|
2
3
|
* Per-agent effort footprint parsed from that agent's run transcript
|
|
3
4
|
* (BB-132). The metrics are transcript-derived and therefore
|
|
@@ -136,4 +137,11 @@ export interface BbAgent {
|
|
|
136
137
|
* {@link emptyAgentFootprint}.
|
|
137
138
|
*/
|
|
138
139
|
parseFootprint(log: string): AgentFootprint;
|
|
140
|
+
/**
|
|
141
|
+
* Whether this agent's OS sandbox can start on this machine. The conductor
|
|
142
|
+
* withholds claims and refuses to spawn while it reports a cause. Returns
|
|
143
|
+
* ok without probing when the agent does not run sandboxed. Optional: an
|
|
144
|
+
* agent without it is never checked.
|
|
145
|
+
*/
|
|
146
|
+
sandboxPreflight?(host?: SandboxHost): Promise<SandboxPreflight>;
|
|
139
147
|
}
|
|
@@ -122,6 +122,8 @@ export interface ConductorStatus {
|
|
|
122
122
|
}
|
|
123
123
|
export interface FinalizableRun {
|
|
124
124
|
runUuid: string;
|
|
125
|
+
/** The run's terminal state; a `cancelled` run is stopped like a `done` one. */
|
|
126
|
+
state: 'done' | 'cancelled';
|
|
125
127
|
/** Numeric run id (the `#<runId>` tab-label token); scopes the run's tab-close at finalise (BB-183). */
|
|
126
128
|
runId: number;
|
|
127
129
|
/**
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Whether an agent's sandbox can start on this machine; see {@link BbAgent.sandboxPreflight}. */
|
|
2
|
+
export type SandboxPreflight = {
|
|
3
|
+
ok: true;
|
|
4
|
+
} | {
|
|
5
|
+
ok: false;
|
|
6
|
+
cause: string;
|
|
7
|
+
fix: string;
|
|
8
|
+
};
|
|
9
|
+
/** The machine a preflight inspects; injected by tests. */
|
|
10
|
+
export interface SandboxHost {
|
|
11
|
+
platform: NodeJS.Platform;
|
|
12
|
+
/** Whether `name` is an executable on `PATH`. */
|
|
13
|
+
onPath(name: string): boolean;
|
|
14
|
+
/** Runs a probe command; never throws. */
|
|
15
|
+
run(cmd: string, args: string[]): Promise<{
|
|
16
|
+
ok: boolean;
|
|
17
|
+
stderr: string;
|
|
18
|
+
}>;
|
|
19
|
+
}
|
|
20
|
+
export declare function systemSandboxHost(env?: NodeJS.ProcessEnv): SandboxHost;
|
|
21
|
+
/** The last non-empty line of a probe's stderr: the error, after any warnings. */
|
|
22
|
+
export declare function lastLine(text: string): string;
|
|
23
|
+
/** The platform's install command for `packages`, from the package manager on `PATH`. */
|
|
24
|
+
export declare function installCommand(host: SandboxHost, packages: string[]): string;
|
|
25
|
+
/**
|
|
26
|
+
* One `sandbox unavailable: …` line per agent whose sandbox cannot start;
|
|
27
|
+
* empty when every agent can start (or declares no preflight).
|
|
28
|
+
*/
|
|
29
|
+
export declare function sandboxFailures(agents: readonly {
|
|
30
|
+
id: string;
|
|
31
|
+
alias?: string;
|
|
32
|
+
agent: {
|
|
33
|
+
sandboxPreflight?(): Promise<SandboxPreflight>;
|
|
34
|
+
};
|
|
35
|
+
}[]): Promise<string[]>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { accessSync, constants } from 'node:fs';
|
|
3
|
+
import { delimiter, join } from 'node:path';
|
|
4
|
+
export function systemSandboxHost(env = process.env) {
|
|
5
|
+
return {
|
|
6
|
+
platform: process.platform,
|
|
7
|
+
onPath: (name) => (env.PATH ?? '').split(delimiter).some((dir) => {
|
|
8
|
+
try {
|
|
9
|
+
accessSync(join(dir, name), constants.X_OK);
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}),
|
|
16
|
+
run: (cmd, args) => new Promise((resolve) => {
|
|
17
|
+
execFile(cmd, args, { env, timeout: 15_000 }, (err, _out, stderr) => {
|
|
18
|
+
resolve({ ok: !err, stderr: String(stderr || err?.message || '') });
|
|
19
|
+
});
|
|
20
|
+
}),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/** The last non-empty line of a probe's stderr: the error, after any warnings. */
|
|
24
|
+
export function lastLine(text) {
|
|
25
|
+
return (text
|
|
26
|
+
.split('\n')
|
|
27
|
+
.map((line) => line.trim())
|
|
28
|
+
.filter(Boolean)
|
|
29
|
+
.pop() ?? 'no error output');
|
|
30
|
+
}
|
|
31
|
+
/** The platform's install command for `packages`, from the package manager on `PATH`. */
|
|
32
|
+
export function installCommand(host, packages) {
|
|
33
|
+
const managers = [
|
|
34
|
+
['apt-get', 'sudo apt-get install -y'],
|
|
35
|
+
['dnf', 'sudo dnf install -y'],
|
|
36
|
+
['pacman', 'sudo pacman -S --needed'],
|
|
37
|
+
];
|
|
38
|
+
const found = managers.find(([bin]) => host.onPath(bin));
|
|
39
|
+
return found
|
|
40
|
+
? `${found[1]} ${packages.join(' ')}`
|
|
41
|
+
: `your package manager (packages: ${packages.join(', ')})`;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* One `sandbox unavailable: …` line per agent whose sandbox cannot start;
|
|
45
|
+
* empty when every agent can start (or declares no preflight).
|
|
46
|
+
*/
|
|
47
|
+
export async function sandboxFailures(agents) {
|
|
48
|
+
const lines = [];
|
|
49
|
+
for (const { id, alias, agent } of agents) {
|
|
50
|
+
const result = (await agent.sandboxPreflight?.()) ?? { ok: true };
|
|
51
|
+
if (!result.ok) {
|
|
52
|
+
lines.push(`sandbox unavailable: ${alias ?? id}: ${result.cause} — ${result.fix} (see https://bakers.build/docs/advanced/run-security#sandbox-prerequisites)`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return [...new Set(lines)]; // unaliased entries of one harness report the same line
|
|
56
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bakers-build/conductor",
|
|
3
|
-
"version": "0.4.1-next.
|
|
3
|
+
"version": "0.4.1-next.536",
|
|
4
4
|
"description": "bakers.build conductor engine + CLI: registers, claims tickets via JSON:API, dispatches agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@dropsh/plugin-jsonapi-schema": "^0.8.2",
|
|
34
34
|
"@dropsh/plugin-oauth2": "^0.8.2",
|
|
35
|
-
"@bakers-build/core": "^0.4.1-next.
|
|
36
|
-
"@bakers-build/message": "^0.4.1-next.
|
|
35
|
+
"@bakers-build/core": "^0.4.1-next.536",
|
|
36
|
+
"@bakers-build/message": "^0.4.1-next.536",
|
|
37
37
|
"commander": "^12.1.0",
|
|
38
38
|
"dropsh": "^0.8.2"
|
|
39
39
|
}
|