@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,620 @@
1
+ import { realpath } from "node:fs/promises";
2
+ import { isAbsolute, relative, resolve } from "node:path";
3
+ import { LocalCapabilityEngine, LocalCapabilityLeaseManager, LocalCapabilityPolicyError, } from "../local-actions/index.js";
4
+ import { McpAttachmentClient } from "../mcp/McpAttachmentClient.js";
5
+ import { McpProxyServer } from "../mcp/McpProxyServer.js";
6
+ import { McpStdioProfileClient } from "../mcp/McpStdioProfileClient.js";
7
+ import { MemoryRunnerStateStore } from "../state/index.js";
8
+ import { HarnessAdapterError, createDefaultHarnessAdapterRegistry, } from "./adapters.js";
9
+ export class HarnessSessionError extends Error {
10
+ code;
11
+ constructor(code, message) {
12
+ super(message);
13
+ this.code = code;
14
+ this.name = "HarnessSessionError";
15
+ }
16
+ }
17
+ export class HarnessSessionManager {
18
+ #config;
19
+ #hostId;
20
+ #localCapabilities;
21
+ #localCapabilityEngine;
22
+ #mcpProofSigner;
23
+ #mcpClientFactory;
24
+ #auditLogger;
25
+ #stateStore;
26
+ #adapterRegistry;
27
+ #sessions = new Map();
28
+ #turnIdsBySession = new Map();
29
+ constructor(config, options = {}) {
30
+ const resolvedOptions = typeof options === "string" ? { hostId: options } : options;
31
+ this.#config = config;
32
+ this.#hostId = resolvedOptions.hostId ?? config.host_id ?? config.runner_id;
33
+ this.#localCapabilities = new LocalCapabilityLeaseManager(config, this.#hostId);
34
+ this.#localCapabilityEngine = new LocalCapabilityEngine(this.#localCapabilities);
35
+ this.#mcpProofSigner = resolvedOptions.mcpProofSigner;
36
+ this.#mcpClientFactory = resolvedOptions.mcpClientFactory ?? defaultMcpClientFactory;
37
+ this.#auditLogger = resolvedOptions.auditLogger;
38
+ this.#stateStore =
39
+ resolvedOptions.stateStore ??
40
+ new MemoryRunnerStateStore(resolvedOptions.replayRetentionEventsPerSession === undefined
41
+ ? {}
42
+ : { eventRetentionPerSession: resolvedOptions.replayRetentionEventsPerSession });
43
+ this.#adapterRegistry = resolvedOptions.adapterRegistry ?? createDefaultHarnessAdapterRegistry();
44
+ }
45
+ #workspaceQueue = Promise.resolve();
46
+ #serializeWorkspace(operation) {
47
+ const result = this.#workspaceQueue.then(operation);
48
+ this.#workspaceQueue = result.then(() => undefined, () => undefined);
49
+ return result;
50
+ }
51
+ updateWorkspaceConfiguration(operation) {
52
+ return this.#serializeWorkspace(async () => {
53
+ if (this.activeSessionCount() > 0)
54
+ throw new HarnessSessionError("workspace_busy", "Stop active sessions before changing workspaces.");
55
+ return operation();
56
+ });
57
+ }
58
+ activeSessionCount() {
59
+ return this.#sessions.size;
60
+ }
61
+ providerDriverStatuses() {
62
+ return this.#adapterRegistry.probeProviders(this.#config.provider_instances);
63
+ }
64
+ localCapabilityEngine() {
65
+ return this.#localCapabilityEngine;
66
+ }
67
+ stateStore() {
68
+ return this.#stateStore;
69
+ }
70
+ retainedEventRanges() {
71
+ return this.#stateStore.retainedEventRanges();
72
+ }
73
+ replayEventsAfter(cursor) {
74
+ const events = [];
75
+ const unavailable = [];
76
+ const retainedRanges = this.#stateStore.retainedEventRanges() ?? { sessions: [] };
77
+ for (const sessionCursor of cursor.sessions) {
78
+ const replayed = this.#stateStore.replayEventsAfter(sessionCursor.session_id, sessionCursor.last_event_sequence);
79
+ if (!replayed) {
80
+ const retainedRange = retainedRanges.sessions.find((range) => range.session_id === sessionCursor.session_id);
81
+ unavailable.push({
82
+ session_id: sessionCursor.session_id,
83
+ requested_after_sequence: sessionCursor.last_event_sequence,
84
+ reason: retainedRange ? "cursor_outside_retention" : "no_retained_events",
85
+ ...(retainedRange
86
+ ? {
87
+ retained_range: {
88
+ first_event_sequence: retainedRange.first_event_sequence,
89
+ last_event_sequence: retainedRange.last_event_sequence,
90
+ },
91
+ }
92
+ : {}),
93
+ });
94
+ continue;
95
+ }
96
+ events.push(...replayed);
97
+ }
98
+ return { events, unavailable };
99
+ }
100
+ sessionSnapshot(commandId, sessionId) {
101
+ const snapshot = this.#stateStore.sessionSnapshot(commandId, sessionId);
102
+ if (!snapshot) {
103
+ throw new HarnessSessionError("session_snapshot_unavailable", `Session '${sessionId}' has no retained events from which to build a snapshot.`);
104
+ }
105
+ return snapshot;
106
+ }
107
+ async resolveLocalActionContext(payload) {
108
+ const session = this.#sessions.get(payload.attribution.session_id);
109
+ if (!session) {
110
+ throw new LocalCapabilityPolicyError("local_capability_lease_missing", `Session '${payload.attribution.session_id}' does not have an active local capability lease.`);
111
+ }
112
+ if (session.workspaceId !== payload.attribution.workspace_id) {
113
+ throw new LocalCapabilityPolicyError("local_capability_workspace_mismatch", `Local action workspace '${payload.attribution.workspace_id}' does not match active session workspace '${session.workspaceId}'.`);
114
+ }
115
+ if (session.providerInstanceId !== payload.attribution.provider_instance_id) {
116
+ throw new LocalCapabilityPolicyError("local_capability_provider_mismatch", `Local action provider '${payload.attribution.provider_instance_id}' does not match active session provider '${session.providerInstanceId}'.`);
117
+ }
118
+ const lease = session.localCapabilityLease;
119
+ if (!lease) {
120
+ throw new LocalCapabilityPolicyError("local_capability_lease_missing", `Session '${session.sessionId}' was not started with a local capability lease.`);
121
+ }
122
+ assertRequestLeaseMatchesActiveLease(payload, lease);
123
+ const workspaceRoot = this.#requireWorkspaceRoot(session.workspaceId);
124
+ await assertSandboxMatchesSession(payload, session, workspaceRoot);
125
+ return {
126
+ session_id: session.sessionId,
127
+ turn_id: payload.attribution.turn_id,
128
+ workspace_id: session.workspaceId,
129
+ provider_instance_id: session.providerInstanceId,
130
+ workspace_root: workspaceRoot,
131
+ sandbox_mode: session.startPayload.sandbox_mode,
132
+ lease,
133
+ };
134
+ }
135
+ recordLocalActionEvents(sessionId, turnId, events) {
136
+ const session = this.#sessions.get(sessionId);
137
+ if (!session) {
138
+ throw new LocalCapabilityPolicyError("local_capability_lease_missing", `Session '${sessionId}' is not active.`);
139
+ }
140
+ return events.map((event) => this.#event(sessionId, turnId, event.event_type, event.data));
141
+ }
142
+ startSession(payload) {
143
+ return this.#serializeWorkspace(() => this.#startSession(payload));
144
+ }
145
+ async #startSession(payload) {
146
+ if (payload.workspace_preflight !== undefined) {
147
+ throw new HarnessSessionError("preflight_unsupported", "Workspace preflight expectations are not implemented by this runner.");
148
+ }
149
+ if (this.#sessions.has(payload.session_id) || this.#stateStore.hasSessionEvents(payload.session_id)) {
150
+ throw new HarnessSessionError("session_exists", `Session '${payload.session_id}' already exists.`);
151
+ }
152
+ const provider = this.#requireProvider(payload.provider_instance_id, payload.driver_kind);
153
+ await this.#assertWorkspaceAllowed(payload.workspace_id, payload.cwd);
154
+ const localCapabilityLease = this.#localCapabilities.validateSessionLease(payload, provider);
155
+ const adapter = this.#adapterRegistry.require(provider.driver_kind);
156
+ await adapter.validateStart({ payload, provider });
157
+ const mcpAttachments = await this.#attachMcpServers(payload, provider);
158
+ const adapterStartPayload = payload;
159
+ let adapterSession;
160
+ try {
161
+ adapterSession = await adapter.startSession({
162
+ payload: adapterStartPayload,
163
+ provider,
164
+ mcpServers: mcpAttachments.adapterAttachments,
165
+ });
166
+ }
167
+ catch (error) {
168
+ await cleanupAdapterSessionStartFailure(adapter, payload.session_id, mcpAttachments.clients, "adapter_start_failed", error);
169
+ throw error;
170
+ }
171
+ const session = {
172
+ sessionId: payload.session_id,
173
+ workspaceId: payload.workspace_id,
174
+ providerInstanceId: provider.id,
175
+ driverKind: provider.driver_kind,
176
+ cwd: payload.cwd,
177
+ startPayload: adapterStartPayload,
178
+ adapter,
179
+ adapterSession,
180
+ ...(localCapabilityLease ? { localCapabilityLease } : {}),
181
+ mcpClients: mcpAttachments.clients,
182
+ mcpServers: mcpAttachments.adapterAttachments,
183
+ };
184
+ this.#sessions.set(payload.session_id, session);
185
+ this.#turnIdsBySession.set(payload.session_id, new Set());
186
+ const events = [
187
+ this.#event(payload.session_id, undefined, "session.started", {
188
+ provider_instance_id: provider.id,
189
+ driver_kind: provider.driver_kind,
190
+ workspace_id: payload.workspace_id,
191
+ cwd: payload.cwd,
192
+ sandbox_mode: payload.sandbox_mode,
193
+ }),
194
+ this.#event(payload.session_id, undefined, "workspace.preflight.completed", {
195
+ workspace_id: payload.workspace_id,
196
+ cwd: payload.cwd,
197
+ result: "passed",
198
+ }),
199
+ this.#event(payload.session_id, undefined, "session.configured", {
200
+ model_selection: payload.model_selection,
201
+ mcp_server_count: payload.mcp_servers.length,
202
+ local_capabilities: localCapabilityLease?.capabilities.map((capability) => capability.id) ?? [],
203
+ }),
204
+ ];
205
+ if (localCapabilityLease) {
206
+ events.push(this.#event(payload.session_id, undefined, "local_capability.lease.created", {
207
+ lease_id: localCapabilityLease.lease_id,
208
+ workspace_id: localCapabilityLease.workspace_id,
209
+ provider_instance_id: localCapabilityLease.provider_instance_id,
210
+ status: "started",
211
+ }));
212
+ }
213
+ for (const attachment of payload.mcp_servers) {
214
+ events.push(this.#event(payload.session_id, undefined, "mcp.status.updated", {
215
+ attachment: attachment.name,
216
+ status: "connected",
217
+ }));
218
+ }
219
+ for (const discovery of mcpAttachments.discoveredTools) {
220
+ events.push(this.#event(payload.session_id, undefined, "mcp.status.updated", {
221
+ attachment: discovery.attachmentName,
222
+ status: "tools_discovered",
223
+ message: `allowed tools: ${discovery.tools.map((tool) => tool.name).join(", ")}`,
224
+ }));
225
+ }
226
+ await this.#recordAudit({
227
+ event: "session.started",
228
+ session_id: payload.session_id,
229
+ provider_instance_id: provider.id,
230
+ workspace_id: payload.workspace_id,
231
+ data: {
232
+ driver_kind: provider.driver_kind,
233
+ cwd: payload.cwd,
234
+ sandbox_mode: payload.sandbox_mode,
235
+ approval_policy: payload.approval_policy,
236
+ mcp_servers: payload.mcp_servers.map((attachment) => attachment.name),
237
+ local_capabilities: localCapabilityLease?.capabilities.map((capability) => capability.id) ?? [],
238
+ },
239
+ });
240
+ return events;
241
+ }
242
+ sendTurn(payload, onEvent) {
243
+ const session = this.#sessions.get(payload.session_id);
244
+ if (!session) {
245
+ throw new HarnessSessionError("session_not_found", `Session '${payload.session_id}' is not active.`);
246
+ }
247
+ const turnIds = this.#turnIdsBySession.get(payload.session_id) ?? new Set();
248
+ if (turnIds.has(payload.turn_id)) {
249
+ throw new HarnessSessionError("turn_exists", `Turn '${payload.turn_id}' already exists in session '${payload.session_id}'.`);
250
+ }
251
+ turnIds.add(payload.turn_id);
252
+ this.#turnIdsBySession.set(payload.session_id, turnIds);
253
+ return this.#runTurn(payload, session, onEvent);
254
+ }
255
+ async #runTurn(payload, session, onEvent) {
256
+ await Promise.resolve();
257
+ const events = [];
258
+ const startedEvent = this.#event(payload.session_id, payload.turn_id, "turn.started", {
259
+ provider_instance_id: session.providerInstanceId,
260
+ input_length: payload.input.length,
261
+ model_selection: payload.model_selection ?? session.startPayload.model_selection,
262
+ });
263
+ if (onEvent) {
264
+ onEvent(startedEvent);
265
+ }
266
+ else {
267
+ events.push(startedEvent);
268
+ }
269
+ let terminalEventType;
270
+ const emitAdapterEvent = (adapterEvent) => {
271
+ if (adapterEvent.event_type === "turn.started") {
272
+ return;
273
+ }
274
+ if (["turn.completed", "turn.failed", "turn.cancelled", "turn.aborted"].includes(adapterEvent.event_type)) {
275
+ terminalEventType = adapterEvent.event_type;
276
+ }
277
+ const event = this.#event(payload.session_id, adapterEvent.turn_id ?? payload.turn_id, adapterEvent.event_type, adapterEvent.data);
278
+ if (onEvent) {
279
+ onEvent(event);
280
+ }
281
+ else {
282
+ events.push(event);
283
+ }
284
+ };
285
+ const adapterEvents = await session.adapter.sendTurn({
286
+ payload,
287
+ session: session.adapterSession,
288
+ startPayload: session.startPayload,
289
+ provider: this.#requireProvider(session.providerInstanceId, session.driverKind),
290
+ mcpServers: session.mcpServers,
291
+ emitEvent: emitAdapterEvent,
292
+ });
293
+ for (const adapterEvent of adapterEvents) {
294
+ emitAdapterEvent(adapterEvent);
295
+ }
296
+ if (terminalEventType)
297
+ await this.#recordAudit({
298
+ event: terminalEventType,
299
+ session_id: payload.session_id,
300
+ turn_id: payload.turn_id,
301
+ provider_instance_id: session.providerInstanceId,
302
+ workspace_id: session.workspaceId,
303
+ data: {
304
+ input_length: payload.input.length,
305
+ },
306
+ });
307
+ return events;
308
+ }
309
+ recordTurnFailure(sessionId, turnId, error) {
310
+ const message = error instanceof Error ? error.message : "Harness turn failed.";
311
+ const code = error instanceof HarnessSessionError || error instanceof HarnessAdapterError ? error.code : "harness_turn_failed";
312
+ return this.#event(sessionId, turnId, "turn.failed", {
313
+ status: "failed",
314
+ final_output: { exit_reason: code },
315
+ error: { code, message, retryable: false },
316
+ });
317
+ }
318
+ async cancelTurn(sessionId, turnId) {
319
+ const session = this.#sessions.get(sessionId);
320
+ if (!session) {
321
+ throw new HarnessSessionError("session_not_found", `Session '${sessionId}' is not active.`);
322
+ }
323
+ const adapterEvents = await session.adapter.cancelTurn({ sessionId, turnId });
324
+ return adapterEvents.map((event) => this.#event(sessionId, event.turn_id ?? turnId, event.event_type, event.data));
325
+ }
326
+ async stopSession(sessionId, reason) {
327
+ const session = this.#sessions.get(sessionId);
328
+ if (!session) {
329
+ throw new HarnessSessionError("session_not_found", `Session '${sessionId}' is not active.`);
330
+ }
331
+ const events = [];
332
+ const adapterEvents = await session.adapter.stopSession({ sessionId, ...(reason ? { reason } : {}) });
333
+ events.push(...adapterEvents.map((event) => this.#event(sessionId, event.turn_id, event.event_type, event.data)));
334
+ await this.#closeMcpClients(session);
335
+ if (session.localCapabilityLease) {
336
+ this.#localCapabilities.revokeLease(session.localCapabilityLease.lease_id);
337
+ events.push(this.#event(sessionId, undefined, "local_capability.lease.revoked", {
338
+ lease_id: session.localCapabilityLease.lease_id,
339
+ workspace_id: session.workspaceId,
340
+ provider_instance_id: session.providerInstanceId,
341
+ status: "revoked",
342
+ }));
343
+ }
344
+ events.push(this.#event(sessionId, undefined, "session.exited", {
345
+ provider_instance_id: session.providerInstanceId,
346
+ reason: reason ?? "stopped",
347
+ }));
348
+ this.#sessions.delete(sessionId);
349
+ this.#turnIdsBySession.delete(sessionId);
350
+ await this.#recordAudit({
351
+ event: "session.exited",
352
+ session_id: sessionId,
353
+ provider_instance_id: session.providerInstanceId,
354
+ workspace_id: session.workspaceId,
355
+ data: {
356
+ reason: reason ?? "stopped",
357
+ },
358
+ });
359
+ return events;
360
+ }
361
+ #requireProvider(providerInstanceId, driverKind) {
362
+ const provider = this.#config.provider_instances.find((candidate) => candidate.id === providerInstanceId);
363
+ if (!provider) {
364
+ throw new HarnessSessionError("provider_not_found", `Provider '${providerInstanceId}' is not configured.`);
365
+ }
366
+ if (!provider.enabled) {
367
+ throw new HarnessSessionError("provider_disabled", `Provider '${providerInstanceId}' is disabled.`);
368
+ }
369
+ if (provider.driver_kind !== driverKind) {
370
+ throw new HarnessSessionError("provider_driver_mismatch", `Provider '${providerInstanceId}' is configured for '${provider.driver_kind}', not '${driverKind}'.`);
371
+ }
372
+ return provider;
373
+ }
374
+ async #assertWorkspaceAllowed(workspaceId, cwd) {
375
+ if (this.#config.workspaces.length === 0) {
376
+ throw new HarnessSessionError("workspace_not_configured", "Runner config has no workspaces configured; refusing to start a local harness session.");
377
+ }
378
+ const workspace = this.#config.workspaces.find((candidate) => candidate.id === workspaceId);
379
+ if (!workspace) {
380
+ throw new HarnessSessionError("workspace_not_allowed", `Workspace '${workspaceId}' is not configured by runner config.`);
381
+ }
382
+ const resolvedCwd = await realpathOrWorkspaceError(cwd);
383
+ const resolvedWorkspace = await realpathOrWorkspaceError(workspace.path);
384
+ const pathFromWorkspace = relative(resolvedWorkspace, resolvedCwd);
385
+ const allowed = pathFromWorkspace === "" || (!pathFromWorkspace.startsWith("..") && !isAbsolute(pathFromWorkspace));
386
+ if (!allowed) {
387
+ throw new HarnessSessionError("workspace_not_allowed", `Workspace '${cwd}' is not allowed by runner config.`);
388
+ }
389
+ }
390
+ #requireWorkspaceRoot(workspaceId) {
391
+ const workspace = this.#config.workspaces.find((candidate) => candidate.id === workspaceId);
392
+ if (!workspace) {
393
+ throw new LocalCapabilityPolicyError("local_capability_workspace_mismatch", `Workspace '${workspaceId}' is not configured by runner config.`);
394
+ }
395
+ return workspace.path;
396
+ }
397
+ async #attachMcpServers(payload, provider) {
398
+ const clients = [];
399
+ const adapterAttachments = [];
400
+ const discoveredTools = [];
401
+ try {
402
+ for (const attachment of payload.mcp_servers) {
403
+ const client = attachment.transport === "runner_stdio_profile"
404
+ ? await this.#createStdioProfileProxy(attachment, payload, provider)
405
+ : this.#mcpClientFactory({
406
+ attachment,
407
+ sessionId: payload.session_id,
408
+ hostId: this.#hostId,
409
+ providerInstanceId: payload.provider_instance_id,
410
+ workspaceId: payload.workspace_id,
411
+ driverKind: provider.driver_kind,
412
+ ...(this.#mcpProofSigner ? { proofSigner: this.#mcpProofSigner } : {}),
413
+ });
414
+ await client.connect();
415
+ clients.push(client);
416
+ if (client.listTools !== undefined) {
417
+ const tools = await client.listTools();
418
+ discoveredTools.push({ attachmentName: attachment.name, tools });
419
+ }
420
+ if (client.adapterAttachment) {
421
+ adapterAttachments.push(client.adapterAttachment);
422
+ }
423
+ else if (attachment.transport === "streamable_http") {
424
+ adapterAttachments.push(toAdapterMcpServer(attachment));
425
+ }
426
+ }
427
+ }
428
+ catch (error) {
429
+ await closeMcpClientsBestEffort(clients);
430
+ throw error;
431
+ }
432
+ return { clients, adapterAttachments, discoveredTools };
433
+ }
434
+ async #createStdioProfileProxy(attachment, payload, provider) {
435
+ const profile = this.#config.mcp_stdio_profiles.find((candidate) => candidate.id === attachment.profile_id);
436
+ if (!profile) {
437
+ throw new HarnessSessionError("mcp_stdio_profile_not_found", `Runner MCP profile '${attachment.profile_id}' is not configured on this host.`);
438
+ }
439
+ if (profile.provider_instance_ids.length > 0 && !profile.provider_instance_ids.includes(provider.id)) {
440
+ throw new HarnessSessionError("mcp_stdio_profile_provider_denied", `Runner MCP profile '${profile.id}' is not allowed for provider '${provider.id}'.`);
441
+ }
442
+ if (isAbsolute(profile.workspace_relative_cwd)) {
443
+ throw new HarnessSessionError("mcp_stdio_profile_cwd_invalid", `Runner MCP profile '${profile.id}' must use a workspace-relative cwd.`);
444
+ }
445
+ const workspaceRoot = await realpathOrWorkspaceError(this.#requireWorkspaceRoot(payload.workspace_id));
446
+ const profileCwd = await realpathOrWorkspaceError(resolve(workspaceRoot, profile.workspace_relative_cwd));
447
+ const relativeCwd = relative(workspaceRoot, profileCwd);
448
+ if (relativeCwd.startsWith("..") || isAbsolute(relativeCwd)) {
449
+ throw new HarnessSessionError("mcp_stdio_profile_cwd_invalid", `Runner MCP profile '${profile.id}' resolves outside workspace '${payload.workspace_id}'.`);
450
+ }
451
+ const allowedTools = intersectAllowedTools(profile.allowed_tools, attachment.allowed_tools);
452
+ const deniedTools = [...new Set([...profile.denied_tools, ...(attachment.denied_tools ?? [])])];
453
+ const upstream = new McpStdioProfileClient({
454
+ name: attachment.name,
455
+ command: profile.command,
456
+ args: profile.args,
457
+ cwd: profileCwd,
458
+ env: profile.env,
459
+ ...(allowedTools ? { allowedTools } : {}),
460
+ deniedTools,
461
+ });
462
+ return new McpProxyServer({
463
+ attachment: {
464
+ name: attachment.name,
465
+ ...(allowedTools ? { allowed_tools: allowedTools } : {}),
466
+ ...(deniedTools.length > 0 ? { denied_tools: deniedTools } : {}),
467
+ },
468
+ upstream,
469
+ });
470
+ }
471
+ async #closeMcpClients(session) {
472
+ await closeMcpClientsBestEffort(session.mcpClients);
473
+ }
474
+ #event(sessionId, turnId, eventType, data) {
475
+ const sequence = this.#stateStore.nextEventSequence(sessionId);
476
+ const payload = {
477
+ session_id: sessionId,
478
+ sequence,
479
+ event_type: eventType,
480
+ created_at: new Date().toISOString(),
481
+ data,
482
+ };
483
+ if (turnId) {
484
+ payload.turn_id = turnId;
485
+ }
486
+ this.#stateStore.appendEvent(payload);
487
+ return payload;
488
+ }
489
+ async #recordAudit(event) {
490
+ if (!this.#auditLogger) {
491
+ return;
492
+ }
493
+ await this.#auditLogger.record(event);
494
+ }
495
+ }
496
+ function defaultMcpClientFactory(request) {
497
+ if (!request.proofSigner) {
498
+ throw new HarnessSessionError("mcp_proof_signer_missing", `MCP attachment '${request.attachment.name}' requires a configured runner proof signer.`);
499
+ }
500
+ const upstream = new McpAttachmentClient(request.attachment, {
501
+ proofContext: {
502
+ session_id: request.sessionId,
503
+ host_id: request.hostId,
504
+ provider_instance_id: request.providerInstanceId,
505
+ workspace_id: request.workspaceId,
506
+ server_id: request.attachment.name,
507
+ },
508
+ proofSigner: request.proofSigner,
509
+ });
510
+ if (request.driverKind === "codex" || request.driverKind === "claude" || request.driverKind === "opencode") {
511
+ return new McpProxyServer({
512
+ attachment: request.attachment,
513
+ upstream,
514
+ });
515
+ }
516
+ return upstream;
517
+ }
518
+ function toAdapterMcpServer(attachment) {
519
+ return {
520
+ name: attachment.name,
521
+ transport: "streamable_http",
522
+ url: attachment.url,
523
+ headers: attachment.headers,
524
+ ...(attachment.allowed_tools ? { allowed_tools: attachment.allowed_tools } : {}),
525
+ ...(attachment.denied_tools ? { denied_tools: attachment.denied_tools } : {}),
526
+ };
527
+ }
528
+ function intersectAllowedTools(profileTools, requestedTools) {
529
+ if (!profileTools)
530
+ return requestedTools;
531
+ if (!requestedTools)
532
+ return profileTools;
533
+ const requested = new Set(requestedTools);
534
+ return profileTools.filter((tool) => requested.has(tool));
535
+ }
536
+ async function realpathOrWorkspaceError(path) {
537
+ try {
538
+ return await realpath(path);
539
+ }
540
+ catch (error) {
541
+ if (error instanceof Error) {
542
+ throw new HarnessSessionError("workspace_not_allowed", `Workspace path '${path}' could not be resolved: ${error.message}`);
543
+ }
544
+ throw error;
545
+ }
546
+ }
547
+ async function realpathOrLocalActionError(path) {
548
+ try {
549
+ return await realpath(path);
550
+ }
551
+ catch (error) {
552
+ if (error instanceof Error) {
553
+ throw new LocalCapabilityPolicyError("local_capability_path_denied", `Local action path '${path}' could not be resolved: ${error.message}`);
554
+ }
555
+ throw error;
556
+ }
557
+ }
558
+ function assertRequestLeaseMatchesActiveLease(payload, lease) {
559
+ if (payload.lease.lease_id !== lease.lease_id ||
560
+ payload.lease.run_id !== lease.run_id ||
561
+ payload.lease.hcp_session_id !== lease.hcp_session_id ||
562
+ payload.lease.execution_host_id !== lease.execution_host_id ||
563
+ payload.lease.provider_instance_id !== lease.provider_instance_id ||
564
+ payload.lease.workspace_id !== lease.workspace_id ||
565
+ payload.attribution.run_id !== lease.run_id) {
566
+ throw new LocalCapabilityPolicyError("local_capability_lease_missing", "Local action lease binding does not match the active session lease.");
567
+ }
568
+ }
569
+ async function assertSandboxMatchesSession(payload, session, workspaceRoot) {
570
+ if (payload.sandbox.mode !== session.startPayload.sandbox_mode) {
571
+ throw new LocalCapabilityPolicyError("local_capability_sandbox_read_only", `Local action sandbox mode '${payload.sandbox.mode}' does not match active session sandbox mode '${session.startPayload.sandbox_mode}'.`);
572
+ }
573
+ const resolvedRequestRoot = await realpathOrLocalActionError(payload.sandbox.workspace_root);
574
+ const resolvedWorkspaceRoot = await realpathOrLocalActionError(workspaceRoot);
575
+ if (resolvedRequestRoot !== resolvedWorkspaceRoot) {
576
+ throw new LocalCapabilityPolicyError("local_capability_sandbox_read_only", "Local action sandbox workspace root does not match the active session workspace.");
577
+ }
578
+ const resolvedRequestCwd = await realpathOrLocalActionError(payload.sandbox.cwd);
579
+ const resolvedSessionCwd = await realpathOrLocalActionError(session.cwd);
580
+ if (resolvedRequestCwd !== resolvedSessionCwd) {
581
+ throw new LocalCapabilityPolicyError("local_capability_sandbox_read_only", "Local action sandbox cwd does not match the active session cwd.");
582
+ }
583
+ }
584
+ async function closeMcpClientsBestEffort(clients) {
585
+ const errors = [];
586
+ for (const client of clients) {
587
+ try {
588
+ await client.close();
589
+ }
590
+ catch (error) {
591
+ errors.push(error instanceof Error ? error.message : "Unknown MCP close failure.");
592
+ }
593
+ }
594
+ if (errors.length > 0) {
595
+ throw new HarnessSessionError("mcp_attachment_close_failed", errors.join("; "));
596
+ }
597
+ }
598
+ async function stopAdapterSessionAfterStartFailure(adapter, sessionId, reason) {
599
+ await adapter.stopSession({ sessionId, reason });
600
+ }
601
+ async function cleanupAdapterSessionStartFailure(adapter, sessionId, mcpClients, reason, originalError) {
602
+ const cleanupErrors = [];
603
+ try {
604
+ await closeMcpClientsBestEffort(mcpClients);
605
+ }
606
+ catch (error) {
607
+ cleanupErrors.push(error instanceof Error ? error.message : "MCP attachment close failed.");
608
+ }
609
+ try {
610
+ await stopAdapterSessionAfterStartFailure(adapter, sessionId, reason);
611
+ }
612
+ catch (error) {
613
+ cleanupErrors.push(error instanceof Error ? `adapter stop failed: ${error.message}` : "adapter stop failed.");
614
+ }
615
+ if (cleanupErrors.length > 0) {
616
+ const originalMessage = originalError instanceof Error ? originalError.message : "Adapter session start failed.";
617
+ throw new HarnessSessionError("adapter_start_cleanup_failed", `${originalMessage}; cleanup failed: ${cleanupErrors.join("; ")}`);
618
+ }
619
+ }
620
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,34 @@
1
+ import type { HarnessModel, HarnessProviderSnapshot, HcpHostCapabilitiesUpdatedPayload } from "@harness-control/protocol";
2
+ import type { ProviderInstanceConfig, RunnerConfig } from "../config/index.js";
3
+ export type ProviderRegistrySnapshot = {
4
+ providers: HarnessProviderSnapshot[];
5
+ local_capabilities: HcpHostCapabilitiesUpdatedPayload["local_capabilities"];
6
+ workspaces: HcpHostCapabilitiesUpdatedPayload["workspaces"];
7
+ mcp_stdio_profiles: NonNullable<HcpHostCapabilitiesUpdatedPayload["mcp_stdio_profiles"]>;
8
+ };
9
+ export type ProviderDriverStatus = {
10
+ execution_capabilities?: HarnessProviderSnapshot["execution_capabilities"];
11
+ provider_instance_id?: string;
12
+ driver_kind: string;
13
+ installed: boolean;
14
+ available: boolean;
15
+ status?: HarnessProviderSnapshot["status"];
16
+ authStatus?: NonNullable<HarnessProviderSnapshot["auth"]>["status"];
17
+ version?: string;
18
+ message?: string;
19
+ models: HarnessModel[];
20
+ };
21
+ export type ProviderContinuationGroup = {
22
+ key: string;
23
+ driver_kind: string;
24
+ provider_instance_ids: string[];
25
+ };
26
+ export declare class ProviderInstanceRegistry {
27
+ #private;
28
+ constructor(config: RunnerConfig, drivers?: ProviderDriverStatus[]);
29
+ static fromConfig(config: RunnerConfig, drivers?: ProviderDriverStatus[]): ProviderInstanceRegistry;
30
+ snapshot(now?: Date): ProviderRegistrySnapshot;
31
+ computeContinuationGroups(): ProviderContinuationGroup[];
32
+ }
33
+ export declare function computeContinuationGroupKey(provider: ProviderInstanceConfig): string;
34
+ //# sourceMappingURL=provider-registry.d.ts.map