harnery 0.22.0 → 0.23.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 (103) hide show
  1. package/README.md +1 -0
  2. package/dist/commands/workflow.d.ts.map +1 -1
  3. package/dist/commands/workflow.js +91 -2
  4. package/dist/core/work/state.d.ts +7 -0
  5. package/dist/core/work/state.d.ts.map +1 -1
  6. package/dist/core/work/state.js +112 -23
  7. package/dist/core/workflow/child-env.d.ts +10 -6
  8. package/dist/core/workflow/child-env.d.ts.map +1 -1
  9. package/dist/core/workflow/child-env.js +15 -7
  10. package/dist/core/workflow/durable-record.d.ts +7 -0
  11. package/dist/core/workflow/durable-record.d.ts.map +1 -0
  12. package/dist/core/workflow/durable-record.js +83 -0
  13. package/dist/core/workflow/engine.d.ts.map +1 -1
  14. package/dist/core/workflow/engine.js +208 -29
  15. package/dist/core/workflow/index.d.ts +2 -0
  16. package/dist/core/workflow/index.d.ts.map +1 -1
  17. package/dist/core/workflow/index.js +1 -0
  18. package/dist/core/workflow/proof.d.ts +4 -0
  19. package/dist/core/workflow/proof.d.ts.map +1 -1
  20. package/dist/core/workflow/proof.js +50 -15
  21. package/dist/core/workflow/run-state.d.ts +3 -0
  22. package/dist/core/workflow/run-state.d.ts.map +1 -1
  23. package/dist/core/workflow/run-state.js +25 -13
  24. package/dist/core/workflow/types.d.ts +10 -0
  25. package/dist/core/workflow/types.d.ts.map +1 -1
  26. package/dist/core/workflow/validate.d.ts +10 -1
  27. package/dist/core/workflow/validate.d.ts.map +1 -1
  28. package/dist/core/workflow/validate.js +75 -1
  29. package/dist/core/workflow/workspaces/attestation-error.d.ts +7 -0
  30. package/dist/core/workflow/workspaces/attestation-error.d.ts.map +1 -0
  31. package/dist/core/workflow/workspaces/attestation-error.js +25 -0
  32. package/dist/core/workflow/workspaces/cancellation.d.ts +8 -0
  33. package/dist/core/workflow/workspaces/cancellation.d.ts.map +1 -0
  34. package/dist/core/workflow/workspaces/cancellation.js +95 -0
  35. package/dist/core/workflow/workspaces/cleanup.d.ts +9 -0
  36. package/dist/core/workflow/workspaces/cleanup.d.ts.map +1 -0
  37. package/dist/core/workflow/workspaces/cleanup.js +223 -0
  38. package/dist/core/workflow/workspaces/execution.d.ts +25 -0
  39. package/dist/core/workflow/workspaces/execution.d.ts.map +1 -0
  40. package/dist/core/workflow/workspaces/execution.js +316 -0
  41. package/dist/core/workflow/workspaces/git.d.ts +25 -0
  42. package/dist/core/workflow/workspaces/git.d.ts.map +1 -0
  43. package/dist/core/workflow/workspaces/git.js +102 -0
  44. package/dist/core/workflow/workspaces/index.d.ts +15 -0
  45. package/dist/core/workflow/workspaces/index.d.ts.map +1 -0
  46. package/dist/core/workflow/workspaces/index.js +7 -0
  47. package/dist/core/workflow/workspaces/inspect.d.ts +85 -0
  48. package/dist/core/workflow/workspaces/inspect.d.ts.map +1 -0
  49. package/dist/core/workflow/workspaces/inspect.js +255 -0
  50. package/dist/core/workflow/workspaces/integration.d.ts +24 -0
  51. package/dist/core/workflow/workspaces/integration.d.ts.map +1 -0
  52. package/dist/core/workflow/workspaces/integration.js +617 -0
  53. package/dist/core/workflow/workspaces/leases.d.ts +37 -0
  54. package/dist/core/workflow/workspaces/leases.d.ts.map +1 -0
  55. package/dist/core/workflow/workspaces/leases.js +380 -0
  56. package/dist/core/workflow/workspaces/lifecycle.d.ts +43 -0
  57. package/dist/core/workflow/workspaces/lifecycle.d.ts.map +1 -0
  58. package/dist/core/workflow/workspaces/lifecycle.js +622 -0
  59. package/dist/core/workflow/workspaces/local-git.d.ts +8 -0
  60. package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -0
  61. package/dist/core/workflow/workspaces/local-git.js +1474 -0
  62. package/dist/core/workflow/workspaces/paths.d.ts +23 -0
  63. package/dist/core/workflow/workspaces/paths.d.ts.map +1 -0
  64. package/dist/core/workflow/workspaces/paths.js +191 -0
  65. package/dist/core/workflow/workspaces/recovery.d.ts +3 -0
  66. package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -0
  67. package/dist/core/workflow/workspaces/recovery.js +50 -0
  68. package/dist/core/workflow/workspaces/state.d.ts +60 -0
  69. package/dist/core/workflow/workspaces/state.d.ts.map +1 -0
  70. package/dist/core/workflow/workspaces/state.js +296 -0
  71. package/dist/core/workflow/workspaces/types.d.ts +445 -0
  72. package/dist/core/workflow/workspaces/types.d.ts.map +1 -0
  73. package/dist/core/workflow/workspaces/types.js +2 -0
  74. package/dist/core/workflow/workspaces/validate.d.ts +14 -0
  75. package/dist/core/workflow/workspaces/validate.d.ts.map +1 -0
  76. package/dist/core/workflow/workspaces/validate.js +476 -0
  77. package/package.json +5 -3
  78. package/src/commands/workflow.ts +110 -4
  79. package/src/core/work/state.ts +156 -28
  80. package/src/core/workflow/child-env.ts +18 -7
  81. package/src/core/workflow/durable-record.ts +94 -0
  82. package/src/core/workflow/engine.ts +230 -30
  83. package/src/core/workflow/index.ts +73 -0
  84. package/src/core/workflow/proof.ts +70 -22
  85. package/src/core/workflow/run-state.ts +46 -11
  86. package/src/core/workflow/types.ts +14 -0
  87. package/src/core/workflow/validate.ts +75 -1
  88. package/src/core/workflow/workspaces/attestation-error.ts +30 -0
  89. package/src/core/workflow/workspaces/cancellation.ts +137 -0
  90. package/src/core/workflow/workspaces/cleanup.ts +308 -0
  91. package/src/core/workflow/workspaces/execution.ts +412 -0
  92. package/src/core/workflow/workspaces/git.ts +125 -0
  93. package/src/core/workflow/workspaces/index.ts +85 -0
  94. package/src/core/workflow/workspaces/inspect.ts +421 -0
  95. package/src/core/workflow/workspaces/integration.ts +829 -0
  96. package/src/core/workflow/workspaces/leases.ts +495 -0
  97. package/src/core/workflow/workspaces/lifecycle.ts +792 -0
  98. package/src/core/workflow/workspaces/local-git.ts +1892 -0
  99. package/src/core/workflow/workspaces/paths.ts +247 -0
  100. package/src/core/workflow/workspaces/recovery.ts +56 -0
  101. package/src/core/workflow/workspaces/state.ts +481 -0
  102. package/src/core/workflow/workspaces/types.ts +517 -0
  103. package/src/core/workflow/workspaces/validate.ts +595 -0
@@ -0,0 +1,23 @@
1
+ import type { FilesystemIdentity, ValidatedFilesystemRoot } from "./types.js";
2
+ export type ValidatedRoot = ValidatedFilesystemRoot;
3
+ export interface OpenContainedDirectory {
4
+ path: string;
5
+ descriptor_path: string;
6
+ fd: number;
7
+ identity: FilesystemIdentity;
8
+ close(): void;
9
+ }
10
+ export declare function filesystemIdentity(path: string): FilesystemIdentity;
11
+ export declare function sameFilesystemIdentity(left: FilesystemIdentity, right: FilesystemIdentity): boolean;
12
+ export declare function validateConfiguredRoot(root: string): ValidatedRoot;
13
+ export declare function revalidateRoot(root: ValidatedRoot): void;
14
+ export declare function assertSafeRelativeSegment(value: string, field: string): void;
15
+ export declare function containsPath(root: string, candidate: string): boolean;
16
+ export declare function assertContainedExisting(root: ValidatedRoot, candidate: string, label: string): string;
17
+ export declare function createContainedDirectories(root: ValidatedRoot, segments: readonly string[]): string;
18
+ export declare function openContainedDirectory(root: ValidatedRoot, segments: readonly string[]): OpenContainedDirectory;
19
+ export declare function candidateUnderRoot(root: ValidatedRoot, segments: readonly string[]): string;
20
+ export declare function assertPathIdentity(path: string, expected: FilesystemIdentity, label: string): void;
21
+ export declare function descriptorBackedPathsSupported(): boolean;
22
+ export declare function descriptorPath(fd: number): string;
23
+ //# sourceMappingURL=paths.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../../../src/core/workflow/workspaces/paths.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAI9E,MAAM,MAAM,aAAa,GAAG,uBAAuB,CAAC;AAEpD,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,EAAE,MAAM,CAAC;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,KAAK,IAAI,IAAI,CAAC;CACf;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,kBAAkB,CAUnE;AAED,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,kBAAkB,EACxB,KAAK,EAAE,kBAAkB,GACxB,OAAO,CAIT;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,CAWlE;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI,CAQxD;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAiB5E;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAGrE;AAED,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,aAAa,EACnB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,GACZ,MAAM,CAYR;AAED,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,aAAa,EACnB,QAAQ,EAAE,SAAS,MAAM,EAAE,GAC1B,MAAM,CAOR;AAED,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,aAAa,EACnB,QAAQ,EAAE,SAAS,MAAM,EAAE,GAC1B,sBAAsB,CA2DxB;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,aAAa,EAAE,QAAQ,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAQ3F;AAED,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,kBAAkB,EAC5B,KAAK,EAAE,MAAM,GACZ,IAAI,CAON;AAwBD,wBAAgB,8BAA8B,IAAI,OAAO,CAOxD;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAKjD"}
@@ -0,0 +1,191 @@
1
+ import { closeSync, constants, existsSync, lstatSync, mkdirSync, openSync, realpathSync, statSync, } from "node:fs";
2
+ import { isAbsolute, join, relative, resolve } from "node:path";
3
+ const WINDOWS_AMBIGUOUS = /[<>:"|?*]|[ .]$/;
4
+ export function filesystemIdentity(path) {
5
+ const stat = statSync(path, { bigint: true });
6
+ if (stat.dev < 0n || stat.ino <= 0n) {
7
+ throw new Error(`filesystem identity is unavailable for ${path}`);
8
+ }
9
+ return {
10
+ platform: process.platform,
11
+ device: stat.dev.toString(),
12
+ inode: stat.ino.toString(),
13
+ };
14
+ }
15
+ export function sameFilesystemIdentity(left, right) {
16
+ return (left.platform === right.platform && left.device === right.device && left.inode === right.inode);
17
+ }
18
+ export function validateConfiguredRoot(root) {
19
+ assertAbsoluteUnambiguous(root, "writable root");
20
+ if (!existsSync(root))
21
+ throw new Error(`writable root does not exist: ${root}`);
22
+ if (lstatSync(root).isSymbolicLink())
23
+ throw new Error(`writable root must not be a symlink: ${root}`);
24
+ if (!lstatSync(root).isDirectory())
25
+ throw new Error(`writable root is not a directory: ${root}`);
26
+ const realpath = realpathSync(root);
27
+ if (realpath !== root) {
28
+ throw new Error(`writable root must not contain symlink components: ${root}`);
29
+ }
30
+ return { configured: root, realpath, identity: filesystemIdentity(realpath) };
31
+ }
32
+ export function revalidateRoot(root) {
33
+ const current = validateConfiguredRoot(root.configured);
34
+ if (current.realpath !== root.realpath ||
35
+ !sameFilesystemIdentity(current.identity, root.identity)) {
36
+ throw new Error(`writable root identity changed: ${root.configured}`);
37
+ }
38
+ }
39
+ export function assertSafeRelativeSegment(value, field) {
40
+ if (typeof value !== "string" || value.length === 0)
41
+ throw new Error(`${field} must be set`);
42
+ if (containsControl(value))
43
+ throw new Error(`${field} contains control characters`);
44
+ if (value !== value.normalize("NFC"))
45
+ throw new Error(`${field} has ambiguous Unicode normalization`);
46
+ if (isAbsolute(value) ||
47
+ value === "." ||
48
+ value === ".." ||
49
+ value.includes("/") ||
50
+ value.includes("\\")) {
51
+ throw new Error(`${field} must be one safe relative segment`);
52
+ }
53
+ if (process.platform === "win32" && WINDOWS_AMBIGUOUS.test(value)) {
54
+ throw new Error(`${field} is ambiguous on this platform`);
55
+ }
56
+ }
57
+ export function containsPath(root, candidate) {
58
+ const rel = relative(resolve(root), resolve(candidate));
59
+ return rel === "" || (rel !== ".." && !rel.startsWith(`..${separator()}`) && !isAbsolute(rel));
60
+ }
61
+ export function assertContainedExisting(root, candidate, label) {
62
+ revalidateRoot(root);
63
+ assertAbsoluteUnambiguous(candidate, label);
64
+ if (!existsSync(candidate))
65
+ throw new Error(`${label} does not exist: ${candidate}`);
66
+ if (lstatSync(candidate).isSymbolicLink())
67
+ throw new Error(`${label} must not be a symlink`);
68
+ const real = realpathSync(candidate);
69
+ if (!containsPath(root.realpath, real)) {
70
+ throw new Error(`${label} ${JSON.stringify(candidate)} escapes ${JSON.stringify(root.configured)}`);
71
+ }
72
+ return real;
73
+ }
74
+ export function createContainedDirectories(root, segments) {
75
+ const opened = openContainedDirectory(root, segments);
76
+ try {
77
+ return opened.path;
78
+ }
79
+ finally {
80
+ opened.close();
81
+ }
82
+ }
83
+ export function openContainedDirectory(root, segments) {
84
+ if (segments.length === 0)
85
+ throw new Error("contained directory requires at least one segment");
86
+ if (!descriptorBackedPathsSupported()) {
87
+ throw new Error("descriptor-backed workspace paths are unavailable");
88
+ }
89
+ const fds = [];
90
+ try {
91
+ revalidateRoot(root);
92
+ let cursor = root.realpath;
93
+ let parentFd = openSync(cursor, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
94
+ fds.push(parentFd);
95
+ if (!sameFilesystemIdentity(filesystemIdentity(descriptorPath(parentFd)), root.identity)) {
96
+ throw new Error("writable root descriptor identity changed");
97
+ }
98
+ for (const [index, segment] of segments.entries()) {
99
+ assertSafeRelativeSegment(segment, `path segment ${index + 1}`);
100
+ const candidate = join(descriptorPath(parentFd), segment);
101
+ if (existsSync(candidate)) {
102
+ const stat = lstatSync(candidate);
103
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
104
+ throw new Error(`contained path component is not an owned directory: ${join(cursor, segment)}`);
105
+ }
106
+ }
107
+ else {
108
+ mkdirSync(candidate, { mode: 0o700 });
109
+ }
110
+ const childFd = openSync(candidate, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
111
+ fds.push(childFd);
112
+ const real = realpathSync(descriptorPath(childFd));
113
+ if (!containsPath(root.realpath, real)) {
114
+ throw new Error(`contained path escaped root: ${join(cursor, segment)}`);
115
+ }
116
+ cursor = real;
117
+ parentFd = childFd;
118
+ }
119
+ revalidateRoot(root);
120
+ let closed = false;
121
+ return {
122
+ path: cursor,
123
+ descriptor_path: descriptorPath(parentFd),
124
+ fd: parentFd,
125
+ identity: filesystemIdentity(descriptorPath(parentFd)),
126
+ close() {
127
+ if (closed)
128
+ return;
129
+ for (const fd of [...fds].reverse())
130
+ closeSync(fd);
131
+ closed = true;
132
+ },
133
+ };
134
+ }
135
+ catch (error) {
136
+ for (const fd of [...fds].reverse())
137
+ closeSync(fd);
138
+ throw error;
139
+ }
140
+ }
141
+ export function candidateUnderRoot(root, segments) {
142
+ revalidateRoot(root);
143
+ for (const [index, segment] of segments.entries()) {
144
+ assertSafeRelativeSegment(segment, `path segment ${index + 1}`);
145
+ }
146
+ const candidate = join(root.realpath, ...segments);
147
+ if (!containsPath(root.realpath, candidate))
148
+ throw new Error("workspace candidate escapes root");
149
+ return candidate;
150
+ }
151
+ export function assertPathIdentity(path, expected, label) {
152
+ if (!existsSync(path) || lstatSync(path).isSymbolicLink()) {
153
+ throw new Error(`${label} is missing or is a symlink`);
154
+ }
155
+ if (!sameFilesystemIdentity(filesystemIdentity(path), expected)) {
156
+ throw new Error(`${label} identity changed`);
157
+ }
158
+ }
159
+ function assertAbsoluteUnambiguous(value, field) {
160
+ if (!value || containsControl(value))
161
+ throw new Error(`${field} contains control characters`);
162
+ if (!isAbsolute(value))
163
+ throw new Error(`${field} must be an absolute path`);
164
+ if (value !== value.normalize("NFC")) {
165
+ throw new Error(`${field} has ambiguous Unicode normalization`);
166
+ }
167
+ if (resolve(value) !== value) {
168
+ throw new Error(`${field} contains parent traversal or ambiguous path normalization`);
169
+ }
170
+ }
171
+ function separator() {
172
+ return process.platform === "win32" ? "\\" : "/";
173
+ }
174
+ function containsControl(value) {
175
+ return Array.from(value).some((character) => {
176
+ const code = character.charCodeAt(0);
177
+ return code < 32 || code === 127;
178
+ });
179
+ }
180
+ export function descriptorBackedPathsSupported() {
181
+ return (process.platform !== "win32" &&
182
+ constants.O_DIRECTORY !== undefined &&
183
+ constants.O_NOFOLLOW !== undefined &&
184
+ existsSync("/proc/self/fd"));
185
+ }
186
+ export function descriptorPath(fd) {
187
+ if (!descriptorBackedPathsSupported() || !Number.isSafeInteger(fd) || fd < 0) {
188
+ throw new Error("descriptor-backed workspace paths are unavailable");
189
+ }
190
+ return `/proc/self/fd/${fd}`;
191
+ }
@@ -0,0 +1,3 @@
1
+ export type WorkflowLiveness = "active" | "inactive" | "unknown";
2
+ export declare function readWorkflowLiveness(coordRoot: string, runId: string): WorkflowLiveness;
3
+ //# sourceMappingURL=recovery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recovery.d.ts","sourceRoot":"","sources":["../../../../src/core/workflow/workspaces/recovery.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,gBAAgB,GAAG,QAAQ,GAAG,UAAU,GAAG,SAAS,CAAC;AAEjE,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,gBAAgB,CAiDvF"}
@@ -0,0 +1,50 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { readWorkflowProof } from "../proof.js";
4
+ export function readWorkflowLiveness(coordRoot, runId) {
5
+ const root = resolve(coordRoot);
6
+ const proofPath = join(root, ".harnery", "workflows", runId, "proof.json");
7
+ if (existsSync(proofPath)) {
8
+ try {
9
+ const proof = readWorkflowProof(root, runId);
10
+ return ["succeeded", "failed"].includes(proof.run.status) ? "inactive" : "unknown";
11
+ }
12
+ catch {
13
+ return "unknown";
14
+ }
15
+ }
16
+ const journalPath = join(root, ".harnery", "workflows", runId, "journal.jsonl");
17
+ if (!existsSync(journalPath))
18
+ return "unknown";
19
+ let events;
20
+ try {
21
+ events = readFileSync(journalPath, "utf8")
22
+ .split("\n")
23
+ .filter(Boolean)
24
+ .map((line) => JSON.parse(line));
25
+ }
26
+ catch {
27
+ return "unknown";
28
+ }
29
+ if (events.some((item) => item.run_id !== runId || typeof item.event !== "string")) {
30
+ return "unknown";
31
+ }
32
+ let liveness;
33
+ for (const item of events) {
34
+ if (item.event === "run.start" || item.event === "run.resume")
35
+ liveness = "active";
36
+ if (item.event === "run.parked")
37
+ liveness = "inactive";
38
+ if (item.event === "run.end") {
39
+ if (typeof item.ok !== "boolean")
40
+ return "unknown";
41
+ liveness = "inactive";
42
+ }
43
+ if (item.event === "workspace.reattach.failed") {
44
+ if (item.status !== "blocked" && item.status !== "lost")
45
+ return "unknown";
46
+ liveness = "inactive";
47
+ }
48
+ }
49
+ return liveness ?? "unknown";
50
+ }
@@ -0,0 +1,60 @@
1
+ import type { GitRepositoryBinding, IntegrationApplyAttempt, IntegrationApplyReceipt, IntegrationAuthorization, IntegrationPlan, IntegrationReviewRecord, WorkspaceAllocationRequest, WorkspaceBinding, WorkspaceCancellationReceipt, WorkspaceCleanupAttempt, WorkspaceCleanupIntent, WorkspaceCleanupReceipt, WorkspaceCleanupResult } from "./types.js";
2
+ export interface WorkspaceClaim {
3
+ schema_version: 1;
4
+ provider_id: string;
5
+ provider_version: string;
6
+ binding_id: string;
7
+ workspace_id: string;
8
+ request: WorkspaceAllocationRequest;
9
+ request_sha256: string;
10
+ recovery_token: string;
11
+ created_at: string;
12
+ workspace_root: string;
13
+ active_root: string;
14
+ writable_root: WorkspaceBinding["writable_root"];
15
+ repository: {
16
+ source_root: GitRepositoryBinding["source_root"];
17
+ common_dir: GitRepositoryBinding["common_dir"];
18
+ base_commit: string;
19
+ target_commit: string;
20
+ target_ref: string;
21
+ workspace_ref: string;
22
+ workspace_branch: string;
23
+ };
24
+ }
25
+ export interface WorkspaceProviderEvent {
26
+ schema_version: 1;
27
+ seq: number;
28
+ previous_sha256: string | null;
29
+ record_sha256: string;
30
+ event: string;
31
+ recorded_at: string;
32
+ request_sha256: string;
33
+ binding_id: string;
34
+ workspace_id: string;
35
+ data: Record<string, unknown>;
36
+ }
37
+ export declare function workflowRunDir(coordRoot: string, runId: string): string;
38
+ export declare function workspaceRequestPath(coordRoot: string, runId: string): string;
39
+ export declare function workspaceClaimPath(coordRoot: string, providerId: string, bindingId: string): string;
40
+ export declare function workspaceEventsPath(coordRoot: string, providerId: string, bindingId: string): string;
41
+ export declare function workspaceBindingPath(coordRoot: string, providerId: string, bindingId: string): string;
42
+ export declare function workspaceLockPath(coordRoot: string, providerId: string, bindingId: string): string;
43
+ export declare function writeWorkspaceRequest(coordRoot: string, request: WorkspaceAllocationRequest): string;
44
+ export declare function readWorkspaceRequest(coordRoot: string, runId: string): WorkspaceAllocationRequest | undefined;
45
+ export declare function writeWorkspaceClaim(coordRoot: string, claim: WorkspaceClaim): boolean;
46
+ export declare function readWorkspaceClaim(coordRoot: string, providerId: string, bindingId: string): WorkspaceClaim | undefined;
47
+ export declare function writeWorkspaceBinding(coordRoot: string, binding: WorkspaceBinding): boolean;
48
+ export declare function readWorkspaceBinding(coordRoot: string, providerId: string, bindingId: string): WorkspaceBinding | undefined;
49
+ export declare function appendWorkspaceEvent(coordRoot: string, claim: WorkspaceClaim, event: string, data?: Record<string, unknown>): WorkspaceProviderEvent;
50
+ export declare function readWorkspaceEvents(coordRoot: string, providerId: string, bindingId: string): WorkspaceProviderEvent[];
51
+ export declare function appendWorkflowJournalEvent(coordRoot: string, runId: string, event: string, data: Record<string, unknown>): void;
52
+ export type WorkflowSupplement = WorkspaceBinding | IntegrationReviewRecord | IntegrationPlan | IntegrationAuthorization | IntegrationApplyReceipt | WorkspaceCleanupResult | WorkspaceCancellationReceipt | WorkspaceCleanupIntent | WorkspaceCleanupReceipt;
53
+ export declare function appendCleanupAttempt(coordRoot: string, runId: string, input: Omit<WorkspaceCleanupAttempt, "seq" | "previous_sha256" | "record_sha256">): WorkspaceCleanupAttempt;
54
+ export declare function appendIntegrationAttempt(coordRoot: string, runId: string, input: Omit<IntegrationApplyAttempt, "seq" | "previous_sha256" | "record_sha256">): IntegrationApplyAttempt;
55
+ export declare function readIntegrationAttempts(coordRoot: string, runId: string): IntegrationApplyAttempt[];
56
+ export declare function readCleanupAttempts(coordRoot: string, runId: string): WorkspaceCleanupAttempt[];
57
+ export declare function writeWorkflowSupplement(coordRoot: string, runId: string, relativePath: string, value: WorkflowSupplement): string;
58
+ export declare function readWorkflowSupplement<T>(coordRoot: string, runId: string, relativePath: string): T | undefined;
59
+ export { canonicalJson, fileSha256, readJsonRecord, stableDigest, writeImmutableJson, } from "../durable-record.js";
60
+ //# sourceMappingURL=state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../../../src/core/workflow/workspaces/state.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,296 @@
1
+ import { chmodSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, statSync, writeFileSync, } from "node:fs";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { canonicalJson, fsyncParentDirectory, readJsonRecord, stableDigest, writeImmutableJson, } from "../durable-record.js";
4
+ const JSON_LIMIT = 512 * 1024;
5
+ const EVENT_LIMIT = 32 * 1024;
6
+ const ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/;
7
+ export function workflowRunDir(coordRoot, runId) {
8
+ assertId(runId, "run id");
9
+ return join(resolve(coordRoot), ".harnery", "workflows", runId);
10
+ }
11
+ export function workspaceRequestPath(coordRoot, runId) {
12
+ return join(workflowRunDir(coordRoot, runId), "workspace-request.json");
13
+ }
14
+ export function workspaceClaimPath(coordRoot, providerId, bindingId) {
15
+ assertId(providerId, "provider id");
16
+ assertId(bindingId, "binding id");
17
+ return join(resolve(coordRoot), ".harnery", "workspaces", providerId, bindingId, "claim.json");
18
+ }
19
+ export function workspaceEventsPath(coordRoot, providerId, bindingId) {
20
+ return join(dirname(workspaceClaimPath(coordRoot, providerId, bindingId)), "events.jsonl");
21
+ }
22
+ export function workspaceBindingPath(coordRoot, providerId, bindingId) {
23
+ return join(dirname(workspaceClaimPath(coordRoot, providerId, bindingId)), "binding.json");
24
+ }
25
+ export function workspaceLockPath(coordRoot, providerId, bindingId) {
26
+ return join(dirname(workspaceClaimPath(coordRoot, providerId, bindingId)), "operation.lease");
27
+ }
28
+ export function writeWorkspaceRequest(coordRoot, request) {
29
+ const path = workspaceRequestPath(coordRoot, request.run_id);
30
+ writeImmutableJson(path, request);
31
+ return path;
32
+ }
33
+ export function readWorkspaceRequest(coordRoot, runId) {
34
+ const path = workspaceRequestPath(coordRoot, runId);
35
+ return existsSync(path)
36
+ ? readJsonRecord(path, "workspace request")
37
+ : undefined;
38
+ }
39
+ export function writeWorkspaceClaim(coordRoot, claim) {
40
+ const path = workspaceClaimPath(coordRoot, claim.provider_id, claim.binding_id);
41
+ try {
42
+ return writeImmutableJson(path, claim);
43
+ }
44
+ catch (error) {
45
+ // A competing allocation may have won the exclusive link with its one
46
+ // random recovery token. The caller must read and validate that claim.
47
+ if (existsSync(path) && error.message.includes("immutable record already exists")) {
48
+ return false;
49
+ }
50
+ throw error;
51
+ }
52
+ }
53
+ export function readWorkspaceClaim(coordRoot, providerId, bindingId) {
54
+ const path = workspaceClaimPath(coordRoot, providerId, bindingId);
55
+ return existsSync(path) ? readJsonRecord(path, "workspace claim") : undefined;
56
+ }
57
+ export function writeWorkspaceBinding(coordRoot, binding) {
58
+ return writeImmutableJson(workspaceBindingPath(coordRoot, binding.provider.id, binding.binding_id), binding);
59
+ }
60
+ export function readWorkspaceBinding(coordRoot, providerId, bindingId) {
61
+ const path = workspaceBindingPath(coordRoot, providerId, bindingId);
62
+ return existsSync(path) ? readJsonRecord(path, "workspace binding") : undefined;
63
+ }
64
+ export function appendWorkspaceEvent(coordRoot, claim, event, data = {}) {
65
+ if (!event || event.length > 100 || Buffer.byteLength(JSON.stringify(data)) > EVENT_LIMIT / 2) {
66
+ throw new Error("workspace provider event is invalid or too large");
67
+ }
68
+ const path = workspaceEventsPath(coordRoot, claim.provider_id, claim.binding_id);
69
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
70
+ const existed = existsSync(path);
71
+ const prior = readWorkspaceEvents(coordRoot, claim.provider_id, claim.binding_id);
72
+ const basis = {
73
+ schema_version: 1,
74
+ seq: prior.length + 1,
75
+ previous_sha256: prior.at(-1)?.record_sha256 ?? null,
76
+ event,
77
+ recorded_at: new Date().toISOString(),
78
+ request_sha256: claim.request_sha256,
79
+ binding_id: claim.binding_id,
80
+ workspace_id: claim.workspace_id,
81
+ data,
82
+ };
83
+ const record = {
84
+ ...basis,
85
+ record_sha256: stableDigest(basis),
86
+ };
87
+ const line = `${canonicalJson(record)}\n`;
88
+ if (Buffer.byteLength(line) > EVENT_LIMIT)
89
+ throw new Error("workspace provider event is too large");
90
+ const fd = openSync(path, "a", 0o600);
91
+ try {
92
+ writeFileSync(fd, line, "utf8");
93
+ fsyncSync(fd);
94
+ }
95
+ finally {
96
+ closeSync(fd);
97
+ }
98
+ chmodSync(path, 0o600);
99
+ if (!existed)
100
+ fsyncParentDirectory(path);
101
+ return record;
102
+ }
103
+ export function readWorkspaceEvents(coordRoot, providerId, bindingId) {
104
+ const path = workspaceEventsPath(coordRoot, providerId, bindingId);
105
+ if (!existsSync(path))
106
+ return [];
107
+ if (statSync(path).size > JSON_LIMIT * 4)
108
+ throw new Error("workspace event journal is too large");
109
+ const claim = readWorkspaceClaim(coordRoot, providerId, bindingId);
110
+ if (!claim)
111
+ throw new Error("workspace event authority claim is missing");
112
+ const records = readFileSync(path, "utf8")
113
+ .split("\n")
114
+ .filter(Boolean)
115
+ .map((line, index) => {
116
+ if (Buffer.byteLength(line) > EVENT_LIMIT) {
117
+ throw new Error(`workspace event ${index + 1} is too large`);
118
+ }
119
+ let record;
120
+ try {
121
+ record = JSON.parse(line);
122
+ }
123
+ catch (error) {
124
+ throw new Error(`cannot parse workspace event ${index + 1}: ${error.message}`);
125
+ }
126
+ const { record_sha256, ...basis } = record;
127
+ const previous = index === 0 ? null : recordsDigestPlaceholder();
128
+ if (record.schema_version !== 1 ||
129
+ record.seq !== index + 1 ||
130
+ record.binding_id !== bindingId ||
131
+ record.workspace_id !== claim.workspace_id ||
132
+ record.request_sha256 !== claim.request_sha256 ||
133
+ record_sha256 !== stableDigest(basis) ||
134
+ (index === 0 && record.previous_sha256 !== null)) {
135
+ throw new Error(`workspace event ${index + 1} is corrupt`);
136
+ }
137
+ void previous;
138
+ return record;
139
+ });
140
+ for (let index = 1; index < records.length; index++) {
141
+ if (records[index].previous_sha256 !== records[index - 1].record_sha256) {
142
+ throw new Error(`workspace event ${index + 1} breaks the digest chain`);
143
+ }
144
+ }
145
+ return records;
146
+ }
147
+ // Keeps the JSON parser above free of a closure over a partially initialized array.
148
+ function recordsDigestPlaceholder() {
149
+ return null;
150
+ }
151
+ export function appendWorkflowJournalEvent(coordRoot, runId, event, data) {
152
+ const path = join(workflowRunDir(coordRoot, runId), "journal.jsonl");
153
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
154
+ const existed = existsSync(path);
155
+ const line = `${JSON.stringify({
156
+ schema_version: 1,
157
+ run_id: runId,
158
+ ts: new Date().toISOString(),
159
+ event,
160
+ ...data,
161
+ })}\n`;
162
+ if (Buffer.byteLength(line) > EVENT_LIMIT)
163
+ throw new Error("workflow journal event is too large");
164
+ const fd = openSync(path, "a", 0o600);
165
+ try {
166
+ writeFileSync(fd, line, "utf8");
167
+ fsyncSync(fd);
168
+ }
169
+ finally {
170
+ closeSync(fd);
171
+ }
172
+ if (!existed)
173
+ fsyncParentDirectory(path);
174
+ }
175
+ export function appendCleanupAttempt(coordRoot, runId, input) {
176
+ const path = join(workflowRunDir(coordRoot, runId), "cleanup", "attempts.jsonl");
177
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
178
+ const existed = existsSync(path);
179
+ const prior = readCleanupAttempts(coordRoot, runId);
180
+ const basis = {
181
+ ...input,
182
+ seq: prior.length + 1,
183
+ previous_sha256: prior.at(-1)?.record_sha256 ?? null,
184
+ };
185
+ const attempt = {
186
+ ...basis,
187
+ record_sha256: stableDigest(basis),
188
+ };
189
+ const line = `${canonicalJson(attempt)}\n`;
190
+ if (Buffer.byteLength(line) > EVENT_LIMIT) {
191
+ throw new Error("workspace cleanup attempt is too large");
192
+ }
193
+ const fd = openSync(path, "a", 0o600);
194
+ try {
195
+ writeFileSync(fd, line, "utf8");
196
+ fsyncSync(fd);
197
+ }
198
+ finally {
199
+ closeSync(fd);
200
+ }
201
+ chmodSync(path, 0o600);
202
+ if (!existed)
203
+ fsyncParentDirectory(path);
204
+ return attempt;
205
+ }
206
+ export function appendIntegrationAttempt(coordRoot, runId, input) {
207
+ const path = join(workflowRunDir(coordRoot, runId), "integration", "attempts.jsonl");
208
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
209
+ const existed = existsSync(path);
210
+ const prior = readIntegrationAttempts(coordRoot, runId);
211
+ const basis = {
212
+ ...input,
213
+ seq: prior.length + 1,
214
+ previous_sha256: prior.at(-1)?.record_sha256 ?? null,
215
+ };
216
+ const attempt = {
217
+ ...basis,
218
+ record_sha256: stableDigest(basis),
219
+ };
220
+ appendChainedRecord(path, attempt, "integration attempt");
221
+ if (!existed)
222
+ fsyncParentDirectory(path);
223
+ return attempt;
224
+ }
225
+ export function readIntegrationAttempts(coordRoot, runId) {
226
+ return readChainedRecords(join(workflowRunDir(coordRoot, runId), "integration", "attempts.jsonl"), "integration attempt");
227
+ }
228
+ export function readCleanupAttempts(coordRoot, runId) {
229
+ return readChainedRecords(join(workflowRunDir(coordRoot, runId), "cleanup", "attempts.jsonl"), "workspace cleanup attempt");
230
+ }
231
+ function appendChainedRecord(path, record, label) {
232
+ const line = `${canonicalJson(record)}\n`;
233
+ if (Buffer.byteLength(line) > EVENT_LIMIT) {
234
+ throw new Error(`${label} is too large`);
235
+ }
236
+ const fd = openSync(path, "a", 0o600);
237
+ try {
238
+ writeFileSync(fd, line, "utf8");
239
+ fsyncSync(fd);
240
+ }
241
+ finally {
242
+ closeSync(fd);
243
+ }
244
+ chmodSync(path, 0o600);
245
+ }
246
+ function readChainedRecords(path, label) {
247
+ if (!existsSync(path))
248
+ return [];
249
+ if (statSync(path).size > JSON_LIMIT * 4) {
250
+ throw new Error(`${label} journal is too large`);
251
+ }
252
+ const records = readFileSync(path, "utf8")
253
+ .split("\n")
254
+ .filter(Boolean)
255
+ .map((line, index) => {
256
+ let record;
257
+ try {
258
+ record = JSON.parse(line);
259
+ }
260
+ catch (error) {
261
+ throw new Error(`cannot parse ${label} ${index + 1}: ${error.message}`);
262
+ }
263
+ const { record_sha256, ...basis } = record;
264
+ if (record.schema_version !== 1 ||
265
+ record.seq !== index + 1 ||
266
+ record_sha256 !== stableDigest(basis) ||
267
+ (index === 0 && record.previous_sha256 !== null)) {
268
+ throw new Error(`${label} ${index + 1} is corrupt`);
269
+ }
270
+ return record;
271
+ });
272
+ for (let index = 1; index < records.length; index++) {
273
+ if (records[index].previous_sha256 !== records[index - 1].record_sha256) {
274
+ throw new Error(`${label} ${index + 1} breaks the digest chain`);
275
+ }
276
+ }
277
+ return records;
278
+ }
279
+ export function writeWorkflowSupplement(coordRoot, runId, relativePath, value) {
280
+ if (relativePath.startsWith("/") ||
281
+ relativePath.split(/[\\/]/).some((part) => !part || part === "." || part === "..")) {
282
+ throw new Error("workflow supplement path is unsafe");
283
+ }
284
+ const path = join(workflowRunDir(coordRoot, runId), relativePath);
285
+ writeImmutableJson(path, value);
286
+ return path;
287
+ }
288
+ export function readWorkflowSupplement(coordRoot, runId, relativePath) {
289
+ const path = join(workflowRunDir(coordRoot, runId), relativePath);
290
+ return existsSync(path) ? readJsonRecord(path, relativePath) : undefined;
291
+ }
292
+ function assertId(value, field) {
293
+ if (!ID.test(value))
294
+ throw new Error(`${field} is invalid`);
295
+ }
296
+ export { canonicalJson, fileSha256, readJsonRecord, stableDigest, writeImmutableJson, } from "../durable-record.js";