@abelxiaoxing/cadence 1.6.0 → 1.8.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 +25 -4
- package/config/.env.example +6 -0
- 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 +28 -8
- package/src/build-windows-job.mjs +95 -0
- package/src/candidate-workspace.ts +6 -0
- package/src/change-verification.ts +764 -67
- 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/durable-contracts.ts +21 -3
- package/src/durable-workflow.ts +55 -40
- package/src/execution-profile.ts +28 -2
- package/src/execution-retention.ts +77 -0
- 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 +151 -1
- package/src/isolation-backend.ts +2 -1
- package/src/operator-cli.mjs +664 -104
- package/src/operator-tools.ts +7 -1
- package/src/package-state.ts +11 -0
- package/src/package-verification.ts +66 -18
- package/src/package-workflow.ts +6 -0
- package/src/phase-execution.ts +18 -7
- package/src/plan-draft-summary.ts +385 -7
- package/src/plan-draft.ts +9 -0
- package/src/remove-path.ts +32 -0
- package/src/subagent-activity.ts +3 -1
- package/src/verification-capability.ts +18 -6
- package/src/verification-environment-io.ts +123 -14
- package/src/verification-environment.ts +50 -3
- package/src/windows-job-backend.ts +419 -0
- package/src/windows-job.c +149 -0
- package/src/workflow-policy.ts +296 -5
- package/src/workflow-recovery-policy.ts +2 -0
- package/src/workflow-state-machine.ts +314 -80
- package/src/workflow-status.ts +251 -59
- package/src/workspace-io-entry.mjs +2 -2
- package/src/workspace-io-worker.mjs +207 -110
package/src/durable-contracts.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { ArtifactStore } from "./artifact-store.ts";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
StructuredVerificationContract,
|
|
4
|
+
VerificationInputObservation,
|
|
5
|
+
} from "./contracts.ts";
|
|
3
6
|
import type { ImplementPlan, PlanTaskDraft } from "./delivery-compiler.ts";
|
|
4
7
|
import type { RoutePolicy, WorkerRoutePolicy } from "./route-policy.ts";
|
|
5
8
|
|
|
@@ -15,6 +18,7 @@ import type {
|
|
|
15
18
|
WorkflowContextRequest,
|
|
16
19
|
WorkflowDeliverySource,
|
|
17
20
|
WorkflowRecoveryFeedback,
|
|
21
|
+
WorkflowVerificationPrerequisite,
|
|
18
22
|
} from "./workflow-policy.ts";
|
|
19
23
|
|
|
20
24
|
import type { WorkspaceStore } from "./workspace-store.ts";
|
|
@@ -38,6 +42,7 @@ export type DurablePhaseVerificationResult =
|
|
|
38
42
|
ok: false;
|
|
39
43
|
kind: "paused" | "retryable" | "approval-needed";
|
|
40
44
|
code: string;
|
|
45
|
+
inputObservation?: VerificationInputObservation;
|
|
41
46
|
};
|
|
42
47
|
|
|
43
48
|
export type DurableChangeVerificationResult =
|
|
@@ -60,6 +65,7 @@ export type DurableChangeVerificationResult =
|
|
|
60
65
|
code: string;
|
|
61
66
|
failureIdentities?: string[];
|
|
62
67
|
attributionReliable?: boolean;
|
|
68
|
+
inputObservation?: VerificationInputObservation;
|
|
63
69
|
};
|
|
64
70
|
|
|
65
71
|
export interface DurableVerificationObservation {
|
|
@@ -68,6 +74,8 @@ export interface DurableVerificationObservation {
|
|
|
68
74
|
failureIdentities: string[];
|
|
69
75
|
code?: string;
|
|
70
76
|
attributionReliable?: boolean;
|
|
77
|
+
/** Code-owned comparable verification obligation, excluding display identity. */
|
|
78
|
+
contractIdentity?: string;
|
|
71
79
|
}
|
|
72
80
|
|
|
73
81
|
export interface DurableVerificationBaseline {
|
|
@@ -89,7 +97,11 @@ export type DurableObservedVerification =
|
|
|
89
97
|
| {
|
|
90
98
|
ok: false;
|
|
91
99
|
outcome:
|
|
92
|
-
| {
|
|
100
|
+
| {
|
|
101
|
+
kind: "paused";
|
|
102
|
+
code: string;
|
|
103
|
+
prerequisite?: WorkflowVerificationPrerequisite;
|
|
104
|
+
}
|
|
93
105
|
| { kind: "approval-needed"; code: string }
|
|
94
106
|
| { kind: "operation-cancelled"; code: "cancelled" };
|
|
95
107
|
};
|
|
@@ -161,6 +173,8 @@ export interface DurableWorkflowEngineOptions {
|
|
|
161
173
|
leaseTtlMs?: number;
|
|
162
174
|
workHardLimit?: number;
|
|
163
175
|
verificationPolicy?: string;
|
|
176
|
+
/** Real process executors must settle cancellation before post-apply cleanup. */
|
|
177
|
+
awaitPostApplySettlement?: boolean;
|
|
164
178
|
verificationEnvironment?(
|
|
165
179
|
plan: ImplementPlan,
|
|
166
180
|
signal: AbortSignal,
|
|
@@ -175,11 +189,15 @@ export interface DurableExecutionResources {
|
|
|
175
189
|
artifacts: ArtifactStore;
|
|
176
190
|
workspaces: WorkspaceStore;
|
|
177
191
|
ledgers: Map<number, TaskLedger>;
|
|
192
|
+
/** Run-shared original-baseline observations, independent of delivery revision. */
|
|
193
|
+
baselineLedger?: TaskLedger;
|
|
178
194
|
baselineRevisionId: string;
|
|
195
|
+
/** Root capture before any authorized boundary expansion; immutable evidence source. */
|
|
196
|
+
originalBaselineRevisionId?: string;
|
|
179
197
|
currentRevisionId: string;
|
|
180
198
|
mergeTail: Promise<void>;
|
|
181
199
|
verificationFact?: object;
|
|
182
|
-
baselinePromises: Map<number, Promise<DurableBaselineResult>>;
|
|
200
|
+
baselinePromises: Map<string | number, Promise<DurableBaselineResult>>;
|
|
183
201
|
environmentIdentity?: string;
|
|
184
202
|
}
|
|
185
203
|
|
package/src/durable-workflow.ts
CHANGED
|
@@ -1,16 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ChangeVerification,
|
|
3
|
-
parseVerificationBaseline,
|
|
4
|
-
sha256Bytes,
|
|
5
|
-
verificationBaselineFact,
|
|
6
|
-
} from "./change-verification.ts";
|
|
1
|
+
import { ChangeVerification, sha256Bytes } from "./change-verification.ts";
|
|
7
2
|
import type {
|
|
8
3
|
DurableExecutionResources,
|
|
9
4
|
DurablePhaseFact,
|
|
10
5
|
DurableResourceInput,
|
|
11
6
|
DurableWorkflowEngineOptions,
|
|
12
7
|
} from "./durable-contracts.ts";
|
|
8
|
+
import { assertExecutionsSettled } from "./execution-retention.ts";
|
|
13
9
|
import { PhaseExecution } from "./phase-execution.ts";
|
|
10
|
+
import { removePathSync } from "./remove-path.ts";
|
|
14
11
|
|
|
15
12
|
export type {
|
|
16
13
|
DurableChangeVerificationResult,
|
|
@@ -23,7 +20,7 @@ interface DurableRunResources extends DurableExecutionResources {
|
|
|
23
20
|
closed: boolean;
|
|
24
21
|
}
|
|
25
22
|
|
|
26
|
-
import { lstatSync, mkdirSync, readFileSync
|
|
23
|
+
import { lstatSync, mkdirSync, readFileSync } from "node:fs";
|
|
27
24
|
import path from "node:path";
|
|
28
25
|
import { DatabaseSync } from "node:sqlite";
|
|
29
26
|
import { ApplyTransaction } from "./apply-transaction.ts";
|
|
@@ -281,6 +278,7 @@ class DurableWorkflowComposition
|
|
|
281
278
|
signal: AbortSignal,
|
|
282
279
|
): Promise<DurableRunResources> {
|
|
283
280
|
signal.throwIfAborted();
|
|
281
|
+
assertExecutionsSettled(this.#options.stateRoot.rootDir);
|
|
284
282
|
const pending = this.#initializing.get(input.runId);
|
|
285
283
|
if (pending) return pending;
|
|
286
284
|
if (
|
|
@@ -330,6 +328,7 @@ class DurableWorkflowComposition
|
|
|
330
328
|
baselineRevisionId?: string;
|
|
331
329
|
currentWorkspaceRevisionId?: string;
|
|
332
330
|
}): DurableRunResources {
|
|
331
|
+
assertExecutionsSettled(this.#options.stateRoot.rootDir);
|
|
333
332
|
const existing = this.#runs.get(input.runId);
|
|
334
333
|
if (existing) {
|
|
335
334
|
if (
|
|
@@ -358,7 +357,16 @@ class DurableWorkflowComposition
|
|
|
358
357
|
const currentRevisionId = input.currentWorkspaceRevisionId;
|
|
359
358
|
if (!baselineRevisionId || !currentRevisionId)
|
|
360
359
|
throw new Error("workflow-workspace-revision-unavailable");
|
|
361
|
-
workspaces.getRevision(baselineRevisionId);
|
|
360
|
+
let originalBaseline = workspaces.getRevision(baselineRevisionId);
|
|
361
|
+
const ancestors = new Set<string>();
|
|
362
|
+
while (originalBaseline.parentRevisionId !== null) {
|
|
363
|
+
if (ancestors.has(originalBaseline.revisionId))
|
|
364
|
+
throw new Error("workflow-baseline-revision-conflict");
|
|
365
|
+
ancestors.add(originalBaseline.revisionId);
|
|
366
|
+
originalBaseline = workspaces.getRevision(
|
|
367
|
+
originalBaseline.parentRevisionId,
|
|
368
|
+
);
|
|
369
|
+
}
|
|
362
370
|
workspaces.getRevision(currentRevisionId);
|
|
363
371
|
let resources!: DurableRunResources;
|
|
364
372
|
const transactions = new ApplyTransaction({
|
|
@@ -366,6 +374,7 @@ class DurableWorkflowComposition
|
|
|
366
374
|
artifacts,
|
|
367
375
|
workspaces,
|
|
368
376
|
hooks: {
|
|
377
|
+
awaitPostApplySettlement: this.#options.awaitPostApplySettlement,
|
|
369
378
|
postApply: ({ transactionId, root: verificationRoot, signal }) =>
|
|
370
379
|
this.#verification.verifyPostApply(
|
|
371
380
|
resources,
|
|
@@ -385,11 +394,13 @@ class DurableWorkflowComposition
|
|
|
385
394
|
ledgers: new Map(),
|
|
386
395
|
transactions,
|
|
387
396
|
baselineRevisionId,
|
|
397
|
+
originalBaselineRevisionId: originalBaseline.revisionId,
|
|
388
398
|
currentRevisionId,
|
|
389
399
|
mergeTail: Promise.resolve(),
|
|
390
400
|
baselinePromises: new Map(),
|
|
391
401
|
closed: false,
|
|
392
402
|
};
|
|
403
|
+
resources.baselineLedger = this.#ledger(resources, 1);
|
|
393
404
|
this.#runs.set(input.runId, resources);
|
|
394
405
|
return resources;
|
|
395
406
|
}
|
|
@@ -729,37 +740,10 @@ class DurableWorkflowComposition
|
|
|
729
740
|
}
|
|
730
741
|
if (!grew) break;
|
|
731
742
|
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
.map((task) => task.deliveryRevision),
|
|
737
|
-
);
|
|
738
|
-
for (const deliveryRevision of retainedRevisions) {
|
|
739
|
-
const ledger = this.#ledger(resources, deliveryRevision);
|
|
740
|
-
const stored = ledger.durableFact(
|
|
741
|
-
this.#verification.baselineFactKey(resources),
|
|
742
|
-
);
|
|
743
|
-
resources.baselinePromises.delete(deliveryRevision);
|
|
744
|
-
if (stored === undefined) continue;
|
|
745
|
-
const baseline = parseVerificationBaseline(stored, resources.artifacts);
|
|
746
|
-
if (baseline.revisionId === expandedBaseline.revisionId) continue;
|
|
747
|
-
if (baseline.revisionId !== input.baselineRevisionId) {
|
|
748
|
-
throw new Error("workflow-verification-baseline-conflict");
|
|
749
|
-
}
|
|
750
|
-
ledger.replaceDurableFact(
|
|
751
|
-
this.#verification.baselineFactKey(resources),
|
|
752
|
-
stored,
|
|
753
|
-
verificationBaselineFact(
|
|
754
|
-
{
|
|
755
|
-
...baseline,
|
|
756
|
-
revisionId: expandedBaseline.revisionId,
|
|
757
|
-
},
|
|
758
|
-
resources.artifacts,
|
|
759
|
-
),
|
|
760
|
-
);
|
|
761
|
-
}
|
|
762
|
-
}
|
|
743
|
+
// Boundary expansion changes candidate/apply inputs, never the root capture
|
|
744
|
+
// or its verification observations. Their contract-bound caches revalidate
|
|
745
|
+
// only the obligations changed by this delivery.
|
|
746
|
+
resources.baselinePromises.clear();
|
|
763
747
|
resources.baselineRevisionId = expandedBaseline.revisionId;
|
|
764
748
|
resources.currentRevisionId = rebuilt.revisionId;
|
|
765
749
|
if (hasVerificationLifecycle(input.plan)) {
|
|
@@ -914,12 +898,13 @@ class DurableWorkflowComposition
|
|
|
914
898
|
}
|
|
915
899
|
|
|
916
900
|
cleanup(runId: string): void {
|
|
901
|
+
assertExecutionsSettled(this.#options.stateRoot.rootDir, true);
|
|
917
902
|
const resources = this.#runs.get(runId);
|
|
918
903
|
if (resources) {
|
|
919
904
|
this.#closeResources(resources);
|
|
920
905
|
this.#runs.delete(runId);
|
|
921
906
|
}
|
|
922
|
-
|
|
907
|
+
removePathSync(this.#runRoot(runId), { recursive: true, force: true });
|
|
923
908
|
}
|
|
924
909
|
|
|
925
910
|
close(): void {
|
|
@@ -932,6 +917,36 @@ class DurableWorkflowComposition
|
|
|
932
917
|
runAttempt(input: Parameters<WorkflowWorker["runAttempt"]>[0]) {
|
|
933
918
|
return this.#phase.runAttempt(input);
|
|
934
919
|
}
|
|
920
|
+
|
|
921
|
+
async prepareTask(input: Parameters<WorkflowWorker["runAttempt"]>[0]) {
|
|
922
|
+
const resources = await this.#prepareResources(input, input.signal);
|
|
923
|
+
const workspace = {
|
|
924
|
+
baselineRevisionId: resources.baselineRevisionId,
|
|
925
|
+
currentWorkspaceRevisionId: resources.currentRevisionId,
|
|
926
|
+
};
|
|
927
|
+
if (hasVerificationLifecycle(input.plan)) {
|
|
928
|
+
const captured = await this.#verification.ensureVerificationBaseline({
|
|
929
|
+
resources,
|
|
930
|
+
ledger: this.#ledger(resources, input.deliveryRevision),
|
|
931
|
+
taskId: input.taskId,
|
|
932
|
+
signal: input.signal,
|
|
933
|
+
});
|
|
934
|
+
if (!captured.ok)
|
|
935
|
+
return {
|
|
936
|
+
...captured.outcome,
|
|
937
|
+
...workspace,
|
|
938
|
+
...(captured.outcome.kind === "paused"
|
|
939
|
+
? {
|
|
940
|
+
verification: {
|
|
941
|
+
scope: "baseline" as const,
|
|
942
|
+
attribution: "environment" as const,
|
|
943
|
+
},
|
|
944
|
+
}
|
|
945
|
+
: {}),
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
return { kind: "prepared" as const, ...workspace };
|
|
949
|
+
}
|
|
935
950
|
hasPendingVerification(input: Parameters<WorkflowWorker["runAttempt"]>[0]) {
|
|
936
951
|
return this.#phase.hasPendingVerification(input);
|
|
937
952
|
}
|
package/src/execution-profile.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
|
|
1
4
|
export interface ExecutionProfile {
|
|
2
|
-
mode: "isolated" | "local-trusted";
|
|
5
|
+
mode: "isolated" | "local-trusted" | "host-trusted";
|
|
3
6
|
timeoutMs: number;
|
|
4
7
|
bwrapPath: string;
|
|
5
8
|
inheritEnvironment: string[];
|
|
9
|
+
windowsHelperPath?: string;
|
|
6
10
|
}
|
|
7
11
|
|
|
8
12
|
/** Operator configuration only. Unknown modes never silently disable isolation. */
|
|
@@ -10,7 +14,11 @@ export function executionProfile(
|
|
|
10
14
|
env: NodeJS.ProcessEnv = process.env,
|
|
11
15
|
): ExecutionProfile {
|
|
12
16
|
const mode = env.ABEL_EXECUTION_MODE ?? "isolated";
|
|
13
|
-
if (
|
|
17
|
+
if (
|
|
18
|
+
mode !== "isolated" &&
|
|
19
|
+
mode !== "local-trusted" &&
|
|
20
|
+
mode !== "host-trusted"
|
|
21
|
+
)
|
|
14
22
|
throw new Error("execution-mode-invalid");
|
|
15
23
|
const timeoutMs = Number(env.ABEL_VERIFICATION_TIMEOUT_MS ?? 600_000);
|
|
16
24
|
if (
|
|
@@ -29,10 +37,28 @@ export function executionProfile(
|
|
|
29
37
|
throw new Error("verification-environment-names-invalid");
|
|
30
38
|
if (mode === "isolated" && inheritEnvironment.length)
|
|
31
39
|
throw new Error("verification-environment-requires-trusted-mode");
|
|
40
|
+
if (
|
|
41
|
+
mode === "host-trusted" &&
|
|
42
|
+
env.ABEL_WINDOWS_JOB_HELPER !== undefined &&
|
|
43
|
+
(!env.ABEL_WINDOWS_JOB_HELPER ||
|
|
44
|
+
env.ABEL_WINDOWS_JOB_HELPER.includes("\0") ||
|
|
45
|
+
!(
|
|
46
|
+
path.isAbsolute(env.ABEL_WINDOWS_JOB_HELPER) ||
|
|
47
|
+
path.win32.isAbsolute(env.ABEL_WINDOWS_JOB_HELPER)
|
|
48
|
+
))
|
|
49
|
+
)
|
|
50
|
+
throw new Error("windows-job-helper-path-invalid");
|
|
32
51
|
return {
|
|
33
52
|
mode,
|
|
34
53
|
timeoutMs,
|
|
35
54
|
bwrapPath: env.ABEL_BWRAP_PATH ?? "/usr/bin/bwrap",
|
|
36
55
|
inheritEnvironment,
|
|
56
|
+
...(mode === "host-trusted"
|
|
57
|
+
? {
|
|
58
|
+
windowsHelperPath:
|
|
59
|
+
env.ABEL_WINDOWS_JOB_HELPER ??
|
|
60
|
+
fileURLToPath(new URL("./windows-job.exe", import.meta.url)),
|
|
61
|
+
}
|
|
62
|
+
: {}),
|
|
37
63
|
};
|
|
38
64
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
closeSync,
|
|
4
|
+
fsyncSync,
|
|
5
|
+
lstatSync,
|
|
6
|
+
openSync,
|
|
7
|
+
readdirSync,
|
|
8
|
+
unlinkSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { syncDirectory } from "./directory-sync.ts";
|
|
13
|
+
|
|
14
|
+
const prefix = ".cadence-execution-";
|
|
15
|
+
const active = new Set<string>();
|
|
16
|
+
const retained = new Map<string, readonly string[]>();
|
|
17
|
+
/** Written before launching a host process. Absence of a settlement receipt
|
|
18
|
+
* survives host loss; reopening must not infer ownership from a recycled PID. */
|
|
19
|
+
export function retainExecution(ownerRoot: string, roots: readonly string[]) {
|
|
20
|
+
assertExecutionsSettled(ownerRoot);
|
|
21
|
+
const file = path.join(ownerRoot, `${prefix}${randomUUID()}.json`);
|
|
22
|
+
const fd = openSync(file, "wx", 0o600);
|
|
23
|
+
try {
|
|
24
|
+
writeFileSync(
|
|
25
|
+
fd,
|
|
26
|
+
JSON.stringify({
|
|
27
|
+
version: 1,
|
|
28
|
+
roots: roots.map((root) => path.resolve(root)),
|
|
29
|
+
}),
|
|
30
|
+
);
|
|
31
|
+
fsyncSync(fd);
|
|
32
|
+
} finally {
|
|
33
|
+
closeSync(fd);
|
|
34
|
+
}
|
|
35
|
+
syncDirectory(ownerRoot);
|
|
36
|
+
active.add(file);
|
|
37
|
+
retained.set(file, roots);
|
|
38
|
+
return {
|
|
39
|
+
settled() {
|
|
40
|
+
unlinkSync(file);
|
|
41
|
+
syncDirectory(ownerRoot);
|
|
42
|
+
active.delete(file);
|
|
43
|
+
retained.delete(file);
|
|
44
|
+
},
|
|
45
|
+
uncertain() {
|
|
46
|
+
active.delete(file);
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export function assertExecutionsSettled(
|
|
51
|
+
ownerRoot: string,
|
|
52
|
+
includeActive = false,
|
|
53
|
+
): void {
|
|
54
|
+
if (!lstatSync(ownerRoot, { throwIfNoEntry: false })) return;
|
|
55
|
+
if (
|
|
56
|
+
readdirSync(ownerRoot).some(
|
|
57
|
+
(name) =>
|
|
58
|
+
name.startsWith(prefix) &&
|
|
59
|
+
(includeActive || !active.has(path.join(ownerRoot, name))),
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
throw new Error("isolation-termination-unconfirmed");
|
|
63
|
+
}
|
|
64
|
+
export function isExecutionRetained(root: string): boolean {
|
|
65
|
+
const resolved = path.resolve(root);
|
|
66
|
+
return [...retained.values()].some((roots) =>
|
|
67
|
+
roots.some((candidate) => {
|
|
68
|
+
const relative = path.relative(resolved, path.resolve(candidate));
|
|
69
|
+
return (
|
|
70
|
+
relative === "" ||
|
|
71
|
+
(relative !== ".." &&
|
|
72
|
+
!relative.startsWith(`..${path.sep}`) &&
|
|
73
|
+
!path.isAbsolute(relative))
|
|
74
|
+
);
|
|
75
|
+
}),
|
|
76
|
+
);
|
|
77
|
+
}
|