@abelxiaoxing/cadence 1.5.3 → 1.7.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 +61 -1
- package/config/plan-draft.example.json +3 -0
- package/config/plan-draft.multiple-tasks.example.json +30 -0
- package/package.json +1 -1
- package/prompts/abel-design.md +33 -3
- package/prompts/abel-implement.md +36 -4
- package/src/apply-transaction.ts +1 -9
- package/src/artifact-store.ts +1 -9
- package/src/change-verification.ts +758 -65
- package/src/contracts.ts +8 -0
- package/src/delivery-compiler.ts +452 -9
- package/src/design-control.ts +2 -2
- package/src/design-diagnostics.ts +6 -4
- package/src/directory-sync.ts +36 -0
- package/src/durable-contracts.ts +19 -3
- package/src/durable-workflow.ts +47 -38
- package/src/implement-continuation.ts +559 -0
- package/src/implement-graph.ts +114 -0
- package/src/implement-plan.ts +2 -0
- package/src/index.ts +179 -1
- package/src/operator-cli.mjs +12 -7
- package/src/package-state.ts +309 -0
- package/src/package-verification.ts +17 -1
- package/src/package-workflow.ts +219 -171
- package/src/phase-execution.ts +7 -1
- package/src/plan-draft-summary.ts +385 -7
- package/src/plan-draft.ts +9 -0
- package/src/run-store.ts +39 -15
- package/src/storage-schema.ts +14 -6
- package/src/subagent-activity.ts +3 -1
- package/src/verification-capability.ts +17 -5
- package/src/verification-environment.ts +20 -2
- package/src/workflow-policy.ts +296 -5
- package/src/workflow-recovery-policy.ts +2 -0
- package/src/workflow-state-machine.ts +400 -131
- package/src/workflow-status.ts +251 -59
- package/src/workspace-io-worker.mjs +48 -42
- package/src/workspace-store.ts +1 -9
package/src/workflow-status.ts
CHANGED
|
@@ -8,10 +8,14 @@ import {
|
|
|
8
8
|
type EngineRunRow,
|
|
9
9
|
type EngineTaskRow,
|
|
10
10
|
hash,
|
|
11
|
+
normalizeVerificationPrerequisite,
|
|
11
12
|
normalizeWorkflowContextRequest,
|
|
12
13
|
normalizeWorkflowVerificationStatus,
|
|
14
|
+
parentRecoveryStrategy,
|
|
13
15
|
parseDeliveryDiagnostics,
|
|
14
16
|
permitsPlanAmendment,
|
|
17
|
+
permitsPrerequisiteAmendment,
|
|
18
|
+
permitsRecoveryAmendment,
|
|
15
19
|
type SafeAttemptDiagnostic,
|
|
16
20
|
} from "./workflow-policy.ts";
|
|
17
21
|
|
|
@@ -60,6 +64,9 @@ export function projectWorkflowStatus({
|
|
|
60
64
|
(right.queue_position ?? Number.MAX_SAFE_INTEGER) ||
|
|
61
65
|
left.task_order - right.task_order,
|
|
62
66
|
);
|
|
67
|
+
const activeRows = rows.filter((row) =>
|
|
68
|
+
["approval-needed", "paused", "retryable"].includes(row.state),
|
|
69
|
+
);
|
|
63
70
|
const firstPaused = firstPausedTask(rows);
|
|
64
71
|
const contextRequest = firstPaused?.context_request_json
|
|
65
72
|
? normalizeWorkflowContextRequest(
|
|
@@ -71,6 +78,10 @@ export function projectWorkflowStatus({
|
|
|
71
78
|
firstPaused?.state === "approval-needed"
|
|
72
79
|
? (firstPaused.pause_code ?? projection.pauseCode ?? "task-paused")
|
|
73
80
|
: (projection.pauseCode ?? firstPaused?.pause_code ?? "task-paused");
|
|
81
|
+
const taskPauseSuperseded =
|
|
82
|
+
projection.state === "paused" &&
|
|
83
|
+
projection.pauseCode !== undefined &&
|
|
84
|
+
!activeRows.some((row) => row.pause_code === projection.pauseCode);
|
|
74
85
|
const approval =
|
|
75
86
|
projection.state === "approval-needed"
|
|
76
87
|
? approvalRequirement(pauseCode, contextRequest)
|
|
@@ -78,36 +89,75 @@ export function projectWorkflowStatus({
|
|
|
78
89
|
const attemptDiagnostic = firstPaused
|
|
79
90
|
? attemptDiagnostics.get(firstPaused.task_id)
|
|
80
91
|
: undefined;
|
|
81
|
-
const recovery =
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
.find((fact) => fact && fact.failures >= fact.feedback.maxAttempts) ??
|
|
85
|
-
attemptDiagnostic?.recovery;
|
|
92
|
+
const recovery = taskPauseSuperseded
|
|
93
|
+
? undefined
|
|
94
|
+
: attemptDiagnostic?.recovery;
|
|
86
95
|
const recoveryExhausted =
|
|
87
96
|
recovery && recovery.failures >= recovery.feedback.maxAttempts;
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
97
|
+
const selectedPrerequisite = normalizeVerificationPrerequisite(
|
|
98
|
+
taskPauseSuperseded ? undefined : attemptDiagnostic?.prerequisite,
|
|
99
|
+
);
|
|
100
|
+
const selectedRecoveryStrategy = parentRecoveryStrategy(
|
|
101
|
+
pauseCode,
|
|
102
|
+
taskPauseSuperseded ? undefined : attemptDiagnostic,
|
|
103
|
+
);
|
|
104
|
+
const selectedPauseActionable =
|
|
105
|
+
(projection.state === "approval-needed" && approval !== undefined) ||
|
|
106
|
+
(projection.state === "paused" &&
|
|
107
|
+
(permitsPlanAmendment(pauseCode) ||
|
|
108
|
+
selectedRecoveryStrategy !== undefined ||
|
|
109
|
+
permitsPrerequisiteAmendment(selectedPrerequisite)));
|
|
110
|
+
const blockers = activeRows.map((row) => {
|
|
111
|
+
const context = row.context_request_json
|
|
112
|
+
? normalizeWorkflowContextRequest(JSON.parse(row.context_request_json))
|
|
113
|
+
: undefined;
|
|
114
|
+
const code = row.pause_code ?? "task-paused";
|
|
115
|
+
const diagnostic = attemptDiagnostics.get(row.task_id);
|
|
116
|
+
const prerequisite = normalizeVerificationPrerequisite(
|
|
117
|
+
diagnostic?.prerequisite,
|
|
118
|
+
);
|
|
119
|
+
const requirement =
|
|
120
|
+
row.state === "approval-needed"
|
|
121
|
+
? approvalRequirement(code, context)
|
|
95
122
|
: undefined;
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
123
|
+
return {
|
|
124
|
+
id: hash(runId, row.task_id, code, canonicalJson(context ?? {})),
|
|
125
|
+
taskId: row.task_id,
|
|
126
|
+
phase: row.phase,
|
|
127
|
+
code,
|
|
128
|
+
kind: requirement ? "decision" : "execution",
|
|
129
|
+
...(requirement ?? {}),
|
|
130
|
+
...(context ? { contextRequest: context } : {}),
|
|
131
|
+
...(prerequisite
|
|
132
|
+
? {
|
|
133
|
+
prerequisite,
|
|
134
|
+
scope:
|
|
135
|
+
prerequisite.scope === "baseline-full-suite" ? "change" : "task",
|
|
136
|
+
minimumRecovery: permitsPrerequisiteAmendment(prerequisite)
|
|
137
|
+
? "Revise the baseline contract to read safe inputs from the retained original revision, then recompile within the existing authority."
|
|
138
|
+
: prerequisite.cause === "capability"
|
|
139
|
+
? "Restore the required runner or external capability; a changed environment identity permits a bounded check."
|
|
140
|
+
: "Restore trusted verification prerequisites; the retained failure does not authorize plan changes.",
|
|
141
|
+
}
|
|
142
|
+
: {}),
|
|
143
|
+
...(diagnostic?.recovery
|
|
144
|
+
? {
|
|
145
|
+
recovery: {
|
|
146
|
+
code: diagnostic.recovery.feedback.code,
|
|
147
|
+
attempts: diagnostic.recovery.failures,
|
|
148
|
+
maxAttempts: diagnostic.recovery.feedback.maxAttempts,
|
|
149
|
+
strategy: diagnostic.recovery.feedback.strategy,
|
|
150
|
+
...(diagnostic.recovery.feedback.failureIdentities
|
|
151
|
+
? {
|
|
152
|
+
failureIdentities:
|
|
153
|
+
diagnostic.recovery.feedback.failureIdentities,
|
|
154
|
+
}
|
|
155
|
+
: {}),
|
|
156
|
+
},
|
|
157
|
+
}
|
|
158
|
+
: {}),
|
|
159
|
+
};
|
|
160
|
+
});
|
|
111
161
|
if (
|
|
112
162
|
projection.state === "approval-needed" &&
|
|
113
163
|
!blockers.some((item) => item.kind === "decision") &&
|
|
@@ -123,9 +173,19 @@ export function projectWorkflowStatus({
|
|
|
123
173
|
});
|
|
124
174
|
}
|
|
125
175
|
const decisions = blockers.filter((item) => item.kind === "decision");
|
|
126
|
-
const amendments =
|
|
127
|
-
|
|
128
|
-
|
|
176
|
+
const amendments = !selectedPauseActionable
|
|
177
|
+
? []
|
|
178
|
+
: blockers.filter(
|
|
179
|
+
(item) =>
|
|
180
|
+
item.kind === "decision" ||
|
|
181
|
+
permitsPlanAmendment(item.code) ||
|
|
182
|
+
permitsRecoveryAmendment(
|
|
183
|
+
item.code,
|
|
184
|
+
attemptDiagnostics.get(item.taskId),
|
|
185
|
+
) ||
|
|
186
|
+
("prerequisite" in item &&
|
|
187
|
+
permitsPrerequisiteAmendment(item.prerequisite)),
|
|
188
|
+
);
|
|
129
189
|
if (
|
|
130
190
|
projection.state === "paused" &&
|
|
131
191
|
permitsPlanAmendment(pauseCode) &&
|
|
@@ -147,6 +207,16 @@ export function projectWorkflowStatus({
|
|
|
147
207
|
remaining: 64 - amendmentUsed,
|
|
148
208
|
exhausted: amendmentUsed >= 64,
|
|
149
209
|
};
|
|
210
|
+
const budgetCode = [
|
|
211
|
+
"change-work-budget-exhausted",
|
|
212
|
+
"change-recovery-budget-exhausted",
|
|
213
|
+
].includes(pauseCode)
|
|
214
|
+
? pauseCode
|
|
215
|
+
: undefined;
|
|
216
|
+
const budgetExhausted = budgetCode !== undefined;
|
|
217
|
+
const hasWorkCapacity =
|
|
218
|
+
!budgetExhausted &&
|
|
219
|
+
(!resourceBudget || resourceBudget.used < resourceBudget.maximum);
|
|
150
220
|
const decisionBatch =
|
|
151
221
|
amendments.length &&
|
|
152
222
|
pauseCode !== "operation-cancelled" &&
|
|
@@ -179,12 +249,139 @@ export function projectWorkflowStatus({
|
|
|
179
249
|
].sort(),
|
|
180
250
|
}
|
|
181
251
|
: undefined;
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const
|
|
252
|
+
const guidanceRow = taskPauseSuperseded ? undefined : firstPaused;
|
|
253
|
+
const guidanceDiagnostic = taskPauseSuperseded
|
|
254
|
+
? undefined
|
|
255
|
+
: attemptDiagnostic;
|
|
256
|
+
const guidanceCode = pauseCode;
|
|
257
|
+
const guidanceStrategy = selectedRecoveryStrategy;
|
|
258
|
+
const failureSequence = recovery
|
|
259
|
+
? failureSequences.get(recovery.key)
|
|
260
|
+
: undefined;
|
|
261
|
+
const recoveryGrant =
|
|
262
|
+
recoveryExhausted &&
|
|
263
|
+
guidanceStrategy !== undefined &&
|
|
264
|
+
projection.state === "paused" &&
|
|
265
|
+
hasWorkCapacity &&
|
|
266
|
+
resourceBudget &&
|
|
267
|
+
failureSequence !== undefined &&
|
|
268
|
+
Number.isSafeInteger(failureSequence) &&
|
|
269
|
+
failureSequence > 0
|
|
270
|
+
? {
|
|
271
|
+
incidentKey: recovery.key,
|
|
272
|
+
failureSequence,
|
|
273
|
+
reason: "parent-directed-retry" as const,
|
|
274
|
+
}
|
|
275
|
+
: undefined;
|
|
276
|
+
const recoveryContinuation =
|
|
277
|
+
projection.state === "paused" &&
|
|
278
|
+
hasWorkCapacity &&
|
|
279
|
+
guidanceStrategy &&
|
|
280
|
+
(!recoveryExhausted || recoveryGrant)
|
|
281
|
+
? {
|
|
282
|
+
owner: "parent" as const,
|
|
283
|
+
automatic: true,
|
|
284
|
+
kind: "inspect-recovery" as const,
|
|
285
|
+
reason: guidanceCode,
|
|
286
|
+
stage: projection.stage,
|
|
287
|
+
change: projection.change,
|
|
288
|
+
metadata: {
|
|
289
|
+
...(guidanceRow
|
|
290
|
+
? { taskId: guidanceRow.task_id, phase: guidanceRow.phase }
|
|
291
|
+
: {}),
|
|
292
|
+
diagnostic: {
|
|
293
|
+
code: guidanceCode,
|
|
294
|
+
strategy:
|
|
295
|
+
guidanceDiagnostic?.recovery?.feedback.strategy ??
|
|
296
|
+
guidanceStrategy,
|
|
297
|
+
...(guidanceDiagnostic?.recovery
|
|
298
|
+
? {
|
|
299
|
+
inspection: guidanceStrategy,
|
|
300
|
+
attempts: guidanceDiagnostic.recovery.failures,
|
|
301
|
+
maxAttempts:
|
|
302
|
+
guidanceDiagnostic.recovery.feedback.maxAttempts,
|
|
303
|
+
...(guidanceDiagnostic.recovery.feedback.failureIdentities
|
|
304
|
+
? {
|
|
305
|
+
failureIdentities:
|
|
306
|
+
guidanceDiagnostic.recovery.feedback
|
|
307
|
+
.failureIdentities,
|
|
308
|
+
}
|
|
309
|
+
: {}),
|
|
310
|
+
}
|
|
311
|
+
: {}),
|
|
312
|
+
...(guidanceDiagnostic?.prerequisite
|
|
313
|
+
? {
|
|
314
|
+
prerequisite: normalizeVerificationPrerequisite(
|
|
315
|
+
guidanceDiagnostic.prerequisite,
|
|
316
|
+
),
|
|
317
|
+
}
|
|
318
|
+
: {}),
|
|
319
|
+
...(guidanceRow?.route_id || engineRun?.route_id
|
|
320
|
+
? {
|
|
321
|
+
route: {
|
|
322
|
+
routeId:
|
|
323
|
+
guidanceRow?.route_id ?? engineRun?.route_id ?? "",
|
|
324
|
+
...((guidanceRow?.route_fingerprint ??
|
|
325
|
+
engineRun?.route_fingerprint)
|
|
326
|
+
? {
|
|
327
|
+
routeFingerprint:
|
|
328
|
+
guidanceRow?.route_fingerprint ??
|
|
329
|
+
engineRun?.route_fingerprint,
|
|
330
|
+
}
|
|
331
|
+
: {}),
|
|
332
|
+
},
|
|
333
|
+
}
|
|
334
|
+
: {}),
|
|
335
|
+
},
|
|
336
|
+
...(recoveryGrant
|
|
337
|
+
? {
|
|
338
|
+
recommendation: {
|
|
339
|
+
kind: "bounded-additional-attempt" as const,
|
|
340
|
+
resume: { recovery: recoveryGrant },
|
|
341
|
+
},
|
|
342
|
+
}
|
|
343
|
+
: {}),
|
|
344
|
+
},
|
|
345
|
+
}
|
|
346
|
+
: undefined;
|
|
347
|
+
const amendmentContinuation =
|
|
348
|
+
decisionBatch && !amendmentBudget.exhausted && hasWorkCapacity
|
|
349
|
+
? {
|
|
350
|
+
owner: "parent" as const,
|
|
351
|
+
automatic: true,
|
|
352
|
+
...decisionBatch.continuation,
|
|
353
|
+
}
|
|
354
|
+
: undefined;
|
|
355
|
+
const interruptedContinuation =
|
|
356
|
+
projection.stage === "abel-implement" &&
|
|
357
|
+
projection.change &&
|
|
358
|
+
projection.pauseCode === "operation-interrupted" &&
|
|
359
|
+
legalControlCommands(projection.state).includes("resume") &&
|
|
360
|
+
(projection.state === "recovering" ||
|
|
361
|
+
(projection.state === "paused" && hasWorkCapacity))
|
|
362
|
+
? {
|
|
363
|
+
owner: "parent" as const,
|
|
364
|
+
automatic: true,
|
|
365
|
+
command: "resume" as const,
|
|
366
|
+
kind:
|
|
367
|
+
projection.state === "recovering"
|
|
368
|
+
? ("settle-apply-recovery" as const)
|
|
369
|
+
: ("resume-interrupted-operation" as const),
|
|
370
|
+
reason: "operation-interrupted" as const,
|
|
371
|
+
stage: projection.stage,
|
|
372
|
+
change: projection.change,
|
|
373
|
+
...(guidanceRow?.pause_code === "operation-interrupted"
|
|
374
|
+
? {
|
|
375
|
+
metadata: {
|
|
376
|
+
taskId: guidanceRow.task_id,
|
|
377
|
+
phase: guidanceRow.phase,
|
|
378
|
+
},
|
|
379
|
+
}
|
|
380
|
+
: {}),
|
|
381
|
+
}
|
|
382
|
+
: undefined;
|
|
383
|
+
const continuation =
|
|
384
|
+
interruptedContinuation ?? recoveryContinuation ?? amendmentContinuation;
|
|
188
385
|
const currentRevision = projection.deliveryRevision ?? 0;
|
|
189
386
|
return {
|
|
190
387
|
runId: projection.runId,
|
|
@@ -220,17 +417,6 @@ export function projectWorkflowStatus({
|
|
|
220
417
|
? {
|
|
221
418
|
decisionBatch,
|
|
222
419
|
amendmentBudget,
|
|
223
|
-
...(!amendmentBudget.exhausted &&
|
|
224
|
-
!budgetExhausted &&
|
|
225
|
-
(!resourceBudget || resourceBudget.used < resourceBudget.maximum)
|
|
226
|
-
? {
|
|
227
|
-
continuation: {
|
|
228
|
-
owner: "parent",
|
|
229
|
-
automatic: true,
|
|
230
|
-
...decisionBatch.continuation,
|
|
231
|
-
},
|
|
232
|
-
}
|
|
233
|
-
: {}),
|
|
234
420
|
}
|
|
235
421
|
: {}),
|
|
236
422
|
...(recoveryExhausted
|
|
@@ -242,20 +428,11 @@ export function projectWorkflowStatus({
|
|
|
242
428
|
attempts: recovery.failures,
|
|
243
429
|
maxAttempts: recovery.feedback.maxAttempts,
|
|
244
430
|
automaticRetryExhausted: true,
|
|
245
|
-
...(
|
|
246
|
-
resourceBudget &&
|
|
247
|
-
resourceBudget.used < resourceBudget.maximum
|
|
248
|
-
? {
|
|
249
|
-
additionalAttempt: {
|
|
250
|
-
incidentKey: recovery.key,
|
|
251
|
-
failureSequence: failureSequences.get(recovery.key) ?? 0,
|
|
252
|
-
reason: "parent-directed-retry",
|
|
253
|
-
},
|
|
254
|
-
}
|
|
255
|
-
: {}),
|
|
431
|
+
...(recoveryGrant ? { additionalAttempt: recoveryGrant } : {}),
|
|
256
432
|
},
|
|
257
433
|
}
|
|
258
434
|
: {}),
|
|
435
|
+
...(continuation ? { continuation } : {}),
|
|
259
436
|
...(projection.terminal ? { terminal: projection.terminal } : {}),
|
|
260
437
|
...(projection.pauseCode || firstPaused?.pause_code
|
|
261
438
|
? {
|
|
@@ -298,6 +475,10 @@ export function projectWorkflowStatus({
|
|
|
298
475
|
receiptHash: "matching-ready-receipt",
|
|
299
476
|
},
|
|
300
477
|
},
|
|
478
|
+
}
|
|
479
|
+
: {}),
|
|
480
|
+
...(approval && projection.change
|
|
481
|
+
? {
|
|
301
482
|
conditionalCommands: [
|
|
302
483
|
{
|
|
303
484
|
command: "resume",
|
|
@@ -310,7 +491,18 @@ export function projectWorkflowStatus({
|
|
|
310
491
|
},
|
|
311
492
|
],
|
|
312
493
|
}
|
|
313
|
-
:
|
|
494
|
+
: recoveryGrant && projection.change
|
|
495
|
+
? {
|
|
496
|
+
conditionalCommands: [
|
|
497
|
+
{
|
|
498
|
+
command: "resume",
|
|
499
|
+
stage: "abel-implement",
|
|
500
|
+
change: projection.change,
|
|
501
|
+
requires: { recovery: recoveryGrant },
|
|
502
|
+
},
|
|
503
|
+
],
|
|
504
|
+
}
|
|
505
|
+
: {}),
|
|
314
506
|
tasks: rows.map((row) => ({
|
|
315
507
|
taskId: row.task_id,
|
|
316
508
|
state: row.state,
|
|
@@ -7,19 +7,41 @@ import { parentPort, workerData } from "node:worker_threads";
|
|
|
7
7
|
import { createHash, randomUUID } from "node:crypto";
|
|
8
8
|
import {
|
|
9
9
|
chmodSync,
|
|
10
|
-
closeSync,
|
|
11
|
-
constants,
|
|
10
|
+
closeSync as closeSync2,
|
|
11
|
+
constants as constants2,
|
|
12
12
|
copyFileSync,
|
|
13
|
-
fsyncSync,
|
|
13
|
+
fsyncSync as fsyncSync2,
|
|
14
14
|
lstatSync,
|
|
15
15
|
mkdirSync,
|
|
16
|
-
openSync,
|
|
16
|
+
openSync as openSync2,
|
|
17
17
|
readFileSync,
|
|
18
18
|
renameSync,
|
|
19
19
|
unlinkSync,
|
|
20
20
|
writeFileSync
|
|
21
21
|
} from "node:fs";
|
|
22
22
|
import path from "node:path";
|
|
23
|
+
|
|
24
|
+
// src/directory-sync.ts
|
|
25
|
+
import { closeSync, constants, fstatSync, fsyncSync, openSync } from "node:fs";
|
|
26
|
+
function syncDirectory(directory) {
|
|
27
|
+
const descriptor = openSync(directory, constants.O_RDONLY);
|
|
28
|
+
try {
|
|
29
|
+
try {
|
|
30
|
+
fsyncSync(descriptor);
|
|
31
|
+
return "synced";
|
|
32
|
+
} catch (error) {
|
|
33
|
+
const failure = error;
|
|
34
|
+
if (process.platform === "win32" && failure?.code === "EPERM" && failure.syscall === "fsync" && fstatSync(descriptor).isDirectory()) {
|
|
35
|
+
return "unsupported-directory-barrier";
|
|
36
|
+
}
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
} finally {
|
|
40
|
+
closeSync(descriptor);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// src/artifact-store.ts
|
|
23
45
|
var SHA256 = /^[a-f0-9]{64}$/u;
|
|
24
46
|
function digest(bytes) {
|
|
25
47
|
return createHash("sha256").update(bytes).digest("hex");
|
|
@@ -55,24 +77,16 @@ function ensureDirectory(directory) {
|
|
|
55
77
|
}
|
|
56
78
|
chmodSync(directory, 448);
|
|
57
79
|
}
|
|
58
|
-
function syncDirectory(directory) {
|
|
59
|
-
const descriptor = openSync(directory, constants.O_RDONLY);
|
|
60
|
-
try {
|
|
61
|
-
fsyncSync(descriptor);
|
|
62
|
-
} finally {
|
|
63
|
-
closeSync(descriptor);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
80
|
function writeAtomic(target, bytes) {
|
|
67
81
|
const directory = path.dirname(target);
|
|
68
82
|
ensureDirectory(directory);
|
|
69
83
|
const temporary = path.join(directory, `.${path.basename(target)}.${process.pid}.${randomUUID()}.tmp`);
|
|
70
|
-
const descriptor =
|
|
84
|
+
const descriptor = openSync2(temporary, constants2.O_CREAT | constants2.O_EXCL | constants2.O_WRONLY, 384);
|
|
71
85
|
try {
|
|
72
86
|
writeFileSync(descriptor, bytes);
|
|
73
|
-
|
|
87
|
+
fsyncSync2(descriptor);
|
|
74
88
|
} finally {
|
|
75
|
-
|
|
89
|
+
closeSync2(descriptor);
|
|
76
90
|
}
|
|
77
91
|
try {
|
|
78
92
|
renameSync(temporary, target);
|
|
@@ -148,7 +162,7 @@ class ArtifactStore {
|
|
|
148
162
|
const stat = lstatSync(source, { throwIfNoEntry: false });
|
|
149
163
|
if (!stat?.isFile() || stat.isSymbolicLink())
|
|
150
164
|
throw new Error("artifact-unavailable");
|
|
151
|
-
copyFileSync(source, destination,
|
|
165
|
+
copyFileSync(source, destination, constants2.COPYFILE_EXCL | constants2.COPYFILE_FICLONE);
|
|
152
166
|
const bytes = readFileSync(destination);
|
|
153
167
|
if (digest(bytes) !== hash)
|
|
154
168
|
throw new Error("artifact-integrity-invalid");
|
|
@@ -199,7 +213,7 @@ class ArtifactStore {
|
|
|
199
213
|
|
|
200
214
|
// src/verification-environment-io.ts
|
|
201
215
|
import {
|
|
202
|
-
constants as
|
|
216
|
+
constants as constants3,
|
|
203
217
|
cpSync,
|
|
204
218
|
lstatSync as lstatSync2,
|
|
205
219
|
mkdirSync as mkdirSync2,
|
|
@@ -329,7 +343,7 @@ function prepareVerificationEnvironmentIo(root, dependencyOwner, runnerBindings,
|
|
|
329
343
|
checkCancelled();
|
|
330
344
|
return true;
|
|
331
345
|
},
|
|
332
|
-
mode:
|
|
346
|
+
mode: constants3.COPYFILE_FICLONE,
|
|
333
347
|
verbatimSymlinks: true
|
|
334
348
|
});
|
|
335
349
|
};
|
|
@@ -410,11 +424,11 @@ function removeEntry(file, checkCancelled) {
|
|
|
410
424
|
// src/verification-identity.ts
|
|
411
425
|
import { createHash as createHash2 } from "node:crypto";
|
|
412
426
|
import {
|
|
413
|
-
closeSync as
|
|
414
|
-
constants as
|
|
415
|
-
fstatSync,
|
|
427
|
+
closeSync as closeSync3,
|
|
428
|
+
constants as constants4,
|
|
429
|
+
fstatSync as fstatSync2,
|
|
416
430
|
lstatSync as lstatSync3,
|
|
417
|
-
openSync as
|
|
431
|
+
openSync as openSync3,
|
|
418
432
|
readdirSync as readdirSync2,
|
|
419
433
|
readlinkSync,
|
|
420
434
|
readSync
|
|
@@ -451,9 +465,9 @@ function verificationEnvironmentDigest(roots, checkCancelled = () => {}) {
|
|
|
451
465
|
}
|
|
452
466
|
if (!stat.isFile())
|
|
453
467
|
throw new Error("verification-environment-unsafe");
|
|
454
|
-
const fd =
|
|
468
|
+
const fd = openSync3(file, constants4.O_RDONLY | constants4.O_NOFOLLOW);
|
|
455
469
|
try {
|
|
456
|
-
const before =
|
|
470
|
+
const before = fstatSync2(fd);
|
|
457
471
|
if (before.ino !== stat.ino || before.dev !== stat.dev)
|
|
458
472
|
throw new Error("verification-environment-changed");
|
|
459
473
|
const content = createHash2("sha256");
|
|
@@ -467,12 +481,12 @@ function verificationEnvironmentDigest(roots, checkCancelled = () => {}) {
|
|
|
467
481
|
throw new Error("verification-environment-limit");
|
|
468
482
|
content.update(buffer.subarray(0, count));
|
|
469
483
|
}
|
|
470
|
-
const after =
|
|
484
|
+
const after = fstatSync2(fd);
|
|
471
485
|
if (before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs)
|
|
472
486
|
throw new Error("verification-environment-changed");
|
|
473
487
|
digest2.update(JSON.stringify(["file", content.digest("hex")]));
|
|
474
488
|
} finally {
|
|
475
|
-
|
|
489
|
+
closeSync3(fd);
|
|
476
490
|
}
|
|
477
491
|
};
|
|
478
492
|
for (const root of [...new Set(roots)].sort())
|
|
@@ -485,12 +499,12 @@ import { execFile, execFileSync } from "node:child_process";
|
|
|
485
499
|
import { createHash as createHash3, randomUUID as randomUUID2 } from "node:crypto";
|
|
486
500
|
import {
|
|
487
501
|
chmodSync as chmodSync2,
|
|
488
|
-
closeSync as
|
|
489
|
-
constants as
|
|
490
|
-
fsyncSync as
|
|
502
|
+
closeSync as closeSync4,
|
|
503
|
+
constants as constants5,
|
|
504
|
+
fsyncSync as fsyncSync3,
|
|
491
505
|
lstatSync as lstatSync5,
|
|
492
506
|
mkdirSync as mkdirSync3,
|
|
493
|
-
openSync as
|
|
507
|
+
openSync as openSync4,
|
|
494
508
|
readFileSync as readFileSync2,
|
|
495
509
|
renameSync as renameSync2,
|
|
496
510
|
unlinkSync as unlinkSync2,
|
|
@@ -765,29 +779,21 @@ function ensureDirectory2(directory) {
|
|
|
765
779
|
}
|
|
766
780
|
chmodSync2(directory, 448);
|
|
767
781
|
}
|
|
768
|
-
function syncDirectory2(directory) {
|
|
769
|
-
const descriptor = openSync3(directory, constants4.O_RDONLY);
|
|
770
|
-
try {
|
|
771
|
-
fsyncSync2(descriptor);
|
|
772
|
-
} finally {
|
|
773
|
-
closeSync3(descriptor);
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
782
|
function writeAtomic2(target, bytes) {
|
|
777
783
|
const directory = path5.dirname(target);
|
|
778
784
|
ensureDirectory2(directory);
|
|
779
785
|
const temporary = path5.join(directory, `.${path5.basename(target)}.${process.pid}.${randomUUID2()}.tmp`);
|
|
780
|
-
const descriptor =
|
|
786
|
+
const descriptor = openSync4(temporary, constants5.O_CREAT | constants5.O_EXCL | constants5.O_WRONLY, 384);
|
|
781
787
|
try {
|
|
782
788
|
writeFileSync3(descriptor, bytes);
|
|
783
|
-
|
|
789
|
+
fsyncSync3(descriptor);
|
|
784
790
|
} finally {
|
|
785
|
-
|
|
791
|
+
closeSync4(descriptor);
|
|
786
792
|
}
|
|
787
793
|
try {
|
|
788
794
|
renameSync2(temporary, target);
|
|
789
795
|
chmodSync2(target, 384);
|
|
790
|
-
|
|
796
|
+
syncDirectory(directory);
|
|
791
797
|
} catch (error) {
|
|
792
798
|
try {
|
|
793
799
|
unlinkSync2(temporary);
|
package/src/workspace-store.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { promisify } from "node:util";
|
|
|
18
18
|
import type { ArtifactStore } from "./artifact-store.ts";
|
|
19
19
|
import { compareCanonicalStrings } from "./canonical.ts";
|
|
20
20
|
import { isValidRelativePath } from "./contracts.ts";
|
|
21
|
+
import { syncDirectory } from "./directory-sync.ts";
|
|
21
22
|
import { observeSafePath } from "./safe-path.ts";
|
|
22
23
|
import { runWorkspaceIo, type WorkspaceIoMetrics } from "./workspace-io.ts";
|
|
23
24
|
|
|
@@ -127,15 +128,6 @@ function ensureDirectory(directory: string): void {
|
|
|
127
128
|
chmodSync(directory, 0o700);
|
|
128
129
|
}
|
|
129
130
|
|
|
130
|
-
function syncDirectory(directory: string): void {
|
|
131
|
-
const descriptor = openSync(directory, constants.O_RDONLY);
|
|
132
|
-
try {
|
|
133
|
-
fsyncSync(descriptor);
|
|
134
|
-
} finally {
|
|
135
|
-
closeSync(descriptor);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
131
|
function writeAtomic(target: string, bytes: Uint8Array): void {
|
|
140
132
|
const directory = path.dirname(target);
|
|
141
133
|
ensureDirectory(directory);
|