@harness-control/runner 0.1.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 (71) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +21 -0
  3. package/dist/audit/index.d.ts +18 -0
  4. package/dist/audit/index.js +28 -0
  5. package/dist/config/index.d.ts +179 -0
  6. package/dist/config/index.js +124 -0
  7. package/dist/connection/index.d.ts +2 -0
  8. package/dist/connection/index.js +2 -0
  9. package/dist/connection/runner-connection.d.ts +22 -0
  10. package/dist/connection/runner-connection.js +631 -0
  11. package/dist/harnesses/adapters/providers/claude-runtime.d.ts +5 -0
  12. package/dist/harnesses/adapters/providers/claude-runtime.js +186 -0
  13. package/dist/harnesses/adapters/providers/claude.d.ts +24 -0
  14. package/dist/harnesses/adapters/providers/claude.js +189 -0
  15. package/dist/harnesses/adapters/providers/cli-process.d.ts +44 -0
  16. package/dist/harnesses/adapters/providers/cli-process.js +195 -0
  17. package/dist/harnesses/adapters/providers/codex-models.d.ts +4 -0
  18. package/dist/harnesses/adapters/providers/codex-models.js +62 -0
  19. package/dist/harnesses/adapters/providers/codex-rpc.d.ts +21 -0
  20. package/dist/harnesses/adapters/providers/codex-rpc.js +114 -0
  21. package/dist/harnesses/adapters/providers/codex-runtime.d.ts +3 -0
  22. package/dist/harnesses/adapters/providers/codex-runtime.js +267 -0
  23. package/dist/harnesses/adapters/providers/codex.d.ts +22 -0
  24. package/dist/harnesses/adapters/providers/codex.js +161 -0
  25. package/dist/harnesses/adapters/providers/mock.d.ts +13 -0
  26. package/dist/harnesses/adapters/providers/mock.js +64 -0
  27. package/dist/harnesses/adapters/providers/native-process.d.ts +9 -0
  28. package/dist/harnesses/adapters/providers/native-process.js +41 -0
  29. package/dist/harnesses/adapters/providers/native-turn.d.ts +17 -0
  30. package/dist/harnesses/adapters/providers/native-turn.js +139 -0
  31. package/dist/harnesses/adapters/providers/opencode.d.ts +44 -0
  32. package/dist/harnesses/adapters/providers/opencode.js +416 -0
  33. package/dist/harnesses/adapters/providers/shared.d.ts +9 -0
  34. package/dist/harnesses/adapters/providers/shared.js +97 -0
  35. package/dist/harnesses/adapters/registry.d.ts +12 -0
  36. package/dist/harnesses/adapters/registry.js +47 -0
  37. package/dist/harnesses/adapters/types.d.ts +54 -0
  38. package/dist/harnesses/adapters/types.js +9 -0
  39. package/dist/harnesses/adapters.d.ts +8 -0
  40. package/dist/harnesses/adapters.js +8 -0
  41. package/dist/harnesses/index.d.ts +93 -0
  42. package/dist/harnesses/index.js +620 -0
  43. package/dist/host/provider-registry.d.ts +34 -0
  44. package/dist/host/provider-registry.js +162 -0
  45. package/dist/index.d.ts +3 -0
  46. package/dist/index.js +201 -0
  47. package/dist/local-actions/dispatcher.d.ts +28 -0
  48. package/dist/local-actions/dispatcher.js +407 -0
  49. package/dist/local-actions/executors.d.ts +159 -0
  50. package/dist/local-actions/executors.js +1103 -0
  51. package/dist/local-actions/index.d.ts +74 -0
  52. package/dist/local-actions/index.js +275 -0
  53. package/dist/logs/index.d.ts +6 -0
  54. package/dist/logs/index.js +9 -0
  55. package/dist/mcp/McpAttachmentClient.d.ts +111 -0
  56. package/dist/mcp/McpAttachmentClient.js +345 -0
  57. package/dist/mcp/McpProxyServer.d.ts +18 -0
  58. package/dist/mcp/McpProxyServer.js +188 -0
  59. package/dist/mcp/McpStdioProfileClient.d.ts +19 -0
  60. package/dist/mcp/McpStdioProfileClient.js +91 -0
  61. package/dist/mcp/index.d.ts +5 -0
  62. package/dist/mcp/index.js +5 -0
  63. package/dist/mcp/redaction.d.ts +3 -0
  64. package/dist/mcp/redaction.js +40 -0
  65. package/dist/pairing/index.d.ts +38 -0
  66. package/dist/pairing/index.js +180 -0
  67. package/dist/state/index.d.ts +76 -0
  68. package/dist/state/index.js +242 -0
  69. package/dist/workspaces/index.d.ts +13 -0
  70. package/dist/workspaces/index.js +110 -0
  71. package/package.json +76 -0
@@ -0,0 +1,416 @@
1
+ import { spawn } from "node:child_process";
2
+ import { z } from "zod";
3
+ import { HarnessAdapterError } from "../types.js";
4
+ import { firstLine, processFailureMessage, spawnProviderCliProcess, } from "./cli-process.js";
5
+ import { adapterMcpServers, assertCliMcpAttachmentProxied, cliMcpServerConfigName, normalizeProviderModels, turnFailedEvent, } from "./shared.js";
6
+ const DEFAULT_PROBE_TIMEOUT_MS = 5_000;
7
+ const DEFAULT_SERVER_START_TIMEOUT_MS = 10_000;
8
+ const DEFAULT_EVENT_SETTLE_TIMEOUT_MS = 5_000;
9
+ const sessionSchema = z.object({ id: z.string().min(1) }).passthrough();
10
+ const promptResponseSchema = z
11
+ .object({
12
+ parts: z.array(z.object({ type: z.string(), text: z.string().optional() }).passthrough()).default([]),
13
+ })
14
+ .passthrough();
15
+ const eventSchema = z
16
+ .object({
17
+ type: z.string(),
18
+ properties: z.record(z.string(), z.unknown()),
19
+ })
20
+ .passthrough();
21
+ export class OpenCodeHarnessAdapter {
22
+ driverKind = "opencode";
23
+ #runtimeFactory;
24
+ #probeTimeoutMs;
25
+ #runtimes = new Map();
26
+ #activeTurns = new Map();
27
+ #stopReasons = new Map();
28
+ constructor(options = {}) {
29
+ this.#runtimeFactory = options.runtimeFactory ?? startOpenCodeRuntime;
30
+ this.#probeTimeoutMs = options.probeTimeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS;
31
+ }
32
+ async probe(provider) {
33
+ const executable = provider.executable_path ?? "opencode";
34
+ const result = await runProbe(executable, [...provider.launch_args, "--version"], provider, this.#probeTimeoutMs);
35
+ const version = firstLine(result.stdout);
36
+ if (result.timedOut || result.error || result.exitCode !== 0) {
37
+ return {
38
+ provider_instance_id: provider.id,
39
+ driver_kind: this.driverKind,
40
+ installed: false,
41
+ available: false,
42
+ status: "unavailable",
43
+ message: result.timedOut
44
+ ? "OpenCode version probe timed out."
45
+ : processFailureMessage(result, "OpenCode executable is not available.", provider.home ? [executable, provider.home] : [executable]),
46
+ models: normalizeProviderModels(provider.models),
47
+ };
48
+ }
49
+ return {
50
+ provider_instance_id: provider.id,
51
+ driver_kind: this.driverKind,
52
+ installed: true,
53
+ available: true,
54
+ status: "ready",
55
+ ...(version ? { version } : {}),
56
+ models: normalizeProviderModels(provider.models),
57
+ };
58
+ }
59
+ async validateStart() {
60
+ return;
61
+ }
62
+ async startSession(input) {
63
+ if (this.#runtimes.has(input.payload.session_id)) {
64
+ throw new HarnessAdapterError("opencode_session_exists", `OpenCode session '${input.payload.session_id}' already exists.`);
65
+ }
66
+ const mcpServers = {};
67
+ for (const attachment of adapterMcpServers(input.mcpServers, input.payload)) {
68
+ assertCliMcpAttachmentProxied(attachment, "OpenCode", "opencode");
69
+ const name = cliMcpServerConfigName(attachment.name, "opencode");
70
+ mcpServers[name] = { type: "remote", url: attachment.url, enabled: true };
71
+ }
72
+ const runtime = await this.#runtimeFactory({
73
+ executable: input.provider.executable_path ?? "opencode",
74
+ launchArgs: input.provider.launch_args,
75
+ cwd: input.payload.cwd,
76
+ env: providerEnvironment(input.provider),
77
+ mcpServers,
78
+ });
79
+ this.#runtimes.set(input.payload.session_id, runtime);
80
+ return { adapter_session_id: runtime.sessionId };
81
+ }
82
+ async sendTurn(input) {
83
+ const runtime = this.#requireRuntime(input.payload.session_id);
84
+ if (this.#activeTurns.has(input.payload.session_id)) {
85
+ throw new HarnessAdapterError("opencode_turn_in_progress", `OpenCode session '${input.payload.session_id}' already has an active turn.`);
86
+ }
87
+ this.#activeTurns.set(input.payload.session_id, input.payload.turn_id);
88
+ try {
89
+ const finalText = await runtime.sendTurn({
90
+ turnId: input.payload.turn_id,
91
+ input: input.payload.input,
92
+ model: input.payload.model_selection?.model ?? input.startPayload.model_selection.model,
93
+ emitEvent: input.emitEvent ?? (() => { }),
94
+ });
95
+ return [
96
+ {
97
+ event_type: "turn.completed",
98
+ turn_id: input.payload.turn_id,
99
+ data: { status: "completed", final_output: { final_text: finalText } },
100
+ },
101
+ ];
102
+ }
103
+ catch (error) {
104
+ if (this.#stopReasons.has(input.payload.session_id))
105
+ return [];
106
+ return [
107
+ turnFailedEvent(input.payload.turn_id, "provider_error", "opencode_turn_failed", error instanceof Error ? error.message : "OpenCode turn failed.", false),
108
+ ];
109
+ }
110
+ finally {
111
+ this.#activeTurns.delete(input.payload.session_id);
112
+ this.#stopReasons.delete(input.payload.session_id);
113
+ }
114
+ }
115
+ async cancelTurn(input) {
116
+ if (this.#activeTurns.get(input.sessionId) !== input.turnId) {
117
+ return [];
118
+ }
119
+ this.#stopReasons.set(input.sessionId, "cancel_requested");
120
+ await this.#requireRuntime(input.sessionId).cancelTurn();
121
+ return [
122
+ {
123
+ event_type: "turn.cancelled",
124
+ turn_id: input.turnId,
125
+ data: { status: "cancelled", final_output: { exit_reason: "cancel_requested" } },
126
+ },
127
+ ];
128
+ }
129
+ async stopSession(input) {
130
+ const runtime = this.#runtimes.get(input.sessionId);
131
+ if (!runtime) {
132
+ return [];
133
+ }
134
+ const activeTurnId = this.#activeTurns.get(input.sessionId);
135
+ if (activeTurnId)
136
+ this.#stopReasons.set(input.sessionId, "session_stopped");
137
+ this.#runtimes.delete(input.sessionId);
138
+ await runtime.close();
139
+ return activeTurnId
140
+ ? [
141
+ {
142
+ event_type: "turn.cancelled",
143
+ turn_id: activeTurnId,
144
+ data: { status: "cancelled", final_output: { exit_reason: "session_stopped" } },
145
+ },
146
+ ]
147
+ : [];
148
+ }
149
+ #requireRuntime(sessionId) {
150
+ const runtime = this.#runtimes.get(sessionId);
151
+ if (!runtime) {
152
+ throw new HarnessAdapterError("opencode_session_not_found", `OpenCode session '${sessionId}' is not active.`);
153
+ }
154
+ return runtime;
155
+ }
156
+ }
157
+ async function runProbe(executable, args, provider, timeoutMs) {
158
+ const handle = spawnProviderCliProcess(executable, args, { cwd: process.cwd(), env: providerEnvironment(provider) });
159
+ let timeout;
160
+ const timedOut = new Promise((resolve) => {
161
+ timeout = setTimeout(() => {
162
+ handle.kill("SIGKILL");
163
+ resolve({ exitCode: null, signal: "SIGKILL", stdout: "", stderr: "", error: undefined, timedOut: true });
164
+ }, timeoutMs);
165
+ });
166
+ const result = await Promise.race([handle.result, timedOut]);
167
+ if (timeout)
168
+ clearTimeout(timeout);
169
+ return result;
170
+ }
171
+ async function startOpenCodeRuntime(input) {
172
+ const config = Object.keys(input.mcpServers).length > 0 ? { mcp: input.mcpServers } : {};
173
+ const child = spawn(input.executable, [...input.launchArgs, "serve", "--hostname=127.0.0.1", "--port=0"], {
174
+ cwd: input.cwd,
175
+ env: { ...process.env, ...input.env, OPENCODE_CONFIG_CONTENT: JSON.stringify(config) },
176
+ stdio: "pipe",
177
+ detached: process.platform !== "win32",
178
+ });
179
+ child.stdin.end();
180
+ const baseUrl = await waitForServerUrl(child, DEFAULT_SERVER_START_TIMEOUT_MS);
181
+ try {
182
+ const session = sessionSchema.parse(await fetchJson(new URL(`/session?directory=${encodeURIComponent(input.cwd)}`, baseUrl), {
183
+ method: "POST",
184
+ headers: { "content-type": "application/json" },
185
+ body: JSON.stringify({ title: "HCP session" }),
186
+ }));
187
+ return new HttpOpenCodeRuntime(child, baseUrl, input.cwd, session.id);
188
+ }
189
+ catch (error) {
190
+ terminateProcess(child);
191
+ throw error;
192
+ }
193
+ }
194
+ class HttpOpenCodeRuntime {
195
+ #child;
196
+ #baseUrl;
197
+ #cwd;
198
+ sessionId;
199
+ #activeRequest;
200
+ constructor(child, baseUrl, cwd, sessionId) {
201
+ this.#child = child;
202
+ this.#baseUrl = baseUrl;
203
+ this.#cwd = cwd;
204
+ this.sessionId = sessionId;
205
+ }
206
+ async sendTurn(input) {
207
+ if (this.#activeRequest) {
208
+ throw new Error(`OpenCode session '${this.sessionId}' already has an active HTTP request.`);
209
+ }
210
+ const abortController = new AbortController();
211
+ this.#activeRequest = abortController;
212
+ const streamReady = createEventStream(new URL(`/event?directory=${encodeURIComponent(this.#cwd)}`, this.#baseUrl), this.sessionId, input, abortController.signal);
213
+ try {
214
+ await streamReady.ready;
215
+ const model = parseModel(input.model);
216
+ const response = promptResponseSchema.parse(await fetchJson(new URL(`/session/${encodeURIComponent(this.sessionId)}/message?directory=${encodeURIComponent(this.#cwd)}`, this.#baseUrl), {
217
+ method: "POST",
218
+ headers: { "content-type": "application/json" },
219
+ body: JSON.stringify({
220
+ parts: [{ type: "text", text: input.input }],
221
+ ...(model ? { model } : {}),
222
+ }),
223
+ signal: abortController.signal,
224
+ }));
225
+ await waitWithTimeout(streamReady.settled, DEFAULT_EVENT_SETTLE_TIMEOUT_MS, "OpenCode did not emit session.idle.");
226
+ return response.parts
227
+ .filter((part) => part.type === "text" && part.text !== undefined)
228
+ .map((part) => part.text ?? "")
229
+ .join("");
230
+ }
231
+ finally {
232
+ abortController.abort();
233
+ await streamReady.completed;
234
+ this.#activeRequest = undefined;
235
+ }
236
+ }
237
+ async cancelTurn() {
238
+ this.#activeRequest?.abort();
239
+ await fetchJson(new URL(`/session/${encodeURIComponent(this.sessionId)}/abort?directory=${encodeURIComponent(this.#cwd)}`, this.#baseUrl), {
240
+ method: "POST",
241
+ });
242
+ }
243
+ async close() {
244
+ this.#activeRequest?.abort();
245
+ terminateProcess(this.#child);
246
+ }
247
+ }
248
+ function createEventStream(url, sessionId, input, signal) {
249
+ let markReady = () => { };
250
+ let rejectReady = () => { };
251
+ const ready = new Promise((resolve, reject) => {
252
+ markReady = resolve;
253
+ rejectReady = reject;
254
+ });
255
+ let markSettled = () => { };
256
+ let rejectSettled = () => { };
257
+ const settled = new Promise((resolve, reject) => {
258
+ markSettled = resolve;
259
+ rejectSettled = reject;
260
+ });
261
+ const completed = (async () => {
262
+ try {
263
+ const response = await fetch(url, { headers: { accept: "text/event-stream" }, signal });
264
+ if (!response.ok || !response.body) {
265
+ throw new Error(`OpenCode event stream failed with HTTP ${response.status}.`);
266
+ }
267
+ markReady();
268
+ await consumeSse(response.body, (value) => {
269
+ const outcome = emitOpenCodeEvent(value, sessionId, input);
270
+ if (outcome === "settled")
271
+ markSettled();
272
+ if (outcome instanceof Error)
273
+ rejectSettled(outcome);
274
+ });
275
+ }
276
+ catch (error) {
277
+ if (!signal.aborted) {
278
+ const normalized = error instanceof Error ? error : new Error("OpenCode event stream failed.");
279
+ rejectReady(normalized);
280
+ throw normalized;
281
+ }
282
+ markReady();
283
+ }
284
+ })();
285
+ return { ready, settled, completed };
286
+ }
287
+ async function consumeSse(stream, onData) {
288
+ const reader = stream.getReader();
289
+ const decoder = new TextDecoder();
290
+ let buffered = "";
291
+ while (true) {
292
+ const result = await reader.read();
293
+ if (result.done)
294
+ break;
295
+ buffered += decoder.decode(result.value, { stream: true }).replaceAll("\r\n", "\n");
296
+ let boundary;
297
+ while ((boundary = buffered.indexOf("\n\n")) >= 0) {
298
+ const block = buffered.slice(0, boundary);
299
+ buffered = buffered.slice(boundary + 2);
300
+ const data = block
301
+ .split("\n")
302
+ .filter((line) => line.startsWith("data:"))
303
+ .map((line) => line.slice(5).trimStart())
304
+ .join("\n");
305
+ if (data.length > 0)
306
+ onData(JSON.parse(data));
307
+ }
308
+ }
309
+ }
310
+ function emitOpenCodeEvent(value, sessionId, input) {
311
+ const event = eventSchema.parse(value);
312
+ if (event.type === "session.idle") {
313
+ return event.properties.sessionID === sessionId ? "settled" : "continue";
314
+ }
315
+ if (event.type === "session.error" && (event.properties.sessionID === undefined || event.properties.sessionID === sessionId)) {
316
+ return new Error("OpenCode reported a session error.");
317
+ }
318
+ if (event.type !== "message.part.updated")
319
+ return "continue";
320
+ const part = event.properties.part;
321
+ const partSchema = z.object({ sessionID: z.string(), type: z.string() }).passthrough();
322
+ const parsedPart = partSchema.parse(part);
323
+ if (parsedPart.sessionID !== sessionId)
324
+ return "continue";
325
+ const delta = event.properties.delta;
326
+ if (typeof delta !== "string" || delta.length === 0)
327
+ return "continue";
328
+ if (parsedPart.type !== "text" && parsedPart.type !== "reasoning")
329
+ return "continue";
330
+ input.emitEvent({
331
+ event_type: parsedPart.type === "reasoning" ? "reasoning.delta" : "content.delta",
332
+ turn_id: input.turnId,
333
+ data: { delta },
334
+ });
335
+ return "continue";
336
+ }
337
+ async function fetchJson(url, init) {
338
+ const response = await fetch(url, init);
339
+ if (!response.ok) {
340
+ const body = await response.text();
341
+ throw new Error(`OpenCode request failed with HTTP ${response.status}${body ? `: ${body}` : "."}`);
342
+ }
343
+ return (await response.json());
344
+ }
345
+ function waitForServerUrl(child, timeoutMs) {
346
+ return new Promise((resolve, reject) => {
347
+ let output = "";
348
+ let settled = false;
349
+ const timeout = setTimeout(() => {
350
+ settleError(new Error(`Timed out waiting ${timeoutMs}ms for OpenCode server startup.`));
351
+ }, timeoutMs);
352
+ const settleError = (error) => {
353
+ if (settled)
354
+ return;
355
+ settled = true;
356
+ clearTimeout(timeout);
357
+ terminateProcess(child);
358
+ reject(error);
359
+ };
360
+ const inspect = (chunk) => {
361
+ if (settled)
362
+ return;
363
+ output += chunk.toString("utf8");
364
+ const match = output.match(/opencode server listening[^\n]*on\s+(https?:\/\/[^\s]+)/i);
365
+ if (!match?.[1])
366
+ return;
367
+ settled = true;
368
+ clearTimeout(timeout);
369
+ resolve(match[1]);
370
+ };
371
+ child.stdout.on("data", inspect);
372
+ child.stderr.on("data", inspect);
373
+ child.once("error", settleError);
374
+ child.once("exit", (code) => {
375
+ settleError(new Error(`OpenCode server exited before startup with code ${code ?? "unknown"}.`));
376
+ });
377
+ });
378
+ }
379
+ function terminateProcess(child) {
380
+ if (child.exitCode !== null || child.signalCode !== null)
381
+ return;
382
+ if (process.platform !== "win32" && child.pid) {
383
+ try {
384
+ process.kill(-child.pid, "SIGTERM");
385
+ return;
386
+ }
387
+ catch (error) {
388
+ if (!(error instanceof Error))
389
+ throw error;
390
+ }
391
+ }
392
+ child.kill("SIGTERM");
393
+ }
394
+ function parseModel(model) {
395
+ const separator = model.indexOf("/");
396
+ if (separator <= 0 || separator === model.length - 1)
397
+ return undefined;
398
+ return { providerID: model.slice(0, separator), modelID: model.slice(separator + 1) };
399
+ }
400
+ function providerEnvironment(provider) {
401
+ return { ...(provider.home ? { HOME: provider.home } : {}), ...provider.env };
402
+ }
403
+ async function waitWithTimeout(completion, timeoutMs, message) {
404
+ let timeout;
405
+ const expired = new Promise((_resolve, reject) => {
406
+ timeout = setTimeout(() => reject(new Error(message)), timeoutMs);
407
+ });
408
+ try {
409
+ await Promise.race([completion, expired]);
410
+ }
411
+ finally {
412
+ if (timeout)
413
+ clearTimeout(timeout);
414
+ }
415
+ }
416
+ //# sourceMappingURL=opencode.js.map
@@ -0,0 +1,9 @@
1
+ import type { HarnessModel, HcpSessionStartPayload } from "@harness-control/protocol";
2
+ import type { ProviderInstanceConfig } from "../../../config/index.js";
3
+ import { type HarnessAdapterEvent, type HarnessAdapterMcpServer } from "../types.js";
4
+ export declare function normalizeProviderModels(models: ProviderInstanceConfig["models"]): HarnessModel[];
5
+ export declare function turnFailedEvent(turnId: string, exitReason: string, code: string, message: string, retryable: boolean, details?: Record<string, unknown>): HarnessAdapterEvent;
6
+ export declare function assertCliMcpAttachmentProxied(attachment: HarnessAdapterMcpServer, providerLabel: string, errorPrefix: string): void;
7
+ export declare function adapterMcpServers(resolved: HarnessAdapterMcpServer[] | undefined, payload: HcpSessionStartPayload): HarnessAdapterMcpServer[];
8
+ export declare function cliMcpServerConfigName(name: string, errorPrefix: string): string;
9
+ //# sourceMappingURL=shared.d.ts.map
@@ -0,0 +1,97 @@
1
+ import { HarnessAdapterError, } from "../types.js";
2
+ export function normalizeProviderModels(models) {
3
+ return models.map((model) => {
4
+ const normalized = {
5
+ id: model.id,
6
+ label: model.label,
7
+ capabilities: {
8
+ option_descriptors: model.capabilities.option_descriptors.map((option) => {
9
+ const normalizedOption = {
10
+ id: option.id,
11
+ label: option.label,
12
+ type: option.type,
13
+ };
14
+ if (option.values !== undefined) {
15
+ normalizedOption.values = option.values;
16
+ }
17
+ if (option.default_value !== undefined) {
18
+ normalizedOption.default_value = option.default_value;
19
+ }
20
+ if (option.current_value !== undefined) {
21
+ normalizedOption.current_value = option.current_value;
22
+ }
23
+ if (option.prompt_injected_values !== undefined) {
24
+ normalizedOption.prompt_injected_values = option.prompt_injected_values;
25
+ }
26
+ return normalizedOption;
27
+ }),
28
+ },
29
+ };
30
+ if (model.is_default !== undefined) {
31
+ normalized.is_default = model.is_default;
32
+ }
33
+ return normalized;
34
+ });
35
+ }
36
+ export function turnFailedEvent(turnId, exitReason, code, message, retryable, details) {
37
+ return {
38
+ event_type: "turn.failed",
39
+ turn_id: turnId,
40
+ data: {
41
+ status: "failed",
42
+ final_output: {
43
+ exit_reason: exitReason,
44
+ },
45
+ error: {
46
+ code,
47
+ message,
48
+ retryable,
49
+ ...(details ? { details } : {}),
50
+ },
51
+ },
52
+ };
53
+ }
54
+ export function assertCliMcpAttachmentProxied(attachment, providerLabel, errorPrefix) {
55
+ cliMcpServerConfigName(attachment.name, errorPrefix);
56
+ if (Object.keys(attachment.headers).length > 0) {
57
+ throw new HarnessAdapterError(`${errorPrefix}_mcp_attachment_requires_proxy`, `${providerLabel} MCP attachment '${attachment.name}' must not expose platform headers to ${providerLabel}; route it through the runner-owned proxy.`);
58
+ }
59
+ let parsedUrl;
60
+ try {
61
+ parsedUrl = new URL(attachment.url);
62
+ }
63
+ catch (error) {
64
+ if (error instanceof Error) {
65
+ throw new HarnessAdapterError(`${errorPrefix}_mcp_attachment_url_invalid`, error.message);
66
+ }
67
+ throw error;
68
+ }
69
+ const isLoopback = parsedUrl.protocol === "http:" && (parsedUrl.hostname === "127.0.0.1" || parsedUrl.hostname === "localhost");
70
+ if (!isLoopback) {
71
+ throw new HarnessAdapterError(`${errorPrefix}_mcp_attachment_requires_proxy`, `${providerLabel} MCP attachments must be routed through a runner-owned loopback MCP proxy so HCP proof headers can be injected.`);
72
+ }
73
+ }
74
+ export function adapterMcpServers(resolved, payload) {
75
+ if (resolved)
76
+ return resolved;
77
+ return payload.mcp_servers.map((attachment) => {
78
+ if (attachment.transport !== "streamable_http") {
79
+ throw new HarnessAdapterError("mcp_stdio_profile_unresolved", `Runner MCP profile '${attachment.profile_id}' must be resolved before starting a provider adapter.`);
80
+ }
81
+ return {
82
+ name: attachment.name,
83
+ transport: "streamable_http",
84
+ url: attachment.url,
85
+ headers: attachment.headers,
86
+ ...(attachment.allowed_tools ? { allowed_tools: attachment.allowed_tools } : {}),
87
+ ...(attachment.denied_tools ? { denied_tools: attachment.denied_tools } : {}),
88
+ };
89
+ });
90
+ }
91
+ export function cliMcpServerConfigName(name, errorPrefix) {
92
+ if (!/^[A-Za-z0-9_-]+$/.test(name)) {
93
+ throw new HarnessAdapterError(`${errorPrefix}_mcp_attachment_name_invalid`, `MCP attachment name '${name}' must contain only ASCII letters, numbers, underscores, or hyphens.`);
94
+ }
95
+ return name;
96
+ }
97
+ //# sourceMappingURL=shared.js.map
@@ -0,0 +1,12 @@
1
+ import type { ProviderInstanceConfig } from "../../config/index.js";
2
+ import type { ProviderDriverStatus } from "../../host/provider-registry.js";
3
+ import { type HarnessAdapter } from "./types.js";
4
+ export declare class HarnessAdapterRegistry {
5
+ #private;
6
+ constructor(adapters: HarnessAdapter[]);
7
+ get(driverKind: string): HarnessAdapter | undefined;
8
+ require(driverKind: string): HarnessAdapter;
9
+ probeProviders(providers: ProviderInstanceConfig[]): Promise<ProviderDriverStatus[]>;
10
+ }
11
+ export declare function createDefaultHarnessAdapterRegistry(): HarnessAdapterRegistry;
12
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1,47 @@
1
+ import { ClaudeHarnessAdapter } from "./providers/claude.js";
2
+ import { CodexHarnessAdapter } from "./providers/codex.js";
3
+ import { MockHarnessAdapter } from "./providers/mock.js";
4
+ import { OpenCodeHarnessAdapter } from "./providers/opencode.js";
5
+ import { HarnessAdapterError } from "./types.js";
6
+ export class HarnessAdapterRegistry {
7
+ #adapters;
8
+ constructor(adapters) {
9
+ this.#adapters = new Map(adapters.map((adapter) => [adapter.driverKind, adapter]));
10
+ }
11
+ get(driverKind) {
12
+ return this.#adapters.get(driverKind);
13
+ }
14
+ require(driverKind) {
15
+ const adapter = this.get(driverKind);
16
+ if (!adapter) {
17
+ throw new HarnessAdapterError("provider_driver_unavailable", `Provider driver '${driverKind}' is not available in this runner.`);
18
+ }
19
+ return adapter;
20
+ }
21
+ async probeProviders(providers) {
22
+ return await Promise.all(providers.map(async (provider) => {
23
+ const adapter = this.get(provider.driver_kind);
24
+ if (!adapter) {
25
+ return {
26
+ provider_instance_id: provider.id,
27
+ driver_kind: provider.driver_kind,
28
+ installed: false,
29
+ available: false,
30
+ status: "unavailable",
31
+ message: `Unsupported provider driver '${provider.driver_kind}'.`,
32
+ models: [],
33
+ };
34
+ }
35
+ return adapter.probe(provider);
36
+ }));
37
+ }
38
+ }
39
+ export function createDefaultHarnessAdapterRegistry() {
40
+ return new HarnessAdapterRegistry([
41
+ new MockHarnessAdapter(),
42
+ new CodexHarnessAdapter(),
43
+ new ClaudeHarnessAdapter(),
44
+ new OpenCodeHarnessAdapter(),
45
+ ]);
46
+ }
47
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1,54 @@
1
+ import type { HcpEventType, HcpSessionStartPayload, HcpTurnSendPayload } from "@harness-control/protocol";
2
+ import type { ProviderInstanceConfig } from "../../config/index.js";
3
+ import type { ProviderDriverStatus } from "../../host/provider-registry.js";
4
+ export type HarnessAdapterEvent = {
5
+ event_type: HcpEventType;
6
+ turn_id?: string;
7
+ data: Record<string, unknown>;
8
+ };
9
+ export type HarnessAdapterSession = {
10
+ adapter_session_id: string;
11
+ };
12
+ export type HarnessAdapterMcpServer = {
13
+ name: string;
14
+ transport: "streamable_http";
15
+ url: string;
16
+ headers: Record<string, string>;
17
+ allowed_tools?: string[];
18
+ denied_tools?: string[];
19
+ };
20
+ export type HarnessAdapterStartInput = {
21
+ payload: HcpSessionStartPayload;
22
+ provider: ProviderInstanceConfig;
23
+ mcpServers?: HarnessAdapterMcpServer[];
24
+ };
25
+ export type HarnessAdapterTurnInput = {
26
+ payload: HcpTurnSendPayload;
27
+ session: HarnessAdapterSession;
28
+ startPayload: HcpSessionStartPayload;
29
+ provider: ProviderInstanceConfig;
30
+ mcpServers?: HarnessAdapterMcpServer[];
31
+ emitEvent?: (event: HarnessAdapterEvent) => void;
32
+ };
33
+ export type HarnessAdapterCancelInput = {
34
+ sessionId: string;
35
+ turnId: string;
36
+ };
37
+ export type HarnessAdapterStopInput = {
38
+ sessionId: string;
39
+ reason?: string;
40
+ };
41
+ export type HarnessAdapter = {
42
+ readonly driverKind: string;
43
+ probe(provider: ProviderInstanceConfig): Promise<ProviderDriverStatus>;
44
+ validateStart(input: HarnessAdapterStartInput): Promise<void>;
45
+ startSession(input: HarnessAdapterStartInput): Promise<HarnessAdapterSession>;
46
+ sendTurn(input: HarnessAdapterTurnInput): Promise<HarnessAdapterEvent[]>;
47
+ cancelTurn(input: HarnessAdapterCancelInput): Promise<HarnessAdapterEvent[]>;
48
+ stopSession(input: HarnessAdapterStopInput): Promise<HarnessAdapterEvent[]>;
49
+ };
50
+ export declare class HarnessAdapterError extends Error {
51
+ readonly code: string;
52
+ constructor(code: string, message: string);
53
+ }
54
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,9 @@
1
+ export class HarnessAdapterError extends Error {
2
+ code;
3
+ constructor(code, message) {
4
+ super(message);
5
+ this.code = code;
6
+ this.name = "HarnessAdapterError";
7
+ }
8
+ }
9
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,8 @@
1
+ export { HarnessAdapterError, type HarnessAdapter, type HarnessAdapterCancelInput, type HarnessAdapterEvent, type HarnessAdapterMcpServer, type HarnessAdapterSession, type HarnessAdapterStartInput, type HarnessAdapterStopInput, type HarnessAdapterTurnInput, } from "./adapters/types.js";
2
+ export { HarnessAdapterRegistry, createDefaultHarnessAdapterRegistry } from "./adapters/registry.js";
3
+ export { type CliManagedProcess, type CliProcessHandle, type CliProcessResult, type CliProcessRunOptions, type CliProcessSpawner, type CodexProcessHandle, type CodexProcessResult, type CodexProcessRunOptions, type CodexProcessSpawner, spawnProviderCliProcess, startManagedCliProcess, } from "./adapters/providers/cli-process.js";
4
+ export { MockHarnessAdapter } from "./adapters/providers/mock.js";
5
+ export { CodexHarnessAdapter, type CodexHarnessAdapterOptions } from "./adapters/providers/codex.js";
6
+ export { ClaudeHarnessAdapter, type ClaudeHarnessAdapterOptions } from "./adapters/providers/claude.js";
7
+ export { OpenCodeHarnessAdapter, type OpenCodeHarnessAdapterOptions, type OpenCodeRuntime, type OpenCodeRuntimeFactory, type OpenCodeRuntimeTurnInput, } from "./adapters/providers/opencode.js";
8
+ //# sourceMappingURL=adapters.d.ts.map
@@ -0,0 +1,8 @@
1
+ export { HarnessAdapterError, } from "./adapters/types.js";
2
+ export { HarnessAdapterRegistry, createDefaultHarnessAdapterRegistry } from "./adapters/registry.js";
3
+ export { spawnProviderCliProcess, startManagedCliProcess, } from "./adapters/providers/cli-process.js";
4
+ export { MockHarnessAdapter } from "./adapters/providers/mock.js";
5
+ export { CodexHarnessAdapter } from "./adapters/providers/codex.js";
6
+ export { ClaudeHarnessAdapter } from "./adapters/providers/claude.js";
7
+ export { OpenCodeHarnessAdapter, } from "./adapters/providers/opencode.js";
8
+ //# sourceMappingURL=adapters.js.map