feat: harden orchestrator execution runtime
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import fs from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
createAiderExecutorAdapter,
|
||||
createGoosedExecutorAdapter,
|
||||
createOpenHandsExecutorAdapter,
|
||||
} from './executor-adapters.mjs';
|
||||
|
||||
function request(executor = 'goosed') {
|
||||
return {
|
||||
executor,
|
||||
task: {
|
||||
instruction: 'Fix the validation',
|
||||
workspaceRef: { kind: 'workspace-alias', id: 'canary' },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function sseResponse(events) {
|
||||
const encoder = new TextEncoder();
|
||||
const body = new ReadableStream({
|
||||
start(controller) {
|
||||
for (const event of events) {
|
||||
controller.enqueue(encoder.encode(
|
||||
`event: ${event.type}\ndata: ${JSON.stringify(event.data)}\n\n`,
|
||||
));
|
||||
}
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
return new Response(body, {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'text/event-stream' },
|
||||
});
|
||||
}
|
||||
|
||||
test('Goosed adapter uses the current session API, streams bounded metadata and returns refs', async () => {
|
||||
const calls = [];
|
||||
const emitted = [];
|
||||
const adapter = createGoosedExecutorAdapter({
|
||||
baseUrl: 'http://127.0.0.1:18006',
|
||||
secret: 'secret',
|
||||
workspaceResolver: async () => '/tmp/canary',
|
||||
fetchImpl: async (url, init) => {
|
||||
calls.push({ url, init });
|
||||
if (url.endsWith('/agent/start')) {
|
||||
return Response.json({ id: 'session-1' });
|
||||
}
|
||||
if (url.endsWith('/events')) {
|
||||
return sseResponse([
|
||||
{ type: 'message', data: { type: 'message', secret: 'not-projected' } },
|
||||
{ type: 'finish', data: { type: 'finish' } },
|
||||
]);
|
||||
}
|
||||
if (url.endsWith('/reply')) return Response.json({ ok: true });
|
||||
throw new Error(`Unexpected URL: ${url}`);
|
||||
},
|
||||
});
|
||||
const result = await adapter.submit(request(), {
|
||||
jobId: 'job-1',
|
||||
emit: async (type, data) => emitted.push({ type, data }),
|
||||
});
|
||||
assert.deepEqual(result.artifactRefs, [{ kind: 'goosed-session', id: 'session-1' }]);
|
||||
assert.equal(result.metrics.eventCount, 2);
|
||||
assert.equal(calls[0].url, 'http://127.0.0.1:18006/agent/start');
|
||||
assert.equal(calls[0].init.headers.authorization, 'Bearer secret');
|
||||
assert.equal(JSON.parse(calls[0].init.body).working_dir, '/tmp/canary');
|
||||
assert.equal(
|
||||
JSON.parse(calls.find((call) => call.url.endsWith('/reply')).init.body)
|
||||
.user_message.content[0].text,
|
||||
'Fix the validation',
|
||||
);
|
||||
assert.deepEqual(
|
||||
emitted.map((event) => event.type),
|
||||
[
|
||||
'executor_job_session_created',
|
||||
'executor_job_adapter_event',
|
||||
'executor_job_adapter_event',
|
||||
],
|
||||
);
|
||||
assert.equal(JSON.stringify(emitted).includes('not-projected'), false);
|
||||
});
|
||||
|
||||
test('Goosed adapter rejects remote targets unless explicitly allowed', () => {
|
||||
assert.throws(
|
||||
() => createGoosedExecutorAdapter({ baseUrl: 'https://executor.example.com' }),
|
||||
/explicit allowRemote/,
|
||||
);
|
||||
assert.doesNotThrow(
|
||||
() => createGoosedExecutorAdapter({
|
||||
baseUrl: 'https://executor.example.com',
|
||||
allowRemote: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('Aider process adapter resolves a workspace alias and never invokes a shell', async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'memind-executor-'));
|
||||
const workspace = path.join(root, 'canary');
|
||||
await fs.mkdir(workspace);
|
||||
const spawnCalls = [];
|
||||
const emitted = [];
|
||||
const spawnImpl = (command, args, options) => {
|
||||
spawnCalls.push({ command, args, options });
|
||||
const child = new EventEmitter();
|
||||
child.stdout = new EventEmitter();
|
||||
child.stderr = new EventEmitter();
|
||||
child.kill = () => {};
|
||||
queueMicrotask(() => {
|
||||
child.stdout.emit('data', Buffer.from('done'));
|
||||
child.emit('exit', 0, null);
|
||||
});
|
||||
return child;
|
||||
};
|
||||
try {
|
||||
const adapter = createAiderExecutorAdapter({
|
||||
command: '/usr/local/bin/aider',
|
||||
allowedRoot: root,
|
||||
workspaceAliases: { canary: workspace },
|
||||
spawnImpl,
|
||||
});
|
||||
const result = await adapter.submit(request('aider'), {
|
||||
jobId: 'job-1',
|
||||
emit: async (type, data) => emitted.push({ type, data }),
|
||||
});
|
||||
assert.equal(result.outcome, 'completed');
|
||||
assert.deepEqual(result.artifactRefs, [{ kind: 'workspace-alias', id: 'canary' }]);
|
||||
assert.equal(spawnCalls[0].options.cwd, await fs.realpath(workspace));
|
||||
assert.equal(spawnCalls[0].options.shell, false);
|
||||
assert.deepEqual(
|
||||
spawnCalls[0].args,
|
||||
['--yes-always', '--message', 'Fix the validation'],
|
||||
);
|
||||
assert.equal(emitted[0].data.stream, 'stdout');
|
||||
assert.equal('HOME' in spawnCalls[0].options.env, false);
|
||||
} finally {
|
||||
await fs.rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('OpenHands adapter refuses workspace aliases outside its isolated root', async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'memind-executor-root-'));
|
||||
const outside = await fs.mkdtemp(path.join(os.tmpdir(), 'memind-executor-outside-'));
|
||||
try {
|
||||
const adapter = createOpenHandsExecutorAdapter({
|
||||
command: '/usr/local/bin/openhands',
|
||||
allowedRoot: root,
|
||||
workspaceAliases: { canary: outside },
|
||||
});
|
||||
await assert.rejects(
|
||||
adapter.submit(request('openhands'), { jobId: 'job-1' }),
|
||||
(error) => error.code === 'EXECUTOR_WORKSPACE_OUTSIDE_ROOT',
|
||||
);
|
||||
} finally {
|
||||
await fs.rm(root, { recursive: true, force: true });
|
||||
await fs.rm(outside, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user