Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -200,14 +200,18 @@ describe('one spawn path', () => {
// while a step runs, a system call by another name that reads and runs no
// workload. machine.ts asks Metal (through `swift`) or `nvidia-smi` how much
// GPU memory a model runtime on this machine has, once, and runs nothing it
// was handed.
// was handed. localRuntimeStart.ts starts the operator's own Ollama as it
// is installed — its service manager, its app, or its binary — when the
// operator asks from the workspace; the request names nothing to run, and
// a runtime confined to a job's sandbox could not serve the machine.
// Named rather than matched, so a new unconfined spawn is argued for here.
expect(importers).toEqual([
'browser/harnessBrowser.ts',
'credentialFetch.ts',
'folderPicker.ts',
'harnessDiscovery.ts',
'keepAwake.ts',
'localRuntimeStart.ts',
'machine.ts',
'orphans.ts',
'runtimes.ts',
Expand Down
161 changes: 161 additions & 0 deletions apps/aflow-executor-host/src/__tests__/localRuntimeStart.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import { describe, expect, it } from 'vitest';

import { startLocalRuntime, type RuntimeStartDeps } from '../localRuntimeStart.js';

interface Machine {
files?: string[];
agents?: string[];
running?: boolean;
/** Commands that fail, by their first argument. */
failing?: string[];
}

/** A machine with the given files, whose runtime answers once something has started it. */
function machine(setup: Machine & Partial<RuntimeStartDeps> = {}) {
const files = new Set(setup.files ?? []);
let running = setup.running ?? false;
const ran: string[] = [];
const served: Array<{ binary: string; env: Record<string, string> }> = [];
const starts = (command: string, args: readonly string[]) =>
command === 'open' ||
(command === 'launchctl' && args[0] === 'kickstart') ||
command === 'systemctl' ||
command.endsWith('/brew');
const deps: RuntimeStartDeps = {
platform: 'darwin',
home: '/Users/op',
uid: 501,
searchPath: ['/opt/homebrew/bin', '/usr/local/bin'],
exists: async (path) => files.has(path),
executable: async (path) => files.has(path),
list: async (dir) => (dir === '/Users/op/Library/LaunchAgents' ? (setup.agents ?? []) : []),
exec: async (command, args) => {
ran.push([command, ...args].join(' '));
if (setup.failing?.includes(args[0] ?? command)) throw new Error(`${command} failed`);
if (starts(command, args)) running = true;
},
serve: (binary, env) => {
served.push({ binary, env });
running = true;
},
answers: async () => running,
sleep: async () => undefined,
waitMs: 2_000,
...setup,
};
return { deps, ran, served };
}

describe('starting Ollama on this machine', () => {
it('starts a Homebrew install through Homebrew, which reinstalls the agent it removed on stop', async () => {
const { deps, ran, served } = machine({
files: [
'/opt/homebrew/opt/ollama/bin/ollama',
'/opt/homebrew/bin/brew',
'/opt/homebrew/bin/ollama',
'/Applications/Ollama.app',
],
});

const answer = await startLocalRuntime(11434, deps);

expect(answer).toEqual({
kind: 'started',
message: 'Started the ollama Homebrew service.',
});
expect(ran).toEqual(['/opt/homebrew/bin/brew services start ollama']);
expect(served).toHaveLength(0);
});

it('starts an installed launch agent as that service, so its settings hold', async () => {
const { deps, ran, served } = machine({
agents: ['sh.brew.ollama.plist', 'com.other.plist'],
files: ['/Applications/Ollama.app', '/opt/homebrew/bin/ollama'],
});

const answer = await startLocalRuntime(11434, deps);

expect(answer).toEqual({ kind: 'started', message: 'Started the sh.brew.ollama service.' });
expect(ran).toEqual([
'launchctl bootstrap gui/501 /Users/op/Library/LaunchAgents/sh.brew.ollama.plist',
'launchctl kickstart gui/501/sh.brew.ollama',
]);
expect(served).toHaveLength(0);
});

it('starts a launch agent that is already loaded, whose bootstrap fails', async () => {
const { deps } = machine({ agents: ['sh.brew.ollama.plist'], failing: ['bootstrap'] });

expect(await startLocalRuntime(11434, deps)).toMatchObject({ kind: 'started' });
});

it('opens the macOS app where no service is installed', async () => {
const { deps, ran } = machine({ files: ['/Applications/Ollama.app'] });

expect(await startLocalRuntime(11434, deps)).toEqual({
kind: 'started',
message: 'Opened the Ollama app.',
});
expect(ran).toEqual(['open -g -a /Applications/Ollama.app']);
});

it('starts a Linux user unit through systemd', async () => {
const { deps, ran } = machine({
platform: 'linux',
files: ['/Users/op/.config/systemd/user/ollama.service', '/usr/local/bin/ollama'],
});

expect(await startLocalRuntime(11434, deps)).toMatchObject({ kind: 'started' });
expect(ran).toEqual(['systemctl --user start ollama']);
});

it('leaves a system service to the administrator, and says how', async () => {
const { deps, served } = machine({
platform: 'linux',
files: ['/etc/systemd/system/ollama.service', '/usr/local/bin/ollama'],
});

const answer = await startLocalRuntime(11434, deps);

expect(answer).toMatchObject({ kind: 'refused' });
expect(answer.message).toContain('sudo systemctl start ollama');
expect(served).toHaveLength(0);
});

it('runs `ollama serve` without the executor’s own credentials where nothing else is installed', async () => {
process.env['REDIS_URL'] = 'redis://:secret@127.0.0.1:6379';
const { deps, served } = machine({ platform: 'linux', files: ['/usr/local/bin/ollama'] });

const answer = await startLocalRuntime(11500, deps);

expect(answer.kind).toBe('started');
expect(served[0]?.binary).toBe('/usr/local/bin/ollama');
expect(served[0]?.env['REDIS_URL']).toBeUndefined();
expect(served[0]?.env['OLLAMA_HOST']).toBe('127.0.0.1:11500');
delete process.env['REDIS_URL'];
});

it('points at the download where Ollama is not installed', async () => {
const answer = await startLocalRuntime(11434, machine().deps);

expect(answer).toMatchObject({ kind: 'refused' });
expect(answer.message).toContain('https://ollama.com/download');
});

it('starts nothing when the runtime already answers', async () => {
const { deps, ran, served } = machine({ running: true, agents: ['sh.brew.ollama.plist'] });

expect(await startLocalRuntime(11434, deps)).toMatchObject({ kind: 'started' });
expect(ran).toHaveLength(0);
expect(served).toHaveLength(0);
});

it('says where to look when what it started never answers', async () => {
const { deps } = machine({ files: ['/Applications/Ollama.app'], answers: async () => false });

const answer = await startLocalRuntime(11434, deps);

expect(answer).toMatchObject({ kind: 'refused' });
expect(answer.message).toContain('~/.ollama/logs');
});
});
12 changes: 2 additions & 10 deletions apps/aflow-executor-host/src/harnessDiscovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,12 @@ import { homedir, tmpdir } from 'node:os';
import { join } from 'node:path';
import { promisify } from 'node:util';

import { probeEnv } from './probeEnv.js';

import type { HarnessOutputFormat } from './harnessProfiles.js';

const run = promisify(execFile);

/** As in `runtimes.ts`: discovery runs operator-controlled programs. */
function probeEnv(): Record<string, string> {
const env: Record<string, string> = {};
for (const name of ['PATH', 'HOME', 'LANG', 'LC_ALL', 'TZ'] as const) {
const value = process.env[name];
if (value !== undefined) env[name] = value;
}
return env;
}

const PROBE_TIMEOUT_MS = 5_000;

/**
Expand Down
37 changes: 37 additions & 0 deletions apps/aflow-executor-host/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ import {
HOST_MACHINES_KEY,
hostBrowserAnswerChannel,
hostBrowserRequestChannel,
hostRuntimeAnswerChannel,
hostRuntimeRequestChannel,
readHostRuntimeRequest,
type HostRuntimeAnswer,
HOST_WITHDRAWAL_CHANNEL,
hostInventoryKey,
type HostInventory,
Expand Down Expand Up @@ -85,6 +89,7 @@ import { createMachineReader } from './machine.js';
import { publishingFolders } from './pushApproval.js';
import { startUnderSignals } from './startUnderSignals.js';
import { createStartupTimings } from './startupTimings.js';
import { startLocalRuntime } from './localRuntimeStart.js';
import { type BackgroundTaskLogger, createBackgroundTaskRunner } from '@aflow/lib';

const log = createServiceLogger('host-executor');
Expand Down Expand Up @@ -635,11 +640,43 @@ async function main(): Promise<void> {
});
});
};
// The operator asking from the workspace for Ollama to start here. Two asks
// at once share one start, so a double click does not race two launches.
const runtimeChannel = hostRuntimeRequestChannel(hostname);
await hostChannels.subscribe(runtimeChannel).catch((error: unknown) => {
log.warn('Could not subscribe to model runtime requests from the workspace', { error });
});
let starting: Promise<HostRuntimeAnswer> | undefined;
const runtimeAsked = (raw: string): void => {
const request = readHostRuntimeRequest(raw, hostname);
if (request === undefined) return;
starting ??= startLocalRuntime(request.port).finally(() => {
starting = undefined;
});
void starting
.then(async (answer) => {
log.info('Asked from the workspace to start the model runtime', {
port: request.port,
outcome: answer.kind,
message: answer.message,
});
await redis.publish(hostRuntimeAnswerChannel(request.answerId), JSON.stringify(answer));
})
.catch((error: unknown) => {
log.warn('Could not answer a model runtime start asked for from the workspace', {
error: error instanceof Error ? error.message : String(error),
});
});
};
hostChannels.on('message', (channel: string, raw: string) => {
if (channel === requestChannel) {
askedFromWorkspace(raw);
return;
}
if (channel === runtimeChannel) {
runtimeAsked(raw);
return;
}
if (channel !== HOST_WITHDRAWAL_CHANNEL) return;
void (async () => {
let notice: HostWithdrawalNotice;
Expand Down
Loading
Loading