aide-sovereign-workbench / daemon /training-manager.mjs
Ferrell Synthetic Intelligence
Add safe Training Room job execution
cf06504
Raw
History Blame Contribute Delete
1.95 kB
import { promises as fs } from 'node:fs';
import { spawn } from 'node:child_process';
export class TrainingManager {
constructor({ manifestPath, workspace, spawnProcess = spawn } = {}) {
this.manifestPath = manifestPath;
this.workspace = workspace;
this.spawnProcess = spawnProcess;
this.jobs = new Map();
this.active = null;
this.logs = [];
}
async load() {
const manifest = JSON.parse(await fs.readFile(this.manifestPath, 'utf8'));
this.jobs = new Map(manifest.jobs.map(job => [job.id, job]));
return manifest;
}
status() {
return { active: this.active ? { id: this.active.id, status: this.active.status } : null, logs: this.logs.slice(-100), jobs: [...this.jobs.values()] };
}
start(id, approved) {
if (approved !== true) throw new Error('explicit training approval required');
if (this.active) throw new Error('another Training Room job is already running');
const job = this.jobs.get(id);
if (!job) throw new Error('training job is not allowlisted');
const child = this.spawnProcess(job.command, job.args, { cwd: this.workspace, stdio: ['ignore', 'pipe', 'pipe'] });
this.active = { id, status: 'running', child };
const record = line => { this.logs.push({ id, line: String(line).trimEnd(), at: new Date().toISOString() }); };
child.stdout?.on('data', data => String(data).split('\n').filter(Boolean).forEach(record));
child.stderr?.on('data', data => String(data).split('\n').filter(Boolean).forEach(line => record(`stderr: ${line}`)));
child.once('exit', code => { record(`job exited with code ${code}`); this.active = null; });
return { id, status: 'running' };
}
stop() {
if (!this.active) return { status: 'idle' };
this.active.child.kill('SIGTERM');
this.logs.push({ id: this.active.id, line: 'job cancelled by user', at: new Date().toISOString() });
this.active = null;
return { status: 'stopped' };
}
}