@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,40 @@
1
+ const REDACTED = "[redacted]";
2
+ const SENSITIVE_KEY_PATTERN = /(^|[-_.])(authorization|cookie|token|access[-_.]?token|refresh[-_.]?token|api[-_.]?key|secret|password|passwd|credential|session)([-_.]|$)/i;
3
+ const AUTH_VALUE_PATTERN = /\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]+/gi;
4
+ const ASSIGNMENT_VALUE_PATTERN = /\b(token|access_token|refresh_token|api_key|apikey|secret|password)=([^&\s]+)/gi;
5
+ export function redactHeaders(headers) {
6
+ if (!headers) {
7
+ return undefined;
8
+ }
9
+ const redacted = {};
10
+ for (const [key, value] of Object.entries(headers)) {
11
+ redacted[key] = isSensitiveKey(key) ? REDACTED : redactString(value);
12
+ }
13
+ return redacted;
14
+ }
15
+ export function redactValue(value) {
16
+ if (typeof value === "string") {
17
+ return redactString(value);
18
+ }
19
+ if (Array.isArray(value)) {
20
+ return value.map((item) => redactValue(item));
21
+ }
22
+ if (isPlainRecord(value)) {
23
+ const redacted = {};
24
+ for (const [key, nestedValue] of Object.entries(value)) {
25
+ redacted[key] = isSensitiveKey(key) ? REDACTED : redactValue(nestedValue);
26
+ }
27
+ return redacted;
28
+ }
29
+ return value;
30
+ }
31
+ function redactString(value) {
32
+ return value.replace(AUTH_VALUE_PATTERN, `$1 ${REDACTED}`).replace(ASSIGNMENT_VALUE_PATTERN, `$1=${REDACTED}`);
33
+ }
34
+ function isSensitiveKey(key) {
35
+ return SENSITIVE_KEY_PATTERN.test(key);
36
+ }
37
+ function isPlainRecord(value) {
38
+ return typeof value === "object" && value !== null && !Array.isArray(value);
39
+ }
40
+ //# sourceMappingURL=redaction.js.map
@@ -0,0 +1,38 @@
1
+ import { runnerCredentialSchema, type PairingCodeResponse } from "@harness-control/protocol";
2
+ import { z } from "zod";
3
+ import type { RunnerConfig } from "../config/index.js";
4
+ declare const runnerCredentialsFileSchema: z.ZodObject<{
5
+ version: z.ZodLiteral<1>;
6
+ credentials: z.ZodArray<z.ZodObject<{
7
+ credential_id: z.ZodString;
8
+ credential_secret: z.ZodString;
9
+ runner_id: z.ZodString;
10
+ host_id: z.ZodString;
11
+ control_plane_url: z.ZodString;
12
+ issued_at: z.ZodString;
13
+ mcp_proof_secret: z.ZodString;
14
+ }, z.core.$strict>>;
15
+ }, z.core.$strict>;
16
+ export type RunnerCredential = z.infer<typeof runnerCredentialSchema>;
17
+ export type RunnerCredentialsFile = z.infer<typeof runnerCredentialsFileSchema>;
18
+ export type ReferencePairingOptions = {
19
+ controlPlaneUrl: string;
20
+ runnerId: string;
21
+ hostId: string;
22
+ onPairingCode: (code: PairingCodeResponse) => void | Promise<void>;
23
+ signal?: AbortSignal;
24
+ };
25
+ export type ReferencePairingResult = {
26
+ controlPlaneUrl: string;
27
+ credential: RunnerCredential;
28
+ pairingCode: string;
29
+ pairingUrl?: string;
30
+ };
31
+ export declare function defaultCredentialsPath(): string;
32
+ export declare function pairWithReferenceControlPlane(options: ReferencePairingOptions): Promise<ReferencePairingResult>;
33
+ export declare function writeRunnerCredentials(path: string, credential: RunnerCredential): Promise<void>;
34
+ export declare function loadRunnerCredential(config: RunnerConfig): Promise<RunnerCredential | undefined>;
35
+ export declare function requestConnectionToken(config: RunnerConfig, credential: RunnerCredential): Promise<string>;
36
+ export declare function normalizeControlPlaneUrl(value: string): string;
37
+ export {};
38
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,180 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { readFileSync } from "node:fs";
3
+ import { createRequire } from "node:module";
4
+ import { setTimeout as delay } from "node:timers/promises";
5
+ import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
6
+ import { homedir } from "node:os";
7
+ import { dirname, join } from "node:path";
8
+ import { HCP_VERSION, runnerCredentialSchema, pairingCodeResponseSchema, pairingExchangeResponseSchema, connectionTokenResponseSchema } from "@harness-control/protocol";
9
+ import { z } from "zod";
10
+ const CREDENTIALS_FILE_VERSION = 1;
11
+ const DEFAULT_CONFIG_DIR = ".hcp-runner";
12
+ const DEFAULT_CREDENTIALS_FILE = "credentials.json";
13
+ const runnerCredentialsFileSchema = z
14
+ .object({
15
+ version: z.literal(CREDENTIALS_FILE_VERSION),
16
+ credentials: z.array(runnerCredentialSchema),
17
+ })
18
+ .strict();
19
+ export function defaultCredentialsPath() {
20
+ return join(homedir(), DEFAULT_CONFIG_DIR, DEFAULT_CREDENTIALS_FILE);
21
+ }
22
+ export async function pairWithReferenceControlPlane(options) {
23
+ const baseUrl = toHttpControlPlaneUrl(options.controlPlaneUrl);
24
+ const exchangeSecret = randomBytes(32).toString("base64url");
25
+ const codeResponse = await postJson(new URL("/pairing-codes", baseUrl), {
26
+ runner_id: options.runnerId,
27
+ host_id: options.hostId,
28
+ protocol_version: HCP_VERSION,
29
+ exchange_secret_hash: createHash("sha256").update(exchangeSecret).digest("hex"),
30
+ }, pairingCodeResponseSchema, options.signal);
31
+ await options.onPairingCode(codeResponse);
32
+ const deadline = Math.min(Date.parse(codeResponse.expires_at), Date.now() + 10 * 60_000);
33
+ while (Date.now() < deadline) {
34
+ const timeout = AbortSignal.timeout(Math.max(1, deadline - Date.now()));
35
+ const signal = options.signal ? AbortSignal.any([options.signal, timeout]) : timeout;
36
+ const exchange = await postJson(new URL("/pairing-exchange", baseUrl), {
37
+ request_id: codeResponse.request_id,
38
+ exchange_secret: exchangeSecret,
39
+ runner_id: options.runnerId,
40
+ host_id: options.hostId,
41
+ protocol_version: HCP_VERSION,
42
+ }, pairingExchangeResponseSchema, signal);
43
+ if (exchange.status === "approved") {
44
+ if (exchange.credential.runner_id !== options.runnerId || exchange.credential.host_id !== options.hostId
45
+ || normalizeControlPlaneUrl(exchange.control_plane_url) !== normalizeControlPlaneUrl(options.controlPlaneUrl)
46
+ || normalizeControlPlaneUrl(exchange.credential.control_plane_url) !== normalizeControlPlaneUrl(options.controlPlaneUrl)) {
47
+ throw new Error("Pairing credential does not match the requested runner, host, or control plane.");
48
+ }
49
+ return {
50
+ controlPlaneUrl: exchange.control_plane_url,
51
+ credential: exchange.credential,
52
+ pairingCode: codeResponse.pairing_code,
53
+ pairingUrl: codeResponse.pairing_url,
54
+ };
55
+ }
56
+ await delay(Math.min(codeResponse.poll_interval_seconds * 1000, Math.max(1, deadline - Date.now())), undefined, { signal: options.signal });
57
+ }
58
+ throw new Error("Pairing request expired. Start a new pairing request.");
59
+ }
60
+ export async function writeRunnerCredentials(path, credential) {
61
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
62
+ const existing = await readRunnerCredentialsFileIfPresent(path);
63
+ const credentials = existing.credentials.filter((candidate) => !(candidate.control_plane_url === credential.control_plane_url &&
64
+ candidate.runner_id === credential.runner_id &&
65
+ candidate.host_id === credential.host_id));
66
+ credentials.push(credential);
67
+ const file = {
68
+ version: CREDENTIALS_FILE_VERSION,
69
+ credentials,
70
+ };
71
+ const temporaryPath = `${path}.${randomBytes(12).toString("hex")}.tmp`;
72
+ try {
73
+ await writeFile(temporaryPath, `${JSON.stringify(file, null, 2)}\n`, { mode: 0o600, flag: "wx" });
74
+ await rename(temporaryPath, path);
75
+ }
76
+ finally {
77
+ await rm(temporaryPath, { force: true });
78
+ }
79
+ }
80
+ export async function loadRunnerCredential(config) {
81
+ const path = config.credentials_path;
82
+ if (!path) {
83
+ return undefined;
84
+ }
85
+ const file = await readRunnerCredentialsFileIfPresent(path);
86
+ const hostId = config.host_id ?? config.runner_id;
87
+ return file.credentials.find((credential) => credential.control_plane_url === config.control_plane_url &&
88
+ credential.runner_id === config.runner_id &&
89
+ credential.host_id === hostId);
90
+ }
91
+ export async function requestConnectionToken(config, credential) {
92
+ const response = await postJson(new URL("/runner-connection-token", toHttpControlPlaneUrl(config.control_plane_url)), {
93
+ credential_id: credential.credential_id,
94
+ credential_secret: credential.credential_secret,
95
+ runner_id: config.runner_id,
96
+ host_id: config.host_id ?? config.runner_id,
97
+ protocol_version: HCP_VERSION,
98
+ protocol_schema_sha256: createHash("sha256").update(readFileSync(createRequire(import.meta.url).resolve("@harness-control/protocol/schema.json"))).digest("hex"),
99
+ }, connectionTokenResponseSchema);
100
+ return response.connection_token;
101
+ }
102
+ export function normalizeControlPlaneUrl(value) {
103
+ const url = new URL(value);
104
+ if (url.username || url.password || url.search || url.hash) {
105
+ throw new Error("Control plane URL must not contain credentials, query parameters, or a fragment.");
106
+ }
107
+ if (url.protocol === "http:") {
108
+ url.protocol = "ws:";
109
+ }
110
+ else if (url.protocol === "https:") {
111
+ url.protocol = "wss:";
112
+ }
113
+ else if (url.protocol !== "ws:" && url.protocol !== "wss:") {
114
+ throw new Error("Control plane URL must use http, https, ws, or wss.");
115
+ }
116
+ if (url.protocol === "ws:" && !["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)) {
117
+ throw new Error("Remote control planes require HTTPS/WSS. Plain HTTP is allowed only on loopback.");
118
+ }
119
+ return url.toString();
120
+ }
121
+ function toHttpControlPlaneUrl(value) {
122
+ const url = new URL(normalizeControlPlaneUrl(value));
123
+ if (url.protocol === "ws:") {
124
+ url.protocol = "http:";
125
+ }
126
+ else if (url.protocol === "wss:") {
127
+ url.protocol = "https:";
128
+ }
129
+ else if (url.protocol !== "http:" && url.protocol !== "https:") {
130
+ throw new Error("Control plane URL must use http, https, ws, or wss.");
131
+ }
132
+ return url;
133
+ }
134
+ async function readRunnerCredentialsFileIfPresent(path) {
135
+ try {
136
+ const raw = await readFile(path, "utf8");
137
+ return runnerCredentialsFileSchema.parse(JSON.parse(raw));
138
+ }
139
+ catch (error) {
140
+ if (isFileMissingError(error)) {
141
+ return {
142
+ version: CREDENTIALS_FILE_VERSION,
143
+ credentials: [],
144
+ };
145
+ }
146
+ throw error;
147
+ }
148
+ }
149
+ async function postJson(url, body, schema, signal) {
150
+ const response = await fetch(url, {
151
+ method: "POST",
152
+ redirect: "error",
153
+ signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(15_000)]) : AbortSignal.timeout(15_000),
154
+ headers: {
155
+ "content-type": "application/json",
156
+ },
157
+ body: JSON.stringify(body),
158
+ });
159
+ const raw = await response.text();
160
+ const parsed = raw.length > 0 ? JSON.parse(raw) : {};
161
+ if (!response.ok) {
162
+ const message = errorMessageFromJson(parsed) ?? `${url.pathname} returned HTTP ${response.status}.`;
163
+ throw new Error(message);
164
+ }
165
+ return schema.parse(parsed);
166
+ }
167
+ function errorMessageFromJson(value) {
168
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
169
+ return undefined;
170
+ }
171
+ const message = value.error;
172
+ return typeof message === "string" && message.length > 0 ? message : undefined;
173
+ }
174
+ function isFileMissingError(error) {
175
+ return (typeof error === "object" &&
176
+ error !== null &&
177
+ "code" in error &&
178
+ error.code === "ENOENT");
179
+ }
180
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,76 @@
1
+ import { type HcpHarnessEventPayload, type HcpNackPayload, type HcpSessionSnapshotPayload, type HostRetainedEventRanges, type LocalActionErrorPayload, type LocalActionRequestPayload, type LocalActionResponsePayload } from "@harness-control/protocol";
2
+ export type PersistedCommandReceipt = {
3
+ payloadHash: string;
4
+ outcome: "ack";
5
+ settledAt: string;
6
+ snapshotPayload?: HcpSessionSnapshotPayload;
7
+ } | {
8
+ payloadHash: string;
9
+ outcome: "nack";
10
+ settledAt: string;
11
+ nackPayload: HcpNackPayload;
12
+ };
13
+ export type PersistedLocalActionReceipt = {
14
+ payloadHash: string;
15
+ requestPayload: LocalActionRequestPayload;
16
+ outcome: "response";
17
+ settledAt: string;
18
+ payload: LocalActionResponsePayload;
19
+ } | {
20
+ payloadHash: string;
21
+ requestPayload: LocalActionRequestPayload;
22
+ outcome: "error";
23
+ settledAt: string;
24
+ payload: LocalActionErrorPayload;
25
+ };
26
+ type RunnerStateData = {
27
+ version: 1;
28
+ events: Record<string, HcpHarnessEventPayload[]>;
29
+ commandReceipts: Record<string, PersistedCommandReceipt>;
30
+ localActionReceipts: Record<string, PersistedLocalActionReceipt>;
31
+ };
32
+ export type RunnerStateStoreOptions = {
33
+ eventRetentionPerSession?: number;
34
+ receiptRetentionMs?: number;
35
+ now?: () => Date;
36
+ };
37
+ export interface RunnerStateStore {
38
+ nextEventSequence(sessionId: string): number;
39
+ appendEvent(event: HcpHarnessEventPayload): void;
40
+ hasSessionEvents(sessionId: string): boolean;
41
+ retainedEventRanges(): HostRetainedEventRanges | undefined;
42
+ replayEventsAfter(sessionId: string, lastEventSequence: number): HcpHarnessEventPayload[] | undefined;
43
+ sessionSnapshot(commandId: string, sessionId: string): HcpSessionSnapshotPayload | undefined;
44
+ getCommandReceipt(commandId: string): PersistedCommandReceipt | undefined;
45
+ setCommandReceipt(commandId: string, receipt: PersistedCommandReceipt): void;
46
+ getLocalActionReceipt(requestId: string): PersistedLocalActionReceipt | undefined;
47
+ setLocalActionReceipt(requestId: string, receipt: PersistedLocalActionReceipt): void;
48
+ }
49
+ declare abstract class BaseRunnerStateStore implements RunnerStateStore {
50
+ #private;
51
+ protected data: RunnerStateData;
52
+ constructor(data: RunnerStateData, options: RunnerStateStoreOptions);
53
+ abstract persist(): void;
54
+ nextEventSequence(sessionId: string): number;
55
+ appendEvent(event: HcpHarnessEventPayload): void;
56
+ hasSessionEvents(sessionId: string): boolean;
57
+ retainedEventRanges(): HostRetainedEventRanges | undefined;
58
+ replayEventsAfter(sessionId: string, lastEventSequence: number): HcpHarnessEventPayload[] | undefined;
59
+ sessionSnapshot(commandId: string, sessionId: string): HcpSessionSnapshotPayload | undefined;
60
+ getCommandReceipt(commandId: string): PersistedCommandReceipt | undefined;
61
+ setCommandReceipt(commandId: string, receipt: PersistedCommandReceipt): void;
62
+ getLocalActionReceipt(requestId: string): PersistedLocalActionReceipt | undefined;
63
+ setLocalActionReceipt(requestId: string, receipt: PersistedLocalActionReceipt): void;
64
+ }
65
+ export declare class MemoryRunnerStateStore extends BaseRunnerStateStore {
66
+ constructor(options?: RunnerStateStoreOptions);
67
+ persist(): void;
68
+ }
69
+ export declare class JsonRunnerStateStore extends BaseRunnerStateStore {
70
+ #private;
71
+ constructor(path: string, options?: RunnerStateStoreOptions);
72
+ persist(): void;
73
+ }
74
+ export declare function defaultRunnerStatePath(runnerId: string): string;
75
+ export {};
76
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,242 @@
1
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+ import { HCP_PAYLOAD_MAX_ENCODED_BYTES, hcpCommandNackPayloadSchema, hcpHarnessEventPayloadSchema, hcpSessionSnapshotPayloadSchema, localActionErrorPayloadSchema, localActionRequestPayloadSchema, localActionResponsePayloadSchema, } from "@harness-control/protocol";
5
+ import { z } from "zod";
6
+ const DEFAULT_RECEIPT_RETENTION_MS = 24 * 60 * 60 * 1000;
7
+ const DEFAULT_EVENT_RETENTION_PER_SESSION = 512;
8
+ const persistedCommandReceiptSchema = z.discriminatedUnion("outcome", [
9
+ z
10
+ .object({
11
+ payloadHash: z.string().min(1),
12
+ outcome: z.literal("ack"),
13
+ settledAt: z.string().datetime({ offset: true }),
14
+ snapshotPayload: hcpSessionSnapshotPayloadSchema.optional(),
15
+ })
16
+ .strict(),
17
+ z
18
+ .object({
19
+ payloadHash: z.string().min(1),
20
+ outcome: z.literal("nack"),
21
+ settledAt: z.string().datetime({ offset: true }),
22
+ nackPayload: hcpCommandNackPayloadSchema,
23
+ })
24
+ .strict(),
25
+ ]);
26
+ const persistedLocalActionReceiptSchema = z.discriminatedUnion("outcome", [
27
+ z
28
+ .object({
29
+ payloadHash: z.string().min(1),
30
+ requestPayload: localActionRequestPayloadSchema,
31
+ outcome: z.literal("response"),
32
+ settledAt: z.string().datetime({ offset: true }),
33
+ payload: localActionResponsePayloadSchema,
34
+ })
35
+ .strict(),
36
+ z
37
+ .object({
38
+ payloadHash: z.string().min(1),
39
+ requestPayload: localActionRequestPayloadSchema,
40
+ outcome: z.literal("error"),
41
+ settledAt: z.string().datetime({ offset: true }),
42
+ payload: localActionErrorPayloadSchema,
43
+ })
44
+ .strict(),
45
+ ]);
46
+ const runnerStateDataSchema = z
47
+ .object({
48
+ version: z.literal(1),
49
+ events: z.record(z.string(), z.array(hcpHarnessEventPayloadSchema)),
50
+ commandReceipts: z.record(z.string(), persistedCommandReceiptSchema),
51
+ localActionReceipts: z.record(z.string(), persistedLocalActionReceiptSchema),
52
+ })
53
+ .strict();
54
+ class BaseRunnerStateStore {
55
+ #eventRetentionPerSession;
56
+ #receiptRetentionMs;
57
+ #now;
58
+ data;
59
+ constructor(data, options) {
60
+ this.data = data;
61
+ this.#eventRetentionPerSession = options.eventRetentionPerSession ?? DEFAULT_EVENT_RETENTION_PER_SESSION;
62
+ this.#receiptRetentionMs = options.receiptRetentionMs ?? DEFAULT_RECEIPT_RETENTION_MS;
63
+ this.#now = options.now ?? (() => new Date());
64
+ if (!Number.isInteger(this.#eventRetentionPerSession) || this.#eventRetentionPerSession < 1) {
65
+ throw new Error("eventRetentionPerSession must be a positive integer.");
66
+ }
67
+ if (!Number.isFinite(this.#receiptRetentionMs) || this.#receiptRetentionMs <= 0) {
68
+ throw new Error("receiptRetentionMs must be positive.");
69
+ }
70
+ this.#pruneExpiredReceipts();
71
+ }
72
+ nextEventSequence(sessionId) {
73
+ return (this.data.events[sessionId]?.at(-1)?.sequence ?? 0) + 1;
74
+ }
75
+ appendEvent(event) {
76
+ const expectedSequence = this.nextEventSequence(event.session_id);
77
+ if (event.sequence !== expectedSequence) {
78
+ throw new Error(`Event sequence ${event.sequence} for session '${event.session_id}' does not match expected sequence ${expectedSequence}.`);
79
+ }
80
+ const events = this.data.events[event.session_id] ?? [];
81
+ events.push(event);
82
+ while (events.length > this.#eventRetentionPerSession) {
83
+ events.shift();
84
+ }
85
+ this.data.events[event.session_id] = events;
86
+ this.persist();
87
+ }
88
+ hasSessionEvents(sessionId) {
89
+ return (this.data.events[sessionId]?.length ?? 0) > 0;
90
+ }
91
+ retainedEventRanges() {
92
+ const sessions = Object.entries(this.data.events)
93
+ .map(([sessionId, events]) => {
94
+ const firstEvent = events[0];
95
+ const lastEvent = events.at(-1);
96
+ return firstEvent && lastEvent
97
+ ? {
98
+ session_id: sessionId,
99
+ first_event_sequence: firstEvent.sequence,
100
+ last_event_sequence: lastEvent.sequence,
101
+ }
102
+ : undefined;
103
+ })
104
+ .filter((range) => range !== undefined)
105
+ .sort((left, right) => left.session_id.localeCompare(right.session_id));
106
+ return sessions.length > 0 ? { sessions } : undefined;
107
+ }
108
+ replayEventsAfter(sessionId, lastEventSequence) {
109
+ const events = this.data.events[sessionId];
110
+ const firstSequence = events?.[0]?.sequence;
111
+ const finalSequence = events?.at(-1)?.sequence;
112
+ if (!events ||
113
+ firstSequence === undefined ||
114
+ finalSequence === undefined ||
115
+ lastEventSequence < firstSequence - 1 ||
116
+ lastEventSequence > finalSequence) {
117
+ return undefined;
118
+ }
119
+ return events.filter((event) => event.sequence > lastEventSequence);
120
+ }
121
+ sessionSnapshot(commandId, sessionId) {
122
+ const events = this.data.events[sessionId];
123
+ if (!events || events.length === 0) {
124
+ return undefined;
125
+ }
126
+ const generatedAt = this.#now().toISOString();
127
+ const retainedStartsAtOne = events[0]?.sequence === 1;
128
+ const selectedEvents = [...events];
129
+ while (selectedEvents.length > 0) {
130
+ const firstEvent = selectedEvents[0];
131
+ const finalEvent = selectedEvents.at(-1);
132
+ const base = {
133
+ command_id: commandId,
134
+ session_id: sessionId,
135
+ generated_at: generatedAt,
136
+ from_sequence: firstEvent.sequence,
137
+ through_sequence: finalEvent.sequence,
138
+ events: [...selectedEvents],
139
+ tombstones: [],
140
+ };
141
+ const complete = retainedStartsAtOne && selectedEvents.length === events.length;
142
+ const snapshot = complete
143
+ ? { ...base, completeness: "complete", omission_semantics: "replace", from_sequence: 1 }
144
+ : {
145
+ ...base,
146
+ completeness: "partial",
147
+ omission_semantics: "preserve",
148
+ reason: retainedStartsAtOne ? "size_limit" : "retention_gap",
149
+ };
150
+ if (Buffer.byteLength(JSON.stringify(snapshot), "utf8") <= HCP_PAYLOAD_MAX_ENCODED_BYTES) {
151
+ return snapshot;
152
+ }
153
+ selectedEvents.shift();
154
+ }
155
+ return undefined;
156
+ }
157
+ getCommandReceipt(commandId) {
158
+ if (this.#pruneExpiredReceipts()) {
159
+ this.persist();
160
+ }
161
+ return this.data.commandReceipts[commandId];
162
+ }
163
+ setCommandReceipt(commandId, receipt) {
164
+ this.data.commandReceipts[commandId] = receipt;
165
+ this.#pruneExpiredReceipts();
166
+ this.persist();
167
+ }
168
+ getLocalActionReceipt(requestId) {
169
+ if (this.#pruneExpiredReceipts()) {
170
+ this.persist();
171
+ }
172
+ return this.data.localActionReceipts[requestId];
173
+ }
174
+ setLocalActionReceipt(requestId, receipt) {
175
+ this.data.localActionReceipts[requestId] = receipt;
176
+ this.#pruneExpiredReceipts();
177
+ this.persist();
178
+ }
179
+ #pruneExpiredReceipts() {
180
+ const cutoff = this.#now().getTime() - this.#receiptRetentionMs;
181
+ let changed = false;
182
+ for (const [commandId, receipt] of Object.entries(this.data.commandReceipts)) {
183
+ if (new Date(receipt.settledAt).getTime() < cutoff) {
184
+ delete this.data.commandReceipts[commandId];
185
+ changed = true;
186
+ }
187
+ }
188
+ for (const [requestId, receipt] of Object.entries(this.data.localActionReceipts)) {
189
+ if (new Date(receipt.settledAt).getTime() < cutoff) {
190
+ delete this.data.localActionReceipts[requestId];
191
+ changed = true;
192
+ }
193
+ }
194
+ return changed;
195
+ }
196
+ }
197
+ export class MemoryRunnerStateStore extends BaseRunnerStateStore {
198
+ constructor(options = {}) {
199
+ super(emptyRunnerState(), options);
200
+ }
201
+ persist() { }
202
+ }
203
+ export class JsonRunnerStateStore extends BaseRunnerStateStore {
204
+ #path;
205
+ constructor(path, options = {}) {
206
+ super(readRunnerState(path), options);
207
+ this.#path = path;
208
+ }
209
+ persist() {
210
+ mkdirSync(dirname(this.#path), { recursive: true, mode: 0o700 });
211
+ const temporaryPath = `${this.#path}.${process.pid}.tmp`;
212
+ writeFileSync(temporaryPath, `${JSON.stringify(this.data, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
213
+ renameSync(temporaryPath, this.#path);
214
+ }
215
+ }
216
+ export function defaultRunnerStatePath(runnerId) {
217
+ const safeRunnerId = runnerId.replace(/[^A-Za-z0-9_-]/g, "-");
218
+ return join(homedir(), ".hcp-runner", "state", `${safeRunnerId}.json`);
219
+ }
220
+ function emptyRunnerState() {
221
+ return {
222
+ version: 1,
223
+ events: {},
224
+ commandReceipts: {},
225
+ localActionReceipts: {},
226
+ };
227
+ }
228
+ function readRunnerState(path) {
229
+ let raw;
230
+ try {
231
+ raw = readFileSync(path, "utf8");
232
+ }
233
+ catch (error) {
234
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
235
+ return emptyRunnerState();
236
+ }
237
+ throw error;
238
+ }
239
+ const parsed = JSON.parse(raw);
240
+ return runnerStateDataSchema.parse(parsed);
241
+ }
242
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,13 @@
1
+ import type { HcpWorkspaceManagement, HcpWorkspacesRequestPayload, HcpWorkspacesResultPayload } from "@harness-control/protocol";
2
+ import { type RunnerConfig } from "../config/index.js";
3
+ import type { HarnessSessionManager } from "../harnesses/index.js";
4
+ export declare class WorkspaceManager {
5
+ private readonly config;
6
+ private readonly configPath;
7
+ private readonly sessions;
8
+ constructor(config: RunnerConfig, configPath: string | undefined, sessions: Pick<HarnessSessionManager, "updateWorkspaceConfiguration">);
9
+ snapshot(): HcpWorkspaceManagement;
10
+ execute(requestId: string, request: HcpWorkspacesRequestPayload): Promise<HcpWorkspacesResultPayload>;
11
+ private change;
12
+ }
13
+ //# sourceMappingURL=index.d.ts.map