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.
- package/README.md +1 -0
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +91 -2
- package/dist/core/work/state.d.ts +7 -0
- package/dist/core/work/state.d.ts.map +1 -1
- package/dist/core/work/state.js +112 -23
- package/dist/core/workflow/child-env.d.ts +10 -6
- package/dist/core/workflow/child-env.d.ts.map +1 -1
- package/dist/core/workflow/child-env.js +15 -7
- package/dist/core/workflow/durable-record.d.ts +7 -0
- package/dist/core/workflow/durable-record.d.ts.map +1 -0
- package/dist/core/workflow/durable-record.js +83 -0
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +208 -29
- package/dist/core/workflow/index.d.ts +2 -0
- package/dist/core/workflow/index.d.ts.map +1 -1
- package/dist/core/workflow/index.js +1 -0
- package/dist/core/workflow/proof.d.ts +4 -0
- package/dist/core/workflow/proof.d.ts.map +1 -1
- package/dist/core/workflow/proof.js +50 -15
- package/dist/core/workflow/run-state.d.ts +3 -0
- package/dist/core/workflow/run-state.d.ts.map +1 -1
- package/dist/core/workflow/run-state.js +25 -13
- package/dist/core/workflow/types.d.ts +10 -0
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/validate.d.ts +10 -1
- package/dist/core/workflow/validate.d.ts.map +1 -1
- package/dist/core/workflow/validate.js +75 -1
- package/dist/core/workflow/workspaces/attestation-error.d.ts +7 -0
- package/dist/core/workflow/workspaces/attestation-error.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/attestation-error.js +25 -0
- package/dist/core/workflow/workspaces/cancellation.d.ts +8 -0
- package/dist/core/workflow/workspaces/cancellation.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/cancellation.js +95 -0
- package/dist/core/workflow/workspaces/cleanup.d.ts +9 -0
- package/dist/core/workflow/workspaces/cleanup.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/cleanup.js +223 -0
- package/dist/core/workflow/workspaces/execution.d.ts +25 -0
- package/dist/core/workflow/workspaces/execution.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/execution.js +316 -0
- package/dist/core/workflow/workspaces/git.d.ts +25 -0
- package/dist/core/workflow/workspaces/git.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/git.js +102 -0
- package/dist/core/workflow/workspaces/index.d.ts +15 -0
- package/dist/core/workflow/workspaces/index.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/index.js +7 -0
- package/dist/core/workflow/workspaces/inspect.d.ts +85 -0
- package/dist/core/workflow/workspaces/inspect.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/inspect.js +255 -0
- package/dist/core/workflow/workspaces/integration.d.ts +24 -0
- package/dist/core/workflow/workspaces/integration.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/integration.js +617 -0
- package/dist/core/workflow/workspaces/leases.d.ts +37 -0
- package/dist/core/workflow/workspaces/leases.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/leases.js +380 -0
- package/dist/core/workflow/workspaces/lifecycle.d.ts +43 -0
- package/dist/core/workflow/workspaces/lifecycle.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/lifecycle.js +622 -0
- package/dist/core/workflow/workspaces/local-git.d.ts +8 -0
- package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/local-git.js +1474 -0
- package/dist/core/workflow/workspaces/paths.d.ts +23 -0
- package/dist/core/workflow/workspaces/paths.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/paths.js +191 -0
- package/dist/core/workflow/workspaces/recovery.d.ts +3 -0
- package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/recovery.js +50 -0
- package/dist/core/workflow/workspaces/state.d.ts +60 -0
- package/dist/core/workflow/workspaces/state.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/state.js +296 -0
- package/dist/core/workflow/workspaces/types.d.ts +445 -0
- package/dist/core/workflow/workspaces/types.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/types.js +2 -0
- package/dist/core/workflow/workspaces/validate.d.ts +14 -0
- package/dist/core/workflow/workspaces/validate.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/validate.js +476 -0
- package/package.json +5 -3
- package/src/commands/workflow.ts +110 -4
- package/src/core/work/state.ts +156 -28
- package/src/core/workflow/child-env.ts +18 -7
- package/src/core/workflow/durable-record.ts +94 -0
- package/src/core/workflow/engine.ts +230 -30
- package/src/core/workflow/index.ts +73 -0
- package/src/core/workflow/proof.ts +70 -22
- package/src/core/workflow/run-state.ts +46 -11
- package/src/core/workflow/types.ts +14 -0
- package/src/core/workflow/validate.ts +75 -1
- package/src/core/workflow/workspaces/attestation-error.ts +30 -0
- package/src/core/workflow/workspaces/cancellation.ts +137 -0
- package/src/core/workflow/workspaces/cleanup.ts +308 -0
- package/src/core/workflow/workspaces/execution.ts +412 -0
- package/src/core/workflow/workspaces/git.ts +125 -0
- package/src/core/workflow/workspaces/index.ts +85 -0
- package/src/core/workflow/workspaces/inspect.ts +421 -0
- package/src/core/workflow/workspaces/integration.ts +829 -0
- package/src/core/workflow/workspaces/leases.ts +495 -0
- package/src/core/workflow/workspaces/lifecycle.ts +792 -0
- package/src/core/workflow/workspaces/local-git.ts +1892 -0
- package/src/core/workflow/workspaces/paths.ts +247 -0
- package/src/core/workflow/workspaces/recovery.ts +56 -0
- package/src/core/workflow/workspaces/state.ts +481 -0
- package/src/core/workflow/workspaces/types.ts +517 -0
- package/src/core/workflow/workspaces/validate.ts +595 -0
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { closeSync, existsSync, fsyncSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, rmdirSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { hostname } from "node:os";
|
|
4
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
5
|
+
const MAX_LEASE_BYTES = 8 * 1024;
|
|
6
|
+
const SAFE_COMPONENT = /^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/;
|
|
7
|
+
/**
|
|
8
|
+
* Acquires a crash-safe lease whose active name is always a hard link to one
|
|
9
|
+
* immutable owner file. Stale takeover is serialized by an atomic recovery
|
|
10
|
+
* directory; only its claimant may pin and replace the observed `current`.
|
|
11
|
+
*/
|
|
12
|
+
export function acquireNoClobberLease(input) {
|
|
13
|
+
validateInput(input);
|
|
14
|
+
const leaseDir = resolve(input.path);
|
|
15
|
+
ensureLeaseDirectory(leaseDir);
|
|
16
|
+
const now = input.now ?? Date.now;
|
|
17
|
+
const created = now();
|
|
18
|
+
const ownerId = randomUUID();
|
|
19
|
+
const ownerFile = `owner-${ownerId}.json`;
|
|
20
|
+
const owner = {
|
|
21
|
+
schema_version: 1,
|
|
22
|
+
owner_id: ownerId,
|
|
23
|
+
owner_file: ownerFile,
|
|
24
|
+
scope: input.scope,
|
|
25
|
+
authority_sha256: input.authoritySha256,
|
|
26
|
+
metadata: input.metadata ? { ...input.metadata } : undefined,
|
|
27
|
+
pid: input.pid ?? process.pid,
|
|
28
|
+
host: input.host ?? hostname(),
|
|
29
|
+
created_at: new Date(created).toISOString(),
|
|
30
|
+
expires_at: new Date(created + input.staleAfterMs).toISOString(),
|
|
31
|
+
};
|
|
32
|
+
const ownerPath = join(leaseDir, ownerFile);
|
|
33
|
+
const ownerBytes = `${JSON.stringify(owner)}\n`;
|
|
34
|
+
writeExclusive(ownerPath, ownerBytes);
|
|
35
|
+
fsyncDirectory(leaseDir);
|
|
36
|
+
const currentPath = join(leaseDir, "current");
|
|
37
|
+
const recoveryPath = join(leaseDir, "recovery");
|
|
38
|
+
let recoveryClaimed = false;
|
|
39
|
+
try {
|
|
40
|
+
if (existsSync(recoveryPath)) {
|
|
41
|
+
reclaimAbandonedRecovery(leaseDir, recoveryPath, input, now());
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
linkSync(ownerPath, currentPath);
|
|
45
|
+
fsyncDirectory(leaseDir);
|
|
46
|
+
assertExactOwner(currentPath, ownerPath, ownerBytes, owner);
|
|
47
|
+
if (existsSync(recoveryPath)) {
|
|
48
|
+
unlinkExactCurrent(currentPath, ownerPath);
|
|
49
|
+
throw new Error(`lease ${input.scope} recovery is already in progress`);
|
|
50
|
+
}
|
|
51
|
+
return leaseHandle(leaseDir, ownerPath, ownerBytes, owner);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
if (error.code !== "EEXIST")
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
const observed = readOwner(currentPath);
|
|
58
|
+
if (leaseIsLive(observed.owner, now(), input.staleAfterMs)) {
|
|
59
|
+
throw new Error(`lease ${input.scope} is held by a live or unexpired owner`);
|
|
60
|
+
}
|
|
61
|
+
if (observed.owner.scope !== input.scope ||
|
|
62
|
+
(input.validateStaleOwner !== undefined && !input.validateStaleOwner(observed.owner))) {
|
|
63
|
+
throw new Error(`stale lease ${input.scope} has mismatched authority`);
|
|
64
|
+
}
|
|
65
|
+
createRecoveryClaim(leaseDir, recoveryPath, owner, observed);
|
|
66
|
+
recoveryClaimed = true;
|
|
67
|
+
input.onRecoveryStep?.("claim_created");
|
|
68
|
+
const pinPath = join(recoveryPath, "stale-pin");
|
|
69
|
+
linkSync(currentPath, pinPath);
|
|
70
|
+
assertSameInodeAndBytes(currentPath, pinPath, observed.bytes);
|
|
71
|
+
fsyncDirectory(recoveryPath);
|
|
72
|
+
input.onRecoveryStep?.("stale_pinned");
|
|
73
|
+
assertSameInodeAndBytes(currentPath, pinPath, observed.bytes);
|
|
74
|
+
unlinkSync(currentPath);
|
|
75
|
+
fsyncDirectory(leaseDir);
|
|
76
|
+
input.onRecoveryStep?.("current_removed");
|
|
77
|
+
linkSync(ownerPath, currentPath);
|
|
78
|
+
assertExactOwner(currentPath, ownerPath, ownerBytes, owner);
|
|
79
|
+
fsyncDirectory(leaseDir);
|
|
80
|
+
input.onRecoveryStep?.("new_current_linked");
|
|
81
|
+
removePinnedOwner(leaseDir, pinPath, observed.owner);
|
|
82
|
+
unlinkSync(pinPath);
|
|
83
|
+
unlinkSync(join(recoveryPath, "claim.json"));
|
|
84
|
+
rmdirSync(recoveryPath);
|
|
85
|
+
fsyncDirectory(leaseDir);
|
|
86
|
+
return {
|
|
87
|
+
...leaseHandle(leaseDir, ownerPath, ownerBytes, owner),
|
|
88
|
+
recovered_owner: observed.owner,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
// Once the recovery directory exists, leave every boundary intact. A
|
|
93
|
+
// later owner can quarantine that exact abandoned claim and reconcile it.
|
|
94
|
+
if (!recoveryClaimed)
|
|
95
|
+
safeUnlink(ownerPath);
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function validateInput(input) {
|
|
100
|
+
if (!SAFE_COMPONENT.test(basename(resolve(input.path))) ||
|
|
101
|
+
!SAFE_COMPONENT.test(input.scope) ||
|
|
102
|
+
!/^[a-f0-9]{64}$/.test(input.authoritySha256) ||
|
|
103
|
+
!Number.isSafeInteger(input.staleAfterMs) ||
|
|
104
|
+
input.staleAfterMs < 1) {
|
|
105
|
+
throw new Error("no-clobber lease input is invalid");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function ensureLeaseDirectory(leaseDir) {
|
|
109
|
+
mkdirSync(dirname(leaseDir), { recursive: true, mode: 0o700 });
|
|
110
|
+
if (existsSync(leaseDir) && !lstatSync(leaseDir).isDirectory()) {
|
|
111
|
+
throw new Error(`lease path is not a directory: ${leaseDir}`);
|
|
112
|
+
}
|
|
113
|
+
if (!existsSync(leaseDir)) {
|
|
114
|
+
mkdirSync(leaseDir, { mode: 0o700 });
|
|
115
|
+
fsyncDirectory(dirname(leaseDir));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function createRecoveryClaim(leaseDir, recoveryPath, claimant, observed) {
|
|
119
|
+
const temporary = join(leaseDir, `recovery-claim-${claimant.owner_id}`);
|
|
120
|
+
mkdirSync(temporary, { mode: 0o700 });
|
|
121
|
+
const claim = {
|
|
122
|
+
schema_version: 1,
|
|
123
|
+
claimant,
|
|
124
|
+
observed_owner: observed.owner,
|
|
125
|
+
observed_sha256: digestBytes(observed.bytes),
|
|
126
|
+
created_at: claimant.created_at,
|
|
127
|
+
};
|
|
128
|
+
try {
|
|
129
|
+
writeExclusive(join(temporary, "claim.json"), `${JSON.stringify(claim)}\n`);
|
|
130
|
+
fsyncDirectory(temporary);
|
|
131
|
+
try {
|
|
132
|
+
renameSync(temporary, recoveryPath);
|
|
133
|
+
fsyncDirectory(leaseDir);
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
if (error.code === "EEXIST" ||
|
|
137
|
+
error.code === "ENOTEMPTY") {
|
|
138
|
+
unlinkSync(join(temporary, "claim.json"));
|
|
139
|
+
rmdirSync(temporary);
|
|
140
|
+
throw new Error(`lease ${claimant.scope} recovery is already in progress`);
|
|
141
|
+
}
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
if (existsSync(join(temporary, "claim.json")))
|
|
147
|
+
unlinkSync(join(temporary, "claim.json"));
|
|
148
|
+
if (existsSync(temporary))
|
|
149
|
+
rmdirSync(temporary);
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function reclaimAbandonedRecovery(leaseDir, recoveryPath, input, now) {
|
|
154
|
+
const claim = readRecoveryClaim(recoveryPath);
|
|
155
|
+
if (leaseIsLive(claim.claimant, now, input.staleAfterMs)) {
|
|
156
|
+
throw new Error(`lease ${input.scope} recovery is already in progress`);
|
|
157
|
+
}
|
|
158
|
+
if (claim.claimant.scope !== input.scope ||
|
|
159
|
+
claim.observed_owner.scope !== input.scope ||
|
|
160
|
+
(input.validateStaleOwner !== undefined && !input.validateStaleOwner(claim.observed_owner))) {
|
|
161
|
+
throw new Error(`stale lease ${input.scope} recovery has mismatched authority`);
|
|
162
|
+
}
|
|
163
|
+
const recoveryPin = join(recoveryPath, "stale-pin");
|
|
164
|
+
if (existsSync(recoveryPin)) {
|
|
165
|
+
const pinned = readOwner(recoveryPin);
|
|
166
|
+
if (pinned.owner.owner_id !== claim.observed_owner.owner_id ||
|
|
167
|
+
digestBytes(pinned.bytes) !== claim.observed_sha256) {
|
|
168
|
+
throw new Error(`stale lease ${input.scope} recovery pin is corrupt`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const quarantine = join(leaseDir, `quarantine-${randomUUID()}`);
|
|
172
|
+
try {
|
|
173
|
+
renameSync(recoveryPath, quarantine);
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
if (error.code === "ENOENT") {
|
|
177
|
+
throw new Error(`lease ${input.scope} recovery raced with another contender`);
|
|
178
|
+
}
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
fsyncDirectory(leaseDir);
|
|
182
|
+
const pinPath = join(quarantine, "stale-pin");
|
|
183
|
+
if (existsSync(pinPath)) {
|
|
184
|
+
const pinned = readOwner(pinPath);
|
|
185
|
+
if (pinned.owner.owner_id !== claim.observed_owner.owner_id ||
|
|
186
|
+
digestBytes(pinned.bytes) !== claim.observed_sha256) {
|
|
187
|
+
throw new Error(`stale lease ${input.scope} recovery pin is corrupt`);
|
|
188
|
+
}
|
|
189
|
+
removePinnedOwner(leaseDir, pinPath, pinned.owner);
|
|
190
|
+
unlinkSync(pinPath);
|
|
191
|
+
}
|
|
192
|
+
const claimantPath = join(leaseDir, claim.claimant.owner_file);
|
|
193
|
+
const currentPath = join(leaseDir, "current");
|
|
194
|
+
if (existsSync(claimantPath) &&
|
|
195
|
+
(!existsSync(currentPath) || !sameInode(claimantPath, currentPath))) {
|
|
196
|
+
unlinkSync(claimantPath);
|
|
197
|
+
}
|
|
198
|
+
unlinkSync(join(quarantine, "claim.json"));
|
|
199
|
+
rmdirSync(quarantine);
|
|
200
|
+
fsyncDirectory(leaseDir);
|
|
201
|
+
}
|
|
202
|
+
function readRecoveryClaim(recoveryPath) {
|
|
203
|
+
const path = join(recoveryPath, "claim.json");
|
|
204
|
+
const size = lstatSync(path).size;
|
|
205
|
+
if (size <= 0 || size > MAX_LEASE_BYTES * 2) {
|
|
206
|
+
throw new Error("lease recovery claim bytes are invalid");
|
|
207
|
+
}
|
|
208
|
+
let claim;
|
|
209
|
+
try {
|
|
210
|
+
claim = JSON.parse(readFileSync(path, "utf8"));
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
throw new Error(`lease recovery claim is corrupt: ${error.message}`);
|
|
214
|
+
}
|
|
215
|
+
if (claim.schema_version !== 1 ||
|
|
216
|
+
!validOwner(claim.claimant) ||
|
|
217
|
+
!validOwner(claim.observed_owner) ||
|
|
218
|
+
!/^[a-f0-9]{64}$/.test(claim.observed_sha256) ||
|
|
219
|
+
typeof claim.created_at !== "string" ||
|
|
220
|
+
claim.created_at !== claim.claimant.created_at) {
|
|
221
|
+
throw new Error("lease recovery claim has an unsupported schema");
|
|
222
|
+
}
|
|
223
|
+
return claim;
|
|
224
|
+
}
|
|
225
|
+
function leaseHandle(leaseDir, ownerPath, ownerBytes, owner) {
|
|
226
|
+
let released = false;
|
|
227
|
+
return {
|
|
228
|
+
owner,
|
|
229
|
+
release() {
|
|
230
|
+
if (released)
|
|
231
|
+
return;
|
|
232
|
+
const currentPath = join(leaseDir, "current");
|
|
233
|
+
assertExactOwner(currentPath, ownerPath, ownerBytes, owner);
|
|
234
|
+
unlinkSync(currentPath);
|
|
235
|
+
unlinkSync(ownerPath);
|
|
236
|
+
fsyncDirectory(leaseDir);
|
|
237
|
+
released = true;
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
function writeExclusive(path, bytes) {
|
|
242
|
+
const fd = openSync(path, "wx", 0o600);
|
|
243
|
+
try {
|
|
244
|
+
writeFileSync(fd, bytes, "utf8");
|
|
245
|
+
fsyncSync(fd);
|
|
246
|
+
}
|
|
247
|
+
finally {
|
|
248
|
+
closeSync(fd);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
function readOwner(path) {
|
|
252
|
+
let size;
|
|
253
|
+
try {
|
|
254
|
+
const stat = lstatSync(path);
|
|
255
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
256
|
+
throw new Error("lease owner is not a plain file");
|
|
257
|
+
size = stat.size;
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
throw new Error(`lease owner cannot be inspected: ${error.message}`);
|
|
261
|
+
}
|
|
262
|
+
if (size <= 0 || size > MAX_LEASE_BYTES)
|
|
263
|
+
throw new Error("lease owner bytes are invalid");
|
|
264
|
+
const bytes = readFileSync(path, "utf8");
|
|
265
|
+
let value;
|
|
266
|
+
try {
|
|
267
|
+
value = JSON.parse(bytes);
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
throw new Error(`lease owner is corrupt: ${error.message}`);
|
|
271
|
+
}
|
|
272
|
+
if (!validOwner(value))
|
|
273
|
+
throw new Error("lease owner has an unsupported schema");
|
|
274
|
+
return { owner: value, bytes };
|
|
275
|
+
}
|
|
276
|
+
function validOwner(value) {
|
|
277
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
278
|
+
return false;
|
|
279
|
+
const owner = value;
|
|
280
|
+
return (owner.schema_version === 1 &&
|
|
281
|
+
typeof owner.owner_id === "string" &&
|
|
282
|
+
typeof owner.owner_file === "string" &&
|
|
283
|
+
owner.owner_file === `owner-${owner.owner_id}.json` &&
|
|
284
|
+
SAFE_COMPONENT.test(owner.owner_file) &&
|
|
285
|
+
typeof owner.scope === "string" &&
|
|
286
|
+
SAFE_COMPONENT.test(owner.scope) &&
|
|
287
|
+
typeof owner.authority_sha256 === "string" &&
|
|
288
|
+
/^[a-f0-9]{64}$/.test(owner.authority_sha256) &&
|
|
289
|
+
(owner.metadata === undefined ||
|
|
290
|
+
(isStringRecord(owner.metadata) && Object.keys(owner.metadata).length <= 20)) &&
|
|
291
|
+
Number.isSafeInteger(owner.pid) &&
|
|
292
|
+
owner.pid > 0 &&
|
|
293
|
+
typeof owner.host === "string" &&
|
|
294
|
+
owner.host.length > 0 &&
|
|
295
|
+
typeof owner.created_at === "string" &&
|
|
296
|
+
typeof owner.expires_at === "string" &&
|
|
297
|
+
Number.isFinite(Date.parse(owner.created_at)) &&
|
|
298
|
+
Number.isFinite(Date.parse(owner.expires_at)));
|
|
299
|
+
}
|
|
300
|
+
function isStringRecord(value) {
|
|
301
|
+
return (typeof value === "object" &&
|
|
302
|
+
value !== null &&
|
|
303
|
+
!Array.isArray(value) &&
|
|
304
|
+
Object.entries(value).every(([key, item]) => SAFE_COMPONENT.test(key) && typeof item === "string" && item.length <= 1_000));
|
|
305
|
+
}
|
|
306
|
+
function leaseIsLive(owner, now, staleAfterMs) {
|
|
307
|
+
const created = Date.parse(owner.created_at);
|
|
308
|
+
const expires = Date.parse(owner.expires_at);
|
|
309
|
+
if (!Number.isFinite(created) ||
|
|
310
|
+
!Number.isFinite(expires) ||
|
|
311
|
+
expires !== created + staleAfterMs ||
|
|
312
|
+
now < created ||
|
|
313
|
+
now < expires) {
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
if (owner.host !== hostname())
|
|
317
|
+
return false;
|
|
318
|
+
try {
|
|
319
|
+
process.kill(owner.pid, 0);
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
catch (error) {
|
|
323
|
+
return error.code === "EPERM";
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
function assertExactOwner(currentPath, ownerPath, expectedBytes, owner) {
|
|
327
|
+
const current = readOwner(currentPath);
|
|
328
|
+
if (current.bytes !== expectedBytes ||
|
|
329
|
+
current.owner.owner_id !== owner.owner_id ||
|
|
330
|
+
!sameInode(currentPath, ownerPath)) {
|
|
331
|
+
throw new Error(`lease ${owner.scope} ownership changed`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
function unlinkExactCurrent(currentPath, ownerPath) {
|
|
335
|
+
if (existsSync(currentPath) && sameInode(currentPath, ownerPath))
|
|
336
|
+
unlinkSync(currentPath);
|
|
337
|
+
}
|
|
338
|
+
function removePinnedOwner(leaseDir, pinPath, owner) {
|
|
339
|
+
const staleOwnerPath = join(leaseDir, owner.owner_file);
|
|
340
|
+
if (basename(staleOwnerPath) === owner.owner_file &&
|
|
341
|
+
existsSync(staleOwnerPath) &&
|
|
342
|
+
sameInode(staleOwnerPath, pinPath)) {
|
|
343
|
+
unlinkSync(staleOwnerPath);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
function assertSameInodeAndBytes(left, right, expectedBytes) {
|
|
347
|
+
if (!sameInode(left, right))
|
|
348
|
+
throw new Error("lease recovery owner inode changed");
|
|
349
|
+
const leftBytes = readFileSync(left, "utf8");
|
|
350
|
+
const rightBytes = readFileSync(right, "utf8");
|
|
351
|
+
if (leftBytes !== expectedBytes || rightBytes !== expectedBytes) {
|
|
352
|
+
throw new Error("lease recovery owner bytes changed");
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
function sameInode(left, right) {
|
|
356
|
+
const a = statSync(left, { bigint: true });
|
|
357
|
+
const b = statSync(right, { bigint: true });
|
|
358
|
+
return a.dev === b.dev && a.ino === b.ino;
|
|
359
|
+
}
|
|
360
|
+
function digestBytes(bytes) {
|
|
361
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
362
|
+
}
|
|
363
|
+
function fsyncDirectory(path) {
|
|
364
|
+
const fd = openSync(path, "r");
|
|
365
|
+
try {
|
|
366
|
+
fsyncSync(fd);
|
|
367
|
+
}
|
|
368
|
+
finally {
|
|
369
|
+
closeSync(fd);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
function safeUnlink(path) {
|
|
373
|
+
try {
|
|
374
|
+
unlinkSync(path);
|
|
375
|
+
}
|
|
376
|
+
catch (error) {
|
|
377
|
+
if (error.code !== "ENOENT")
|
|
378
|
+
throw error;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { WorkEvent } from "../../work/state.js";
|
|
2
|
+
import type { WorkflowRunManifest } from "../run-state.js";
|
|
3
|
+
import type { WorkflowProof } from "../types.js";
|
|
4
|
+
import type { WorkspaceProviderEvent } from "./state.js";
|
|
5
|
+
import type { IntegrationApplyAttempt, IntegrationApplyReceipt, IntegrationAuthorization, IntegrationPlan, WorkspaceBinding, WorkspaceCancellationReceipt, WorkspaceCancellationResult, WorkspaceCleanupAttempt, WorkspaceCleanupIntent, WorkspaceCleanupReceipt, WorkspaceIntegrationState, WorkspaceLifecycleState, WorkspaceProofOutcome, WorkspaceResourceState } from "./types.js";
|
|
6
|
+
export interface WorkspaceLifecycleProjectionInput {
|
|
7
|
+
allocation_unsupported?: boolean;
|
|
8
|
+
binding?: WorkspaceBinding;
|
|
9
|
+
provider_events?: readonly WorkspaceProviderEvent[];
|
|
10
|
+
manifest?: WorkflowRunManifest;
|
|
11
|
+
workflow_journal?: readonly {
|
|
12
|
+
event: string;
|
|
13
|
+
ok?: boolean;
|
|
14
|
+
status?: "blocked" | "lost" | WorkspaceCancellationResult["status"];
|
|
15
|
+
work_event_seq?: number;
|
|
16
|
+
work_event_sha256?: string;
|
|
17
|
+
}[];
|
|
18
|
+
proof?: WorkflowProof;
|
|
19
|
+
proof_sha256?: string;
|
|
20
|
+
integration_plan?: IntegrationPlan;
|
|
21
|
+
integration_authorization?: IntegrationAuthorization;
|
|
22
|
+
integration_attempts?: readonly IntegrationApplyAttempt[];
|
|
23
|
+
integration_receipt?: IntegrationApplyReceipt;
|
|
24
|
+
cleanup_intent?: WorkspaceCleanupIntent;
|
|
25
|
+
cleanup_attempts?: readonly WorkspaceCleanupAttempt[];
|
|
26
|
+
cleanup_receipt?: WorkspaceCleanupReceipt;
|
|
27
|
+
work_events?: readonly WorkEvent[];
|
|
28
|
+
cancellation_outcome?: WorkspaceCancellationResult;
|
|
29
|
+
cancellation_receipt?: WorkspaceCancellationReceipt;
|
|
30
|
+
}
|
|
31
|
+
export interface WorkspaceLifecycleProjection {
|
|
32
|
+
state: WorkspaceLifecycleState;
|
|
33
|
+
workflow_outcome: WorkspaceProofOutcome | null;
|
|
34
|
+
resource_state: WorkspaceResourceState | null;
|
|
35
|
+
integration_state: WorkspaceIntegrationState;
|
|
36
|
+
provider_event_chain_sha256: string | null;
|
|
37
|
+
cancellation: "none" | "host_requested" | "confirmed";
|
|
38
|
+
}
|
|
39
|
+
export declare function isWorkspaceLifecycleState(value: unknown): value is WorkspaceLifecycleState;
|
|
40
|
+
export declare function assertWorkspaceLifecycleTransition(from: WorkspaceLifecycleState, to: WorkspaceLifecycleState): void;
|
|
41
|
+
export declare function deriveWorkspaceLifecycle(input: WorkspaceLifecycleProjectionInput): WorkspaceLifecycleProjection;
|
|
42
|
+
export declare function validateProviderEventChain(events: readonly WorkspaceProviderEvent[], binding?: WorkspaceBinding): void;
|
|
43
|
+
//# sourceMappingURL=lifecycle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../../../src/core/workflow/workspaces/lifecycle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAErD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAC3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AACzD,OAAO,KAAK,EACV,uBAAuB,EACvB,uBAAuB,EACvB,wBAAwB,EACxB,eAAe,EACf,gBAAgB,EAChB,4BAA4B,EAC5B,2BAA2B,EAC3B,uBAAuB,EACvB,sBAAsB,EACtB,uBAAuB,EACvB,yBAAyB,EACzB,uBAAuB,EACvB,qBAAqB,EACrB,sBAAsB,EACvB,MAAM,YAAY,CAAC;AAgHpB,MAAM,WAAW,iCAAiC;IAChD,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B,eAAe,CAAC,EAAE,SAAS,sBAAsB,EAAE,CAAC;IACpD,QAAQ,CAAC,EAAE,mBAAmB,CAAC;IAC/B,gBAAgB,CAAC,EAAE,SAAS;QAC1B,KAAK,EAAE,MAAM,CAAC;QACd,EAAE,CAAC,EAAE,OAAO,CAAC;QACb,MAAM,CAAC,EAAE,SAAS,GAAG,MAAM,GAAG,2BAA2B,CAAC,QAAQ,CAAC,CAAC;QACpE,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,iBAAiB,CAAC,EAAE,MAAM,CAAC;KAC5B,EAAE,CAAC;IACJ,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gBAAgB,CAAC,EAAE,eAAe,CAAC;IACnC,yBAAyB,CAAC,EAAE,wBAAwB,CAAC;IACrD,oBAAoB,CAAC,EAAE,SAAS,uBAAuB,EAAE,CAAC;IAC1D,mBAAmB,CAAC,EAAE,uBAAuB,CAAC;IAC9C,cAAc,CAAC,EAAE,sBAAsB,CAAC;IACxC,gBAAgB,CAAC,EAAE,SAAS,uBAAuB,EAAE,CAAC;IACtD,eAAe,CAAC,EAAE,uBAAuB,CAAC;IAC1C,WAAW,CAAC,EAAE,SAAS,SAAS,EAAE,CAAC;IACnC,oBAAoB,CAAC,EAAE,2BAA2B,CAAC;IACnD,oBAAoB,CAAC,EAAE,4BAA4B,CAAC;CACrD;AAED,MAAM,WAAW,4BAA4B;IAC3C,KAAK,EAAE,uBAAuB,CAAC;IAC/B,gBAAgB,EAAE,qBAAqB,GAAG,IAAI,CAAC;IAC/C,cAAc,EAAE,sBAAsB,GAAG,IAAI,CAAC;IAC9C,iBAAiB,EAAE,yBAAyB,CAAC;IAC7C,2BAA2B,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3C,YAAY,EAAE,MAAM,GAAG,gBAAgB,GAAG,WAAW,CAAC;CACvD;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,uBAAuB,CAE1F;AAED,wBAAgB,kCAAkC,CAChD,IAAI,EAAE,uBAAuB,EAC7B,EAAE,EAAE,uBAAuB,GAC1B,IAAI,CAON;AAED,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,iCAAiC,GACvC,4BAA4B,CA2G9B;AAicD,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,SAAS,sBAAsB,EAAE,EACzC,OAAO,CAAC,EAAE,gBAAgB,GACzB,IAAI,CAiBN"}
|