feat: harden orchestrator execution runtime
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env node
|
||||
import os from 'node:os';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import {
|
||||
createAiderExecutorAdapter,
|
||||
createGoosedExecutorAdapter,
|
||||
createOpenHandsExecutorAdapter,
|
||||
} from './executor-adapters.mjs';
|
||||
import {
|
||||
createExecutorWorkerClient,
|
||||
createExecutorWorkerRuntime,
|
||||
createWorkerAdapterRegistry,
|
||||
} from './executor-worker-runtime.mjs';
|
||||
|
||||
function envFlag(value, fallback = false) {
|
||||
const normalized = String(value ?? '').trim().toLowerCase();
|
||||
if (!normalized) return fallback;
|
||||
return ['1', 'true', 'yes', 'on'].includes(normalized);
|
||||
}
|
||||
|
||||
function positiveInteger(value, fallback) {
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) && number > 0 ? Math.floor(number) : fallback;
|
||||
}
|
||||
|
||||
function jsonObject(value, fallback = {}) {
|
||||
try {
|
||||
const parsed = JSON.parse(String(value ?? ''));
|
||||
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : fallback;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
export function createConfiguredWorker({ env = process.env, fetchImpl = globalThis.fetch } = {}) {
|
||||
const registry = createWorkerAdapterRegistry();
|
||||
const workspaceAliases = jsonObject(env.MEMIND_EXECUTOR_WORKSPACE_ALIASES_JSON);
|
||||
const allowedRoot = String(env.MEMIND_EXECUTOR_WORKSPACE_ROOT ?? '').trim();
|
||||
const goosedUrl = String(env.MEMIND_EXECUTOR_GOOSED_URL ?? '').trim();
|
||||
if (goosedUrl) {
|
||||
registry.register(createGoosedExecutorAdapter({
|
||||
baseUrl: goosedUrl,
|
||||
secret: env.MEMIND_EXECUTOR_GOOSED_TOKEN,
|
||||
fetchImpl,
|
||||
allowRemote: envFlag(env.MEMIND_EXECUTOR_ALLOW_REMOTE_GOOSED, false),
|
||||
workspaceResolver: async (reference) => workspaceAliases[reference?.id] ?? null,
|
||||
}));
|
||||
}
|
||||
if (env.MEMIND_EXECUTOR_AIDER_COMMAND && allowedRoot) {
|
||||
registry.register(createAiderExecutorAdapter({
|
||||
command: env.MEMIND_EXECUTOR_AIDER_COMMAND,
|
||||
allowedRoot,
|
||||
workspaceAliases,
|
||||
}));
|
||||
}
|
||||
if (env.MEMIND_EXECUTOR_OPENHANDS_COMMAND && allowedRoot) {
|
||||
registry.register(createOpenHandsExecutorAdapter({
|
||||
command: env.MEMIND_EXECUTOR_OPENHANDS_COMMAND,
|
||||
allowedRoot,
|
||||
workspaceAliases,
|
||||
}));
|
||||
}
|
||||
if (!registry.ids().length) {
|
||||
throw new Error('Executor worker has no configured adapters');
|
||||
}
|
||||
const client = createExecutorWorkerClient({
|
||||
baseUrl: env.MEMIND_ORCHESTRATOR_URL ?? 'http://127.0.0.1:8093',
|
||||
serviceToken: env.MEMIND_ORCHESTRATOR_SERVICE_TOKEN,
|
||||
workerToken: env.MEMIND_ORCHESTRATOR_WORKER_TOKEN,
|
||||
fetchImpl,
|
||||
});
|
||||
return createExecutorWorkerRuntime({
|
||||
client,
|
||||
registry,
|
||||
workerId: env.MEMIND_EXECUTOR_WORKER_ID ?? `${os.hostname()}-${process.pid}`,
|
||||
heartbeatMs: positiveInteger(env.MEMIND_EXECUTOR_HEARTBEAT_MS, 10_000),
|
||||
leaseDurationMs: positiveInteger(env.MEMIND_EXECUTOR_LEASE_MS, 30_000),
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const worker = createConfiguredWorker();
|
||||
const once = process.argv.includes('--once');
|
||||
const recover = process.argv.includes('--recover-expired');
|
||||
if (recover) {
|
||||
console.log(JSON.stringify(await worker.recoverExpired(), null, 2));
|
||||
return;
|
||||
}
|
||||
if (once) {
|
||||
console.log(JSON.stringify(await worker.runOnce(), null, 2));
|
||||
return;
|
||||
}
|
||||
let stopping = false;
|
||||
const stop = async () => {
|
||||
if (stopping) return;
|
||||
stopping = true;
|
||||
await worker.stop();
|
||||
};
|
||||
process.once('SIGINT', () => void stop());
|
||||
process.once('SIGTERM', () => void stop());
|
||||
const pollMs = positiveInteger(process.env.MEMIND_EXECUTOR_POLL_MS, 1000);
|
||||
console.log(JSON.stringify({ ok: true, ...worker.status() }));
|
||||
while (!stopping) {
|
||||
await worker.runOnce();
|
||||
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
||||
}
|
||||
}
|
||||
|
||||
const isEntrypoint = process.argv[1]
|
||||
&& import.meta.url === pathToFileURL(process.argv[1]).href;
|
||||
if (isEntrypoint) {
|
||||
await main();
|
||||
}
|
||||
|
||||
export const executorWorkerEntrypointInternals = {
|
||||
envFlag,
|
||||
jsonObject,
|
||||
positiveInteger,
|
||||
};
|
||||
Reference in New Issue
Block a user