oorja-harness-host 0.0.0-stage → 0.2.0

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.
Files changed (44) hide show
  1. package/README.md +22 -2
  2. package/dist/database/index.d.ts +6 -0
  3. package/dist/database/index.js +29 -0
  4. package/dist/harnesses/claudeCode/index.d.ts +8 -0
  5. package/dist/harnesses/claudeCode/index.js +16 -0
  6. package/dist/harnesses/codex/index.d.ts +8 -0
  7. package/dist/harnesses/codex/index.js +16 -0
  8. package/dist/harnesses/doctor.d.ts +2 -0
  9. package/dist/harnesses/doctor.js +52 -0
  10. package/dist/harnesses/index.d.ts +8 -0
  11. package/dist/harnesses/index.js +42 -0
  12. package/dist/harnesses/openCode/index.d.ts +8 -0
  13. package/dist/harnesses/openCode/index.js +17 -0
  14. package/dist/harnesses/prepare.d.ts +2 -0
  15. package/dist/harnesses/prepare.js +11 -0
  16. package/dist/harnesses/runtime.d.ts +8 -0
  17. package/dist/harnesses/runtime.js +47 -0
  18. package/dist/harnesses/types.d.ts +21 -0
  19. package/dist/harnesses/types.js +1 -0
  20. package/dist/host/Future.d.ts +7 -0
  21. package/dist/host/Future.js +15 -0
  22. package/dist/host/HarnessHost.d.ts +39 -0
  23. package/dist/host/HarnessHost.js +369 -0
  24. package/dist/host/activity.d.ts +25 -0
  25. package/dist/host/activity.js +71 -0
  26. package/dist/host/models.d.ts +2 -0
  27. package/dist/host/models.js +16 -0
  28. package/dist/index.d.ts +8 -0
  29. package/dist/index.js +6 -0
  30. package/dist/messages.d.ts +150 -0
  31. package/dist/messages.js +37 -0
  32. package/dist/repo/HarnessRepo.d.ts +21 -0
  33. package/dist/repo/HarnessRepo.js +76 -0
  34. package/dist/repo/index.d.ts +2 -0
  35. package/dist/repo/index.js +2 -0
  36. package/dist/repo/migrations/001-initial.d.ts +2 -0
  37. package/dist/repo/migrations/001-initial.js +26 -0
  38. package/dist/repo/migrations/index.d.ts +2 -0
  39. package/dist/repo/migrations/index.js +2 -0
  40. package/dist/runtime.d.ts +13 -0
  41. package/dist/runtime.js +1 -0
  42. package/dist/sandbox/local.d.ts +15 -0
  43. package/dist/sandbox/local.js +126 -0
  44. package/package.json +36 -4
package/README.md CHANGED
@@ -1,3 +1,23 @@
1
- # Temporary Holding Version
1
+ # Harness Host
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Runtime support for hosting AI coding harness sessions on a computer connected
4
+ to a Supakit room.
5
+
6
+ The package owns live harness sessions, adapters, preparation, resume state,
7
+ and the shared message contract. Applications own channel connections, routing,
8
+ machine identity, and command-line presentation.
9
+
10
+ Turn output remains buffered in memory until the client confirms that it has
11
+ saved it. On graceful shutdown, the host saves the opaque AI SDK resume state
12
+ provided by each harness session so the next process can resume it.
13
+
14
+ Harness state is stored in one SQLite database. The host keeps exclusive access
15
+ to that database while it is running; `stop()` saves the sessions and closes
16
+ the database.
17
+
18
+ ```ts
19
+ const host = await HarnessHost.start({
20
+ // ...
21
+ stateFile: '/path/to/harness-host.sqlite',
22
+ })
23
+ ```
@@ -0,0 +1,6 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ export interface Migration {
3
+ version: number;
4
+ apply: (database: DatabaseSync) => void;
5
+ }
6
+ export declare const openDatabase: (file: string, migrations: Migration[]) => DatabaseSync;
@@ -0,0 +1,29 @@
1
+ import { mkdirSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { DatabaseSync } from 'node:sqlite';
4
+ export const openDatabase = (file, migrations) => {
5
+ mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
6
+ const database = new DatabaseSync(file, { timeout: 0 });
7
+ try {
8
+ database.exec('PRAGMA locking_mode = EXCLUSIVE');
9
+ database.exec('PRAGMA journal_mode = DELETE');
10
+ database.exec('BEGIN EXCLUSIVE');
11
+ const { user_version: currentVersion } = database
12
+ .prepare('PRAGMA user_version')
13
+ .get();
14
+ for (const migration of migrations) {
15
+ if (migration.version <= currentVersion)
16
+ continue;
17
+ migration.apply(database);
18
+ database.exec(`PRAGMA user_version = ${migration.version}`);
19
+ }
20
+ database.exec('COMMIT');
21
+ return database;
22
+ }
23
+ catch (error) {
24
+ if (database.isTransaction)
25
+ database.exec('ROLLBACK');
26
+ database.close();
27
+ throw error;
28
+ }
29
+ };
@@ -0,0 +1,8 @@
1
+ import { createClaudeCode } from '@ai-sdk/harness-claude-code';
2
+ export type ClaudeCodeConfig = NonNullable<Parameters<typeof createClaudeCode>[0]>;
3
+ export declare const createClaudeCodeRuntime: ({ cwd, config }: {
4
+ cwd: string;
5
+ config?: ClaudeCodeConfig;
6
+ }) => import("../../index.js").HarnessRuntime;
7
+ export declare const prepareClaudeCode: (cwd: string) => Promise<string>;
8
+ export declare const runClaudeCodeDoctor: (cwd: string) => Promise<import("../types.js").HarnessDoctorCheck[]>;
@@ -0,0 +1,16 @@
1
+ import { createClaudeCode } from '@ai-sdk/harness-claude-code';
2
+ import { HarnessId } from '../../messages.js';
3
+ import { runHarnessDoctor } from '../doctor.js';
4
+ import { prepareHarness } from '../prepare.js';
5
+ import { createHarnessRuntime } from '../runtime.js';
6
+ export const createClaudeCodeRuntime = ({ cwd, config }) => createHarnessRuntime({
7
+ harnessId: HarnessId.CLAUDE_CODE,
8
+ cwd,
9
+ createHarness: (port) => createClaudeCode({
10
+ ...config,
11
+ port,
12
+ portEndpoint: { url: `ws://127.0.0.1:${port}` },
13
+ }),
14
+ });
15
+ export const prepareClaudeCode = (cwd) => prepareHarness(cwd, createClaudeCode());
16
+ export const runClaudeCodeDoctor = (cwd) => runHarnessDoctor(cwd, 'Claude Code');
@@ -0,0 +1,8 @@
1
+ import { createCodex } from '@ai-sdk/harness-codex';
2
+ export type CodexConfig = NonNullable<Parameters<typeof createCodex>[0]>;
3
+ export declare const createCodexRuntime: ({ cwd, config }: {
4
+ cwd: string;
5
+ config?: CodexConfig;
6
+ }) => import("../../index.js").HarnessRuntime;
7
+ export declare const prepareCodex: (cwd: string) => Promise<string>;
8
+ export declare const runCodexDoctor: (cwd: string) => Promise<import("../types.js").HarnessDoctorCheck[]>;
@@ -0,0 +1,16 @@
1
+ import { createCodex } from '@ai-sdk/harness-codex';
2
+ import { HarnessId } from '../../messages.js';
3
+ import { runHarnessDoctor } from '../doctor.js';
4
+ import { prepareHarness } from '../prepare.js';
5
+ import { createHarnessRuntime } from '../runtime.js';
6
+ export const createCodexRuntime = ({ cwd, config }) => createHarnessRuntime({
7
+ harnessId: HarnessId.CODEX,
8
+ cwd,
9
+ createHarness: (port) => createCodex({
10
+ ...config,
11
+ port,
12
+ portEndpoint: { url: `ws://127.0.0.1:${port}` },
13
+ }),
14
+ });
15
+ export const prepareCodex = (cwd) => prepareHarness(cwd, createCodex());
16
+ export const runCodexDoctor = (cwd) => runHarnessDoctor(cwd, 'Codex');
@@ -0,0 +1,2 @@
1
+ import type { HarnessDoctorCheck } from './types.js';
2
+ export declare const runHarnessDoctor: (cwd: string, harnessName: string) => Promise<HarnessDoctorCheck[]>;
@@ -0,0 +1,52 @@
1
+ import { constants } from 'node:fs';
2
+ import { access } from 'node:fs/promises';
3
+ import { createServer } from 'node:net';
4
+ import { LocalSandboxSession } from '../sandbox/local.js';
5
+ export const runHarnessDoctor = async (cwd, harnessName) => {
6
+ const sandbox = new LocalSandboxSession(cwd);
7
+ const home = process.env.HOME;
8
+ const [pnpm, homeIsWritable, loopbackIsAvailable] = await Promise.all([
9
+ Promise.resolve(sandbox.run({ command: 'pnpm --version' })).catch(() => undefined),
10
+ home ? canAccess(home, constants.R_OK | constants.W_OK) : false,
11
+ checkLoopbackPort().then(() => true, () => false),
12
+ ]);
13
+ const nodeMajor = Number(process.versions.node.split('.')[0]);
14
+ return [
15
+ {
16
+ name: 'node',
17
+ ok: nodeMajor >= 22,
18
+ detail: `Node ${process.versions.node}; requires Node 22 or newer`,
19
+ },
20
+ {
21
+ name: 'pnpm',
22
+ ok: pnpm?.exitCode === 0,
23
+ detail: pnpm?.exitCode === 0 ? `pnpm ${pnpm.stdout.trim()}` : `pnpm is required for ${harnessName} bootstrap`,
24
+ },
25
+ {
26
+ name: 'home',
27
+ ok: homeIsWritable,
28
+ detail: home ? `${home} must be readable and writable` : 'HOME is not set',
29
+ },
30
+ {
31
+ name: 'loopback-port',
32
+ ok: loopbackIsAvailable,
33
+ detail: loopbackIsAvailable
34
+ ? 'Can bind an ephemeral port on 127.0.0.1'
35
+ : 'Could not bind an ephemeral port on 127.0.0.1',
36
+ },
37
+ ];
38
+ };
39
+ const checkLoopbackPort = () => new Promise((resolve, reject) => {
40
+ const server = createServer();
41
+ server.once('error', reject);
42
+ server.listen(0, '127.0.0.1', () => server.close((error) => (error ? reject(error) : resolve())));
43
+ });
44
+ const canAccess = async (target, mode) => {
45
+ try {
46
+ await access(target, mode);
47
+ return true;
48
+ }
49
+ catch {
50
+ return false;
51
+ }
52
+ };
@@ -0,0 +1,8 @@
1
+ import { HarnessId } from '../messages.js';
2
+ import type { HarnessRuntime } from '../runtime.js';
3
+ import type { HarnessDoctorResult, HarnessPrepareResult } from './types.js';
4
+ export declare const ALL_HARNESSES: "all";
5
+ export type HarnessSelection = HarnessId | typeof ALL_HARNESSES;
6
+ export declare const doctorHarnessHost: (cwd: string) => Promise<HarnessDoctorResult[]>;
7
+ export declare const prepareHarnessHost: (cwd: string, selections?: readonly HarnessSelection[]) => Promise<HarnessPrepareResult[]>;
8
+ export declare const createHarnessRuntimes: (cwd: string) => HarnessRuntime[];
@@ -0,0 +1,42 @@
1
+ import { createClaudeCodeRuntime, prepareClaudeCode, runClaudeCodeDoctor, } from './claudeCode/index.js';
2
+ import { createCodexRuntime, prepareCodex, runCodexDoctor } from './codex/index.js';
3
+ import { createOpenCodeRuntime, prepareOpenCode, runOpenCodeDoctor } from './openCode/index.js';
4
+ import { HarnessId } from '../messages.js';
5
+ export const ALL_HARNESSES = 'all';
6
+ export const doctorHarnessHost = async (cwd) => Promise.all(harnesses.map(async ({ harnessId, doctor }) => ({
7
+ harnessId,
8
+ checks: await doctor(cwd),
9
+ })));
10
+ export const prepareHarnessHost = async (cwd, selections = [ALL_HARNESSES]) => {
11
+ const results = [];
12
+ for (const harness of selectHarnesses(selections)) {
13
+ results.push({ harnessId: harness.harnessId, detail: await harness.prepare(cwd) });
14
+ }
15
+ return results;
16
+ };
17
+ export const createHarnessRuntimes = (cwd) => harnesses.map(({ createRuntime }) => createRuntime(cwd));
18
+ const codex = {
19
+ harnessId: HarnessId.CODEX,
20
+ createRuntime: (cwd) => createCodexRuntime({ cwd }),
21
+ doctor: runCodexDoctor,
22
+ prepare: prepareCodex,
23
+ };
24
+ const claudeCode = {
25
+ harnessId: HarnessId.CLAUDE_CODE,
26
+ createRuntime: (cwd) => createClaudeCodeRuntime({ cwd }),
27
+ doctor: runClaudeCodeDoctor,
28
+ prepare: prepareClaudeCode,
29
+ };
30
+ const openCode = {
31
+ harnessId: HarnessId.OPENCODE,
32
+ createRuntime: (cwd) => createOpenCodeRuntime({ cwd }),
33
+ doctor: runOpenCodeDoctor,
34
+ prepare: prepareOpenCode,
35
+ };
36
+ const harnesses = [codex, claudeCode, openCode];
37
+ const selectHarnesses = (selections) => {
38
+ if (selections.includes(ALL_HARNESSES))
39
+ return harnesses;
40
+ const selected = new Set(selections);
41
+ return harnesses.filter(({ harnessId }) => selected.has(harnessId));
42
+ };
@@ -0,0 +1,8 @@
1
+ import { createOpenCode } from '@ai-sdk/harness-opencode';
2
+ export type OpenCodeConfig = NonNullable<Parameters<typeof createOpenCode>[0]>;
3
+ export declare const createOpenCodeRuntime: ({ cwd, config }: {
4
+ cwd: string;
5
+ config?: OpenCodeConfig;
6
+ }) => import("../../index.js").HarnessRuntime;
7
+ export declare const prepareOpenCode: (cwd: string) => Promise<string>;
8
+ export declare const runOpenCodeDoctor: (cwd: string) => Promise<import("../types.js").HarnessDoctorCheck[]>;
@@ -0,0 +1,17 @@
1
+ import { createOpenCode } from '@ai-sdk/harness-opencode';
2
+ import { HarnessId } from '../../messages.js';
3
+ import { runHarnessDoctor } from '../doctor.js';
4
+ import { prepareHarness } from '../prepare.js';
5
+ import { createHarnessRuntime } from '../runtime.js';
6
+ export const createOpenCodeRuntime = ({ cwd, config }) => createHarnessRuntime({
7
+ harnessId: HarnessId.OPENCODE,
8
+ cwd,
9
+ createHarness: (port) => createOpenCode({
10
+ ...config,
11
+ provider: config?.provider ?? 'opencode',
12
+ port,
13
+ portEndpoint: { url: `ws://127.0.0.1:${port}` },
14
+ }),
15
+ });
16
+ export const prepareOpenCode = (cwd) => prepareHarness(cwd, createOpenCode());
17
+ export const runOpenCodeDoctor = (cwd) => runHarnessDoctor(cwd, 'OpenCode');
@@ -0,0 +1,2 @@
1
+ import { type HarnessAgentAdapter } from '@ai-sdk/harness/agent';
2
+ export declare const prepareHarness: (cwd: string, harness: HarnessAgentAdapter) => Promise<string>;
@@ -0,0 +1,11 @@
1
+ import { createHarnessSandboxTemplate } from '@ai-sdk/harness/agent';
2
+ import { LocalSandboxSession } from '../sandbox/local.js';
3
+ export const prepareHarness = async (cwd, harness) => {
4
+ const template = await createHarnessSandboxTemplate({
5
+ harnesses: [harness],
6
+ sandboxConfig: { workDir: '.' },
7
+ });
8
+ if (template)
9
+ await template.prepare({ session: new LocalSandboxSession(cwd) });
10
+ return 'Installed locally and ready to use.';
11
+ };
@@ -0,0 +1,8 @@
1
+ import { type HarnessAgentAdapter } from '@ai-sdk/harness/agent';
2
+ import { HarnessId } from '../messages.js';
3
+ import type { HarnessRuntime } from '../runtime.js';
4
+ export declare const createHarnessRuntime: ({ harnessId, cwd, createHarness, }: {
5
+ harnessId: HarnessId;
6
+ cwd: string;
7
+ createHarness: (port: number) => HarnessAgentAdapter;
8
+ }) => HarnessRuntime;
@@ -0,0 +1,47 @@
1
+ import { createServer } from 'node:net';
2
+ import { HarnessAgent } from '@ai-sdk/harness/agent';
3
+ import { jsonSchema } from 'ai';
4
+ import { HARNESS_MODELS } from '../host/models.js';
5
+ import { LocalSandboxSession } from '../sandbox/local.js';
6
+ export const createHarnessRuntime = ({ harnessId, cwd, createHarness, }) => ({
7
+ harnessId,
8
+ createSession: async (sessionId, resumeFrom) => {
9
+ const port = await allocateLoopbackPort();
10
+ const agent = new HarnessAgent({
11
+ model: HARNESS_MODELS[harnessId].defaultModel,
12
+ harness: createHarness(port),
13
+ permissionMode: 'allow-all',
14
+ sandboxConfig: { workDir: '.' },
15
+ callOptionsSchema: jsonSchema({
16
+ type: 'object',
17
+ properties: { model: { type: 'string' } },
18
+ additionalProperties: false,
19
+ }),
20
+ prepareCall: ({ options, ...call }) => ({ ...call, model: options.model ?? call.model }),
21
+ });
22
+ const session = await agent.createSession({
23
+ sessionId,
24
+ resumeFrom,
25
+ sandboxSession: new LocalSandboxSession(cwd),
26
+ });
27
+ return {
28
+ stream: async (prompt, abortSignal, model) => (await agent.stream({ session, prompt, abortSignal, options: { model } })).fullStream,
29
+ steer: (text) => agent.experimental_steer({ session, text }),
30
+ stop: () => session.stop(),
31
+ destroy: () => session.destroy(),
32
+ };
33
+ },
34
+ });
35
+ const allocateLoopbackPort = () => new Promise((resolve, reject) => {
36
+ const server = createServer();
37
+ server.once('error', reject);
38
+ server.listen(0, '127.0.0.1', () => {
39
+ const address = server.address();
40
+ if (address === null || typeof address === 'string') {
41
+ server.close();
42
+ reject(new Error('Unable to allocate a loopback port'));
43
+ return;
44
+ }
45
+ server.close((error) => (error ? reject(error) : resolve(address.port)));
46
+ });
47
+ });
@@ -0,0 +1,21 @@
1
+ import type { HarnessId } from '../messages.js';
2
+ import type { HarnessRuntime } from '../runtime.js';
3
+ export type HarnessDoctorCheck = {
4
+ name: string;
5
+ ok: boolean;
6
+ detail: string;
7
+ };
8
+ export type HarnessDoctorResult = {
9
+ harnessId: HarnessId;
10
+ checks: HarnessDoctorCheck[];
11
+ };
12
+ export type HarnessPrepareResult = {
13
+ harnessId: HarnessId;
14
+ detail: string;
15
+ };
16
+ export type HarnessImplementation = {
17
+ harnessId: HarnessId;
18
+ createRuntime: (cwd: string) => HarnessRuntime;
19
+ doctor: (cwd: string) => Promise<HarnessDoctorCheck[]>;
20
+ prepare: (cwd: string) => Promise<string>;
21
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,7 @@
1
+ export declare class Future<T> {
2
+ readonly promise: Promise<T>;
3
+ resolve?: (arg: T) => void;
4
+ reject?: (error: unknown) => void;
5
+ onFinally?: () => void;
6
+ constructor(futureBase?: (resolve: (arg: T) => void, reject: (error: unknown) => void) => void, onFinally?: () => void);
7
+ }
@@ -0,0 +1,15 @@
1
+ export class Future {
2
+ promise;
3
+ resolve;
4
+ reject;
5
+ onFinally;
6
+ constructor(futureBase, onFinally) {
7
+ this.onFinally = onFinally;
8
+ this.promise = new Promise(async (resolve, reject) => {
9
+ this.resolve = resolve;
10
+ this.reject = reject;
11
+ if (futureBase)
12
+ await futureBase(resolve, reject);
13
+ }).finally(() => this.onFinally?.());
14
+ }
15
+ }
@@ -0,0 +1,39 @@
1
+ import { type HarnessCommand, type HarnessEvent, type HostState } from '../messages.js';
2
+ import type { HarnessRuntime } from '../runtime.js';
3
+ export type HarnessHostOptions = {
4
+ hostId: string;
5
+ hostname: string;
6
+ stateFile: string;
7
+ runtimes: HarnessRuntime[];
8
+ emit: (event: HarnessEvent) => void;
9
+ onError: (error: unknown) => void;
10
+ };
11
+ export declare class HarnessHost {
12
+ private readonly options;
13
+ private readonly database;
14
+ private readonly repo;
15
+ private readonly runtimes;
16
+ private readonly sessions;
17
+ private readonly openingSessions;
18
+ private stopping;
19
+ private constructor();
20
+ static start: (options: HarnessHostOptions) => Promise<HarnessHost>;
21
+ getState: () => HostState;
22
+ handleCommand: (command: HarnessCommand) => Promise<void>;
23
+ stop: () => Promise<void>;
24
+ private submitMessage;
25
+ private steer;
26
+ private runTurn;
27
+ private abortTurn;
28
+ private sendTurnSnapshot;
29
+ private handleTurnSaved;
30
+ private destroySession;
31
+ private setSessionModel;
32
+ private setModel;
33
+ private getSession;
34
+ private openSession;
35
+ private publishTurnPart;
36
+ private emitActivity;
37
+ private saveSession;
38
+ private emitState;
39
+ }