@abelxiaoxing/cadence 1.8.3 → 2.0.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 (208) hide show
  1. package/README.md +324 -111
  2. package/THIRD_PARTY_NOTICES.md +15 -0
  3. package/agents/design-explorer.md +14 -26
  4. package/agents/diagnosis-worker.md +14 -25
  5. package/agents/implementation-worker.md +16 -35
  6. package/config/.env.example +2 -5
  7. package/config/workflow-task.example.json +7 -0
  8. package/docs/remote-access.md +60 -0
  9. package/docs/reuse-and-distribution.md +44 -0
  10. package/docs/service-management.md +108 -0
  11. package/licenses/bundled-web.txt +6245 -0
  12. package/licenses/pi-web-ui-MIT.txt +21 -0
  13. package/licenses/pi-web-ui-reuse.md +22 -0
  14. package/package.json +94 -21
  15. package/src/index.ts +22 -1807
  16. package/src/native-build.ts +6 -0
  17. package/src/native-client.ts +103 -0
  18. package/src/native-export.ts +58 -0
  19. package/src/native-history.ts +359 -0
  20. package/src/operator-cli.mjs +453 -1937
  21. package/src/plan-proposal.ts +87 -0
  22. package/src/sdk-worker.mjs +549 -0
  23. package/src/service/contracts/active.ts +82 -0
  24. package/src/service/contracts/budget.ts +148 -0
  25. package/src/service/contracts/build.ts +67 -0
  26. package/src/service/contracts/capture.ts +48 -0
  27. package/src/service/contracts/core.ts +33 -0
  28. package/src/service/contracts/current.ts +261 -0
  29. package/src/service/contracts/domain.ts +68 -0
  30. package/src/service/contracts/history.ts +2245 -0
  31. package/src/service/contracts/models.ts +139 -0
  32. package/src/service/coordination.ts +862 -0
  33. package/src/service/private-files.ts +204 -0
  34. package/src/service-cli.mjs +5592 -0
  35. package/src/service-client.ts +747 -0
  36. package/src/service-contract.ts +245 -0
  37. package/src/service-host.mjs +22382 -0
  38. package/src/service-http-client.ts +425 -0
  39. package/src/service-wire.ts +372 -0
  40. package/src/task-shell.ts +1045 -0
  41. package/src/task-store.ts +479 -0
  42. package/src/task-types.ts +184 -0
  43. package/src/verification-diagnostics.ts +29 -72
  44. package/src/worker-capture.mjs +1197 -0
  45. package/src/worker-public.mjs +1753 -0
  46. package/src/worker-redaction.mjs +608 -0
  47. package/src/worker-tools.mjs +202 -0
  48. package/web/dist/assets/KaTeX_AMS-Regular.ttf +0 -0
  49. package/web/dist/assets/KaTeX_AMS-Regular.woff +0 -0
  50. package/web/dist/assets/KaTeX_AMS-Regular.woff2 +0 -0
  51. package/web/dist/assets/KaTeX_Caligraphic-Bold.ttf +0 -0
  52. package/web/dist/assets/KaTeX_Caligraphic-Bold.woff +0 -0
  53. package/web/dist/assets/KaTeX_Caligraphic-Bold.woff2 +0 -0
  54. package/web/dist/assets/KaTeX_Caligraphic-Regular.ttf +0 -0
  55. package/web/dist/assets/KaTeX_Caligraphic-Regular.woff +0 -0
  56. package/web/dist/assets/KaTeX_Caligraphic-Regular.woff2 +0 -0
  57. package/web/dist/assets/KaTeX_Fraktur-Bold.ttf +0 -0
  58. package/web/dist/assets/KaTeX_Fraktur-Bold.woff +0 -0
  59. package/web/dist/assets/KaTeX_Fraktur-Bold.woff2 +0 -0
  60. package/web/dist/assets/KaTeX_Fraktur-Regular.ttf +0 -0
  61. package/web/dist/assets/KaTeX_Fraktur-Regular.woff +0 -0
  62. package/web/dist/assets/KaTeX_Fraktur-Regular.woff2 +0 -0
  63. package/web/dist/assets/KaTeX_Main-Bold.ttf +0 -0
  64. package/web/dist/assets/KaTeX_Main-Bold.woff +0 -0
  65. package/web/dist/assets/KaTeX_Main-Bold.woff2 +0 -0
  66. package/web/dist/assets/KaTeX_Main-BoldItalic.ttf +0 -0
  67. package/web/dist/assets/KaTeX_Main-BoldItalic.woff +0 -0
  68. package/web/dist/assets/KaTeX_Main-BoldItalic.woff2 +0 -0
  69. package/web/dist/assets/KaTeX_Main-Italic.ttf +0 -0
  70. package/web/dist/assets/KaTeX_Main-Italic.woff +0 -0
  71. package/web/dist/assets/KaTeX_Main-Italic.woff2 +0 -0
  72. package/web/dist/assets/KaTeX_Main-Regular.ttf +0 -0
  73. package/web/dist/assets/KaTeX_Main-Regular.woff +0 -0
  74. package/web/dist/assets/KaTeX_Main-Regular.woff2 +0 -0
  75. package/web/dist/assets/KaTeX_Math-BoldItalic.ttf +0 -0
  76. package/web/dist/assets/KaTeX_Math-BoldItalic.woff +0 -0
  77. package/web/dist/assets/KaTeX_Math-BoldItalic.woff2 +0 -0
  78. package/web/dist/assets/KaTeX_Math-Italic.ttf +0 -0
  79. package/web/dist/assets/KaTeX_Math-Italic.woff +0 -0
  80. package/web/dist/assets/KaTeX_Math-Italic.woff2 +0 -0
  81. package/web/dist/assets/KaTeX_SansSerif-Bold.ttf +0 -0
  82. package/web/dist/assets/KaTeX_SansSerif-Bold.woff +0 -0
  83. package/web/dist/assets/KaTeX_SansSerif-Bold.woff2 +0 -0
  84. package/web/dist/assets/KaTeX_SansSerif-Italic.ttf +0 -0
  85. package/web/dist/assets/KaTeX_SansSerif-Italic.woff +0 -0
  86. package/web/dist/assets/KaTeX_SansSerif-Italic.woff2 +0 -0
  87. package/web/dist/assets/KaTeX_SansSerif-Regular.ttf +0 -0
  88. package/web/dist/assets/KaTeX_SansSerif-Regular.woff +0 -0
  89. package/web/dist/assets/KaTeX_SansSerif-Regular.woff2 +0 -0
  90. package/web/dist/assets/KaTeX_Script-Regular.ttf +0 -0
  91. package/web/dist/assets/KaTeX_Script-Regular.woff +0 -0
  92. package/web/dist/assets/KaTeX_Script-Regular.woff2 +0 -0
  93. package/web/dist/assets/KaTeX_Size1-Regular.ttf +0 -0
  94. package/web/dist/assets/KaTeX_Size1-Regular.woff +0 -0
  95. package/web/dist/assets/KaTeX_Size1-Regular.woff2 +0 -0
  96. package/web/dist/assets/KaTeX_Size2-Regular.ttf +0 -0
  97. package/web/dist/assets/KaTeX_Size2-Regular.woff +0 -0
  98. package/web/dist/assets/KaTeX_Size2-Regular.woff2 +0 -0
  99. package/web/dist/assets/KaTeX_Size3-Regular.ttf +0 -0
  100. package/web/dist/assets/KaTeX_Size3-Regular.woff +0 -0
  101. package/web/dist/assets/KaTeX_Size3-Regular.woff2 +0 -0
  102. package/web/dist/assets/KaTeX_Size4-Regular.ttf +0 -0
  103. package/web/dist/assets/KaTeX_Size4-Regular.woff +0 -0
  104. package/web/dist/assets/KaTeX_Size4-Regular.woff2 +0 -0
  105. package/web/dist/assets/KaTeX_Typewriter-Regular.ttf +0 -0
  106. package/web/dist/assets/KaTeX_Typewriter-Regular.woff +0 -0
  107. package/web/dist/assets/KaTeX_Typewriter-Regular.woff2 +0 -0
  108. package/web/dist/assets/cadence.css +2 -0
  109. package/web/dist/assets/cadence.js +7 -0
  110. package/web/dist/assets/markdown.js +296 -0
  111. package/web/dist/assets/react.js +9 -0
  112. package/web/dist/assets/rolldown-runtime.js +1 -0
  113. package/web/dist/assets/timeline-client.js +41 -0
  114. package/web/dist/assets/vis-timeline.css +1274 -0
  115. package/web/dist/assets/vis-timeline.js +32 -0
  116. package/web/dist/index.html +18 -0
  117. package/web/dist/manifest.json +505 -0
  118. package/config/plan-draft.example.json +0 -125
  119. package/config/plan-draft.multiple-tasks.example.json +0 -375
  120. package/config/plan-draft.quick.example.json +0 -78
  121. package/config/routes.example.json +0 -39
  122. package/prompts/abel-design.md +0 -382
  123. package/prompts/abel-diagnose.md +0 -71
  124. package/prompts/abel-implement.md +0 -284
  125. package/prompts/abel-init.md +0 -52
  126. package/src/activation.ts +0 -44
  127. package/src/activity-contracts.ts +0 -82
  128. package/src/agent-registry.ts +0 -53
  129. package/src/apply-transaction.ts +0 -1437
  130. package/src/artifact-store.ts +0 -213
  131. package/src/build-windows-job.mjs +0 -95
  132. package/src/candidate-artifact.ts +0 -143
  133. package/src/candidate-patch.ts +0 -271
  134. package/src/candidate-workspace.ts +0 -80
  135. package/src/canonical.ts +0 -16
  136. package/src/change-contract-schema.ts +0 -191
  137. package/src/change-contract.ts +0 -324
  138. package/src/change-verification.ts +0 -1617
  139. package/src/child-budget.ts +0 -58
  140. package/src/child-model.ts +0 -142
  141. package/src/child-session.ts +0 -640
  142. package/src/contracts.ts +0 -2476
  143. package/src/control-contracts.ts +0 -382
  144. package/src/delivery-compiler.ts +0 -1805
  145. package/src/delivery-revision.ts +0 -42
  146. package/src/design-control.ts +0 -1299
  147. package/src/design-diagnostics.ts +0 -181
  148. package/src/design-journal.ts +0 -1773
  149. package/src/directory-sync.ts +0 -36
  150. package/src/durable-contracts.ts +0 -231
  151. package/src/durable-workflow.ts +0 -978
  152. package/src/evidence-draft.ts +0 -122
  153. package/src/execution-profile.ts +0 -64
  154. package/src/execution-retention.ts +0 -77
  155. package/src/file-snapshot.ts +0 -163
  156. package/src/implement-continuation.ts +0 -559
  157. package/src/implement-graph.ts +0 -690
  158. package/src/implement-plan.ts +0 -72
  159. package/src/install-config.d.mts +0 -6
  160. package/src/isolation-backend.ts +0 -353
  161. package/src/model-source.ts +0 -22
  162. package/src/openspec-cli.ts +0 -494
  163. package/src/operator-cli-entry.mjs +0 -24
  164. package/src/operator-tools.ts +0 -309
  165. package/src/package-candidate.ts +0 -227
  166. package/src/package-delivery.ts +0 -486
  167. package/src/package-state.ts +0 -320
  168. package/src/package-verification.ts +0 -635
  169. package/src/package-workflow.ts +0 -342
  170. package/src/packet-runtime.ts +0 -570
  171. package/src/parent-provider.ts +0 -375
  172. package/src/phase-execution.ts +0 -2259
  173. package/src/pi-adapter.ts +0 -19
  174. package/src/plan-draft-summary.ts +0 -527
  175. package/src/plan-draft.ts +0 -502
  176. package/src/remove-path.ts +0 -32
  177. package/src/route-policy.ts +0 -545
  178. package/src/run-state.ts +0 -420
  179. package/src/run-store.ts +0 -1179
  180. package/src/safe-path.ts +0 -77
  181. package/src/scoped-grep-worker.mjs +0 -23
  182. package/src/scoped-tools.ts +0 -775
  183. package/src/single-task-draft.ts +0 -142
  184. package/src/sqlite-schema.ts +0 -171
  185. package/src/state-root.ts +0 -215
  186. package/src/storage-schema.ts +0 -360
  187. package/src/subagent-activity.ts +0 -1417
  188. package/src/submit-tool.ts +0 -950
  189. package/src/task-ledger.ts +0 -1338
  190. package/src/transport-budget.ts +0 -52
  191. package/src/verification-capability.ts +0 -1191
  192. package/src/verification-environment-io.ts +0 -389
  193. package/src/verification-environment.ts +0 -162
  194. package/src/verification-identity.ts +0 -77
  195. package/src/verification-scan.ts +0 -64
  196. package/src/windows-job-backend.ts +0 -419
  197. package/src/windows-job.c +0 -149
  198. package/src/worker-broker.ts +0 -806
  199. package/src/workflow-engine.ts +0 -28
  200. package/src/workflow-policy.ts +0 -1679
  201. package/src/workflow-recovery-policy.ts +0 -97
  202. package/src/workflow-scheduling.ts +0 -105
  203. package/src/workflow-state-machine.ts +0 -4355
  204. package/src/workflow-status.ts +0 -576
  205. package/src/workspace-io-entry.mjs +0 -76
  206. package/src/workspace-io-worker.mjs +0 -1312
  207. package/src/workspace-io.ts +0 -89
  208. package/src/workspace-store.ts +0 -634
@@ -1,1617 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import { mkdtempSync } from "node:fs";
3
- import path from "node:path";
4
- import { verifyCumulativeRevision } from "./apply-transaction.ts";
5
- import type { ArtifactStore } from "./artifact-store.ts";
6
- import { canonicalJson } from "./canonical.ts";
7
- import {
8
- type StructuredVerificationContract,
9
- verificationInputPaths,
10
- } from "./contracts.ts";
11
- import type { PlanTaskDraft } from "./delivery-compiler.ts";
12
- import type {
13
- DurableAffectedResult,
14
- DurableBaselineResult,
15
- DurableChangeVerificationResult,
16
- DurableExecutionResources,
17
- DurableObservedVerification,
18
- DurablePhaseVerificationResult,
19
- DurableResourceInput,
20
- DurableVerificationBaseline,
21
- DurableVerificationObservation,
22
- DurableWorkflowEngineOptions,
23
- } from "./durable-contracts.ts";
24
- import { isExecutionRetained } from "./execution-retention.ts";
25
- import { removePathSync } from "./remove-path.ts";
26
-
27
- import { observeSafePath } from "./safe-path.ts";
28
-
29
- import type { TaskLedger } from "./task-ledger.ts";
30
- import {
31
- baselineRecoveryKey,
32
- type DurableVerificationScope,
33
- hash,
34
- hasVerificationLifecycle,
35
- IDENTIFIER,
36
- isRecord,
37
- SHA256,
38
- type WorkflowAttemptOutcome,
39
- type WorkflowChangeVerifier,
40
- type WorkflowVerificationPrerequisite,
41
- } from "./workflow-policy.ts";
42
- import type { WorkspaceRevision } from "./workspace-store.ts";
43
- export function sha256Bytes(bytes: Uint8Array): string {
44
- return createHash("sha256").update(bytes).digest("hex");
45
- }
46
-
47
- function verificationContractIdentity(
48
- verification: StructuredVerificationContract,
49
- ): string {
50
- const executionSemantics = (
51
- contract: StructuredVerificationContract,
52
- ): unknown => {
53
- if (contract.kind === "steps") {
54
- return {
55
- kind: contract.kind,
56
- steps: contract.steps.map((step) => executionSemantics(step)),
57
- };
58
- }
59
- const {
60
- id: _id,
61
- classification: _classification,
62
- expectedFailure: _expectedFailure,
63
- ...semantics
64
- } = contract;
65
- return semantics;
66
- };
67
- return hash(
68
- "verification-contract-v1",
69
- canonicalJson(executionSemantics(verification)),
70
- );
71
- }
72
-
73
- const BASELINE_CAPABILITY_CODES = new Set([
74
- "dependency-path-unsafe",
75
- "isolation-backend-launch-failed",
76
- "isolation-backend-unavailable",
77
- "isolation-termination-unconfirmed",
78
- "local-executable-missing",
79
- "runner-missing",
80
- "sandbox-runtime-unavailable",
81
- "verification-environment-changed",
82
- "verification-environment-requires-trusted-mode",
83
- "verification-environment-unavailable",
84
- "verification-runner-launch-failed",
85
- "workspace-dependency-missing",
86
- ]);
87
-
88
- function hasExactKeys(
89
- value: Record<string, unknown>,
90
- required: readonly string[],
91
- optional: readonly string[] = [],
92
- ): boolean {
93
- const keys = Object.keys(value);
94
- const allowed = new Set([...required, ...optional]);
95
- return (
96
- required.every((key) => Object.hasOwn(value, key)) &&
97
- keys.every((key) => allowed.has(key))
98
- );
99
- }
100
-
101
- function verificationIdentities(
102
- result: DurableChangeVerificationResult,
103
- contractIdentity: string,
104
- ): string[] {
105
- // Keep the complete comparison set; only Worker/status summaries are bounded.
106
- const supplied = result.failureIdentities;
107
- if (
108
- Array.isArray(supplied) &&
109
- supplied.every(
110
- (identity) => typeof identity === "string" && SHA256.test(identity),
111
- )
112
- ) {
113
- return [...new Set(supplied)].sort();
114
- }
115
- if (result.ok) return [];
116
- return [hash("verification-failure-v2", contractIdentity, result.code)];
117
- }
118
- export function verificationBaselineFact(
119
- baseline: DurableVerificationBaseline,
120
- artifacts: ArtifactStore,
121
- ) {
122
- // Complete private evidence must not inherit the bounded model projection limits.
123
- const artifact = artifacts.put(Buffer.from(JSON.stringify(baseline)));
124
- return {
125
- revisionId: baseline.revisionId,
126
- baselineArtifactHash: artifact.hash,
127
- };
128
- }
129
- export function parseVerificationBaseline(
130
- value: unknown,
131
- artifacts: ArtifactStore,
132
- ): DurableVerificationBaseline {
133
- if (isRecord(value) && typeof value.baselineArtifactHash === "string") {
134
- const revisionId = value.revisionId;
135
- value = JSON.parse(
136
- Buffer.from(artifacts.read(value.baselineArtifactHash)).toString("utf8"),
137
- );
138
- if (!isRecord(value) || value.revisionId !== revisionId)
139
- throw new Error("workflow-verification-baseline-conflict");
140
- }
141
- if (
142
- !isRecord(value) ||
143
- typeof value.revisionId !== "string" ||
144
- !SHA256.test(value.revisionId) ||
145
- !Array.isArray(value.targetContracts) ||
146
- !Array.isArray(value.affected) ||
147
- !isRecord(value.fullSuite)
148
- ) {
149
- throw new Error("workflow-verification-baseline-invalid");
150
- }
151
- const baseline = structuredClone(
152
- value,
153
- ) as unknown as DurableVerificationBaseline;
154
- const validObservation = (observation: DurableVerificationObservation) =>
155
- (observation.status === "passed" || observation.status === "failed") &&
156
- typeof observation.verificationId === "string" &&
157
- IDENTIFIER.test(observation.verificationId) &&
158
- Array.isArray(observation.failureIdentities) &&
159
- observation.failureIdentities.every((identity) => SHA256.test(identity)) &&
160
- (observation.code === undefined || typeof observation.code === "string") &&
161
- (observation.attributionReliable === undefined ||
162
- typeof observation.attributionReliable === "boolean") &&
163
- (observation.contractIdentity === undefined ||
164
- (typeof observation.contractIdentity === "string" &&
165
- SHA256.test(observation.contractIdentity)));
166
- if (
167
- !baseline.targetContracts.every(
168
- (entry) =>
169
- isRecord(entry) &&
170
- typeof entry.taskId === "string" &&
171
- typeof entry.verificationId === "string" &&
172
- typeof entry.expectedFailure === "string",
173
- ) ||
174
- !baseline.affected.every(
175
- (entry) =>
176
- isRecord(entry) &&
177
- typeof entry.taskId === "string" &&
178
- isRecord(entry.observation) &&
179
- validObservation(entry.observation),
180
- ) ||
181
- !validObservation(baseline.fullSuite)
182
- ) {
183
- throw new Error("workflow-verification-baseline-invalid");
184
- }
185
- return baseline;
186
- }
187
-
188
- export interface ChangeVerificationServices {
189
- prepareResources(
190
- input: DurableResourceInput,
191
- signal: AbortSignal,
192
- ): Promise<DurableExecutionResources>;
193
- ledger(
194
- resources: DurableExecutionResources,
195
- deliveryRevision: number,
196
- ): TaskLedger;
197
- revalidatePhasePolicy(
198
- resources: DurableExecutionResources,
199
- ledger: TaskLedger,
200
- task: PlanTaskDraft,
201
- signal: AbortSignal,
202
- ): Promise<WorkflowAttemptOutcome | undefined>;
203
- run(runId: string): DurableExecutionResources | undefined;
204
- }
205
- export class ChangeVerification {
206
- readonly #services: ChangeVerificationServices;
207
- readonly #options: Pick<
208
- DurableWorkflowEngineOptions,
209
- | "verificationEnvironment"
210
- | "verifyPhase"
211
- | "verifyChange"
212
- | "verificationPolicy"
213
- >;
214
- constructor(
215
- options: Pick<
216
- DurableWorkflowEngineOptions,
217
- | "verificationEnvironment"
218
- | "verifyPhase"
219
- | "verifyChange"
220
- | "verificationPolicy"
221
- >,
222
- services: ChangeVerificationServices,
223
- ) {
224
- this.#options = options;
225
- this.#services = services;
226
- }
227
-
228
- #originalBaselineRevisionId(resources: DurableExecutionResources): string {
229
- return (
230
- (
231
- resources as DurableExecutionResources & {
232
- originalBaselineRevisionId?: string;
233
- }
234
- ).originalBaselineRevisionId ?? resources.baselineRevisionId
235
- );
236
- }
237
-
238
- #observedEnvironmentIdentity(resources: DurableExecutionResources): string {
239
- const identity = resources.environmentIdentity;
240
- if (!identity) return "unobserved";
241
- return SHA256.test(identity)
242
- ? identity
243
- : hash("verification-environment-identity-v1", identity);
244
- }
245
-
246
- #baselinePrerequisite(input: {
247
- resources: DurableExecutionResources;
248
- root: string;
249
- revisionId: string;
250
- scope: "baseline-task-affected" | "baseline-full-suite";
251
- verification: StructuredVerificationContract;
252
- result: Extract<DurableChangeVerificationResult, { ok: false }>;
253
- taskId?: string;
254
- }): WorkflowVerificationPrerequisite {
255
- const originalRevisionId = this.#originalBaselineRevisionId(
256
- input.resources,
257
- );
258
- if (input.revisionId !== originalRevisionId) {
259
- throw new Error("workflow-verification-baseline-conflict");
260
- }
261
- const base = {
262
- kind: "verification-prerequisite" as const,
263
- scope: input.scope,
264
- verificationId: input.verification.id,
265
- contractIdentity: verificationContractIdentity(input.verification),
266
- originalRevisionId,
267
- environmentIdentity: this.#observedEnvironmentIdentity(input.resources),
268
- ...(input.taskId ? { taskId: input.taskId } : {}),
269
- };
270
- const observedInputs = verificationInputPaths(input.verification).map(
271
- (relative) => ({
272
- relative,
273
- observation: observeSafePath(input.root, relative),
274
- }),
275
- );
276
- const unsafe = observedInputs.find(
277
- ({ observation }) =>
278
- observation.kind !== "file" && observation.kind !== "absent",
279
- );
280
- if (unsafe) {
281
- return {
282
- ...base,
283
- cause: "unsafe-input",
284
- input: { path: unsafe.relative, kind: "unsafe" },
285
- };
286
- }
287
- const absent = observedInputs.find(
288
- ({ observation }) => observation.kind === "absent",
289
- );
290
- if (absent) {
291
- const output = input.resources.plan.outputs.find(
292
- (candidate) => candidate.path === absent.relative,
293
- );
294
- return output
295
- ? {
296
- ...base,
297
- cause: "future-output",
298
- input: { path: absent.relative, kind: "absent" },
299
- producer: structuredClone(output.producer),
300
- }
301
- : {
302
- ...base,
303
- cause: "missing-input",
304
- input: { path: absent.relative, kind: "absent" },
305
- };
306
- }
307
- return {
308
- ...base,
309
- cause: BASELINE_CAPABILITY_CODES.has(input.result.code)
310
- ? "capability"
311
- : "unknown",
312
- };
313
- }
314
-
315
- async #refreshEnvironment(
316
- resources: DurableExecutionResources,
317
- signal: AbortSignal,
318
- ): Promise<void> {
319
- if (!this.#options.verificationEnvironment) return;
320
- const identity = await this.#options.verificationEnvironment(
321
- resources.plan,
322
- signal,
323
- );
324
- if (identity !== resources.environmentIdentity) {
325
- resources.environmentIdentity = identity;
326
- resources.baselinePromises.clear();
327
- resources.verificationFact = undefined;
328
- }
329
- }
330
-
331
- async #environmentCurrent(
332
- runId: string,
333
- signal: AbortSignal,
334
- expected?: string,
335
- ): Promise<boolean> {
336
- if (!this.#options.verificationEnvironment) return true;
337
- const resources = this.#services.run(runId);
338
- if (!resources) return false;
339
- try {
340
- const identity = await this.#options.verificationEnvironment(
341
- resources.plan,
342
- signal,
343
- );
344
- resources.environmentIdentity ??= identity;
345
- return identity === (expected ?? resources.environmentIdentity);
346
- } catch {
347
- signal.throwIfAborted();
348
- return false;
349
- }
350
- }
351
-
352
- async verifyPhase(
353
- input: Parameters<DurableWorkflowEngineOptions["verifyPhase"]>[0],
354
- ): Promise<DurablePhaseVerificationResult> {
355
- if (await this.#environmentCurrent(input.runId, input.signal)) {
356
- const identity = this.#services.run(input.runId)?.environmentIdentity;
357
- const result = await this.#options.verifyPhase(input);
358
- if (await this.#environmentCurrent(input.runId, input.signal, identity))
359
- return result;
360
- }
361
- return {
362
- ok: false,
363
- kind: "paused",
364
- code: "verification-environment-changed",
365
- };
366
- }
367
-
368
- async #verifyChange(
369
- input: Parameters<DurableWorkflowEngineOptions["verifyChange"]>[0],
370
- ): Promise<DurableChangeVerificationResult> {
371
- if (await this.#environmentCurrent(input.runId, input.signal)) {
372
- const identity = this.#services.run(input.runId)?.environmentIdentity;
373
- const result = await this.#options.verifyChange(input);
374
- if (await this.#environmentCurrent(input.runId, input.signal, identity))
375
- return result;
376
- }
377
- return {
378
- ok: false,
379
- kind: "environment",
380
- code: "verification-environment-changed",
381
- };
382
- }
383
-
384
- async verifyPostApply(
385
- resources: DurableExecutionResources,
386
- root: string,
387
- signal: AbortSignal,
388
- transactionId: string,
389
- ): Promise<{ ok: true } | { ok: false; code: string }> {
390
- if (this.#options.verificationEnvironment) {
391
- const fact = this.#services
392
- .ledger(resources, resources.deliveryRevision)
393
- .durableFact(`apply-environment-${hash(transactionId).slice(0, 40)}`);
394
- if (!isRecord(fact) || typeof fact.identity !== "string")
395
- return { ok: false, code: "verification-environment-changed" };
396
- resources.environmentIdentity = fact.identity;
397
- if (
398
- !(await this.#environmentCurrent(
399
- resources.runId,
400
- signal,
401
- fact.identity,
402
- ))
403
- )
404
- return { ok: false, code: "verification-environment-changed" };
405
- }
406
- if (
407
- resources.plan.outputs.some(
408
- (output) => observeSafePath(root, output.path).kind !== "file",
409
- )
410
- ) {
411
- return { ok: false, code: "producer-output-unavailable" };
412
- }
413
- const lifecycle = hasVerificationLifecycle(resources.plan);
414
- const result = await this.#verifyChange({
415
- runId: resources.runId,
416
- deliveryRevision: resources.deliveryRevision,
417
- root,
418
- plan: structuredClone(resources.plan),
419
- ...(lifecycle
420
- ? {
421
- scope: "post-apply" as const,
422
- verification: structuredClone(
423
- resources.plan.verification.change.postApply,
424
- ),
425
- }
426
- : {}),
427
- signal,
428
- });
429
- return result.ok &&
430
- result.exitCode === 0 &&
431
- result.classification.length > 0
432
- ? { ok: true }
433
- : {
434
- ok: false,
435
- code: result.ok ? "post-apply-verification-invalid" : result.code,
436
- };
437
- }
438
-
439
- async #observeVerification(input: {
440
- resources: DurableExecutionResources;
441
- revisionId: string;
442
- scope: DurableVerificationScope;
443
- verification: StructuredVerificationContract;
444
- signal: AbortSignal;
445
- taskId?: string;
446
- }): Promise<DurableObservedVerification> {
447
- if (input.signal.aborted) {
448
- return {
449
- ok: false,
450
- outcome: { kind: "operation-cancelled", code: "cancelled" },
451
- };
452
- }
453
- const root = mkdtempSync(path.join(input.resources.root, "verification-"));
454
- const contractIdentity = verificationContractIdentity(input.verification);
455
- try {
456
- await input.resources.workspaces.materializeAsync(
457
- input.revisionId,
458
- root,
459
- input.signal,
460
- );
461
- const result = await this.#verifyChange({
462
- runId: input.resources.runId,
463
- deliveryRevision: input.resources.deliveryRevision,
464
- root,
465
- plan: structuredClone(input.resources.plan),
466
- scope: input.scope,
467
- verification: structuredClone(input.verification),
468
- ...(input.taskId ? { taskId: input.taskId } : {}),
469
- signal: input.signal,
470
- });
471
- if (result.ok) {
472
- return {
473
- ok: true,
474
- observation: {
475
- status: "passed",
476
- verificationId: input.verification.id,
477
- failureIdentities: verificationIdentities(result, contractIdentity),
478
- contractIdentity,
479
- },
480
- };
481
- }
482
- if (result.kind === "verification") {
483
- return {
484
- ok: true,
485
- observation: {
486
- status: "failed",
487
- verificationId: input.verification.id,
488
- failureIdentities: verificationIdentities(result, contractIdentity),
489
- code: result.code,
490
- attributionReliable: result.attributionReliable,
491
- contractIdentity,
492
- },
493
- };
494
- }
495
- if (result.kind === "approval-boundary") {
496
- return {
497
- ok: false,
498
- outcome: { kind: "approval-needed", code: result.code },
499
- };
500
- }
501
- if (result.kind === "cancelled" || input.signal.aborted) {
502
- return {
503
- ok: false,
504
- outcome: { kind: "operation-cancelled", code: "cancelled" },
505
- };
506
- }
507
- const baselineScope =
508
- input.scope === "baseline-task-affected" ||
509
- input.scope === "baseline-full-suite"
510
- ? input.scope
511
- : undefined;
512
- return {
513
- ok: false,
514
- outcome: {
515
- kind: "paused",
516
- code: result.code,
517
- ...(baselineScope
518
- ? {
519
- prerequisite: this.#baselinePrerequisite({
520
- resources: input.resources,
521
- root,
522
- revisionId: input.revisionId,
523
- scope: baselineScope,
524
- verification: input.verification,
525
- result,
526
- ...(input.taskId ? { taskId: input.taskId } : {}),
527
- }),
528
- }
529
- : {}),
530
- },
531
- };
532
- } finally {
533
- if (!isExecutionRetained(root))
534
- removePathSync(root, { recursive: true, force: true });
535
- }
536
- }
537
-
538
- async #captureVerificationBaseline(input: {
539
- resources: DurableExecutionResources;
540
- ledger: TaskLedger;
541
- signal: AbortSignal;
542
- taskId?: string;
543
- }): Promise<DurableBaselineResult> {
544
- const aggregateFactKey = this.#aggregateBaselineFactKey(input.resources);
545
- for (const factKey of [
546
- aggregateFactKey,
547
- this.baselineFactKey(input.resources),
548
- ]) {
549
- const stored = input.ledger.durableFact(factKey);
550
- if (stored === undefined) continue;
551
- const baseline = parseVerificationBaseline(
552
- stored,
553
- input.resources.artifacts,
554
- );
555
- if (
556
- baseline.revisionId !==
557
- this.#originalBaselineRevisionId(input.resources)
558
- ) {
559
- throw new Error("workflow-verification-baseline-conflict");
560
- }
561
- // Legacy aggregates remain inspectable, but do not bind the complete
562
- // verification contract and therefore cannot authorize evidence reuse.
563
- if (factKey !== aggregateFactKey) continue;
564
- const compatible = this.#compatibleAggregateBaseline(
565
- baseline,
566
- input.resources.plan,
567
- );
568
- if (!compatible) continue;
569
- if (!input.taskId) return { ok: true, baseline };
570
- const affected = baseline.affected.filter(
571
- (entry) => entry.taskId === input.taskId,
572
- );
573
- if (affected.length === 1) {
574
- return { ok: true, baseline: { ...baseline, affected } };
575
- }
576
- }
577
- const plan = input.resources.plan;
578
- const tasks = input.taskId
579
- ? plan.tasks.filter((task) => task.taskId === input.taskId)
580
- : plan.tasks;
581
- if (tasks.length !== (input.taskId ? 1 : plan.tasks.length)) {
582
- throw new Error("workflow-task-baseline-unavailable");
583
- }
584
- const pendingAffected = tasks.map(async (task) => {
585
- const observed = await this.#captureBaselineObservation({
586
- resources: input.resources,
587
- ledger: input.ledger,
588
- verification: task.baselineVerification ?? task.affectedVerification,
589
- scope: "baseline-task-affected",
590
- taskId: task.taskId,
591
- signal: input.signal,
592
- });
593
- return { taskId: task.taskId, observed };
594
- });
595
- const pendingFullSuite = this.#captureBaselineObservation({
596
- resources: input.resources,
597
- ledger: input.ledger,
598
- verification: plan.verification.baseline.fullSuite,
599
- scope: "baseline-full-suite",
600
- signal: input.signal,
601
- });
602
- const settled = await Promise.allSettled([
603
- ...pendingAffected,
604
- pendingFullSuite,
605
- ]);
606
- const rejected = settled.find(
607
- (result): result is PromiseRejectedResult => result.status === "rejected",
608
- );
609
- if (rejected) throw rejected.reason;
610
- const fulfilled = settled as Array<
611
- PromiseFulfilledResult<
612
- | Awaited<(typeof pendingAffected)[number]>
613
- | Awaited<typeof pendingFullSuite>
614
- >
615
- >;
616
- const affectedResults = fulfilled
617
- .slice(0, pendingAffected.length)
618
- .map((result) => result.value) as Awaited<
619
- (typeof pendingAffected)[number]
620
- >[];
621
- const fullSuiteResult = fulfilled.at(-1);
622
- if (!fullSuiteResult) {
623
- throw new Error("workflow-verification-baseline-unavailable");
624
- }
625
- const fullSuite = fullSuiteResult.value as Awaited<typeof pendingFullSuite>;
626
- const affectedFailure = affectedResults.find((entry) => !entry.observed.ok);
627
- if (affectedFailure && !affectedFailure.observed.ok) {
628
- return affectedFailure.observed;
629
- }
630
- if (!fullSuite.ok) return fullSuite;
631
- const baseline: DurableVerificationBaseline = {
632
- revisionId: this.#originalBaselineRevisionId(input.resources),
633
- targetContracts: plan.tasks
634
- .filter(
635
- (task) =>
636
- !task.verificationMode || task.verificationMode === "behavior",
637
- )
638
- .map((task) => ({
639
- taskId: task.taskId,
640
- verificationId: task.phases.red.verification.id,
641
- expectedFailure:
642
- "expectedFailure" in task.phases.red.verification
643
- ? (task.phases.red.verification.expectedFailure ??
644
- task.phases.red.verification.id)
645
- : task.phases.red.verification.id,
646
- })),
647
- affected: affectedResults.map((entry) => {
648
- if (!entry.observed.ok) {
649
- throw new Error("workflow-task-baseline-unavailable");
650
- }
651
- return {
652
- taskId: entry.taskId,
653
- observation: entry.observed.observation,
654
- };
655
- }),
656
- fullSuite: fullSuite.observation,
657
- };
658
- if (!input.taskId) {
659
- input.ledger.putDurableFact(
660
- this.#aggregateBaselineFactKey(input.resources),
661
- verificationBaselineFact(baseline, input.resources.artifacts),
662
- );
663
- }
664
- return { ok: true, baseline };
665
- }
666
-
667
- #compatibleAggregateBaseline(
668
- baseline: DurableVerificationBaseline,
669
- plan: DurableExecutionResources["plan"],
670
- ): boolean {
671
- const matches = (
672
- observation: DurableVerificationObservation,
673
- contract: StructuredVerificationContract,
674
- ): boolean =>
675
- observation.verificationId === contract.id &&
676
- observation.contractIdentity === verificationContractIdentity(contract);
677
- return (
678
- matches(baseline.fullSuite, plan.verification.baseline.fullSuite) &&
679
- baseline.affected.length === plan.tasks.length &&
680
- plan.tasks.every((task) => {
681
- const entries = baseline.affected.filter(
682
- (entry) => entry.taskId === task.taskId,
683
- );
684
- const [entry] = entries;
685
- return (
686
- entries.length === 1 &&
687
- entry !== undefined &&
688
- matches(
689
- entry.observation,
690
- task.baselineVerification ?? task.affectedVerification,
691
- )
692
- );
693
- })
694
- );
695
- }
696
-
697
- #aggregateBaselineFactKey(resources: DurableExecutionResources): string {
698
- return `${this.baselineFactKey(resources)}-all-${hash(
699
- this.#originalBaselineRevisionId(resources),
700
- canonicalJson({
701
- tasks: resources.plan.tasks.map((task) => ({
702
- taskId: task.taskId,
703
- verification: task.baselineVerification ?? task.affectedVerification,
704
- })),
705
- fullSuite: resources.plan.verification.baseline.fullSuite,
706
- }),
707
- ).slice(0, 40)}`;
708
- }
709
-
710
- #baselineOwnerIdentity(
711
- resources: DurableExecutionResources,
712
- scope: "baseline-task-affected" | "baseline-full-suite",
713
- taskId?: string,
714
- ): string {
715
- if (scope === "baseline-full-suite") {
716
- if (taskId !== undefined)
717
- throw new Error("workflow-verification-baseline-failure-invalid");
718
- return hash("baseline-observation-owner-v1", "global");
719
- }
720
- if (!taskId)
721
- throw new Error("workflow-verification-baseline-failure-invalid");
722
- const tasks = resources.plan.tasks.filter((task) => task.taskId === taskId);
723
- const task = tasks[0];
724
- if (!task || tasks.length !== 1)
725
- throw new Error("workflow-verification-baseline-failure-invalid");
726
- return baselineRecoveryKey(task, "red");
727
- }
728
-
729
- #baselineObservationFactKey(
730
- resources: DurableExecutionResources,
731
- verification: StructuredVerificationContract,
732
- scope: "baseline-task-affected" | "baseline-full-suite",
733
- taskId?: string,
734
- ): string {
735
- return `${this.baselineFactKey(resources)}-o-${hash(
736
- this.#originalBaselineRevisionId(resources),
737
- verificationContractIdentity(verification),
738
- scope,
739
- this.#baselineOwnerIdentity(resources, scope, taskId),
740
- ).slice(0, 40)}`;
741
- }
742
-
743
- #baselineFailureFactKey(input: {
744
- resources: DurableExecutionResources;
745
- verification: StructuredVerificationContract;
746
- scope: "baseline-task-affected" | "baseline-full-suite";
747
- taskId?: string;
748
- }): string {
749
- return `verification-baseline-failure-${hash(
750
- this.#originalBaselineRevisionId(input.resources),
751
- verificationContractIdentity(input.verification),
752
- input.scope,
753
- this.#baselineOwnerIdentity(input.resources, input.scope, input.taskId),
754
- this.#observedEnvironmentIdentity(input.resources),
755
- this.#options.verificationPolicy ?? "legacy",
756
- ).slice(0, 40)}`;
757
- }
758
-
759
- #legacyBaselineFailureFactKey(input: {
760
- resources: DurableExecutionResources;
761
- verification: StructuredVerificationContract;
762
- scope: "baseline-task-affected" | "baseline-full-suite";
763
- taskId?: string;
764
- }): string {
765
- return `verification-baseline-failure-${hash(
766
- this.#originalBaselineRevisionId(input.resources),
767
- verificationContractIdentity(input.verification),
768
- input.scope,
769
- input.taskId ?? "global",
770
- this.#observedEnvironmentIdentity(input.resources),
771
- this.#options.verificationPolicy ?? "legacy",
772
- ).slice(0, 40)}`;
773
- }
774
-
775
- #projectBaselineFailure(
776
- code: string,
777
- observation: { path: string; kind: "absent" | "unsafe" } | undefined,
778
- input: {
779
- resources: DurableExecutionResources;
780
- verification: StructuredVerificationContract;
781
- scope: "baseline-task-affected" | "baseline-full-suite";
782
- taskId?: string;
783
- },
784
- ): Extract<DurableObservedVerification, { ok: false }> {
785
- const prerequisite: WorkflowVerificationPrerequisite = {
786
- kind: "verification-prerequisite",
787
- scope: input.scope,
788
- cause: BASELINE_CAPABILITY_CODES.has(code) ? "capability" : "unknown",
789
- verificationId: input.verification.id,
790
- contractIdentity: verificationContractIdentity(input.verification),
791
- originalRevisionId: this.#originalBaselineRevisionId(input.resources),
792
- environmentIdentity: this.#observedEnvironmentIdentity(input.resources),
793
- ...(input.taskId ? { taskId: input.taskId } : {}),
794
- };
795
- if (observation?.kind === "unsafe") {
796
- prerequisite.cause = "unsafe-input";
797
- prerequisite.input = observation;
798
- } else if (observation?.kind === "absent") {
799
- const outputs = input.resources.plan.outputs.filter(
800
- (output) => output.path === observation.path,
801
- );
802
- if (outputs.length > 1)
803
- throw new Error("workflow-verification-baseline-failure-invalid");
804
- prerequisite.cause =
805
- outputs.length === 1 ? "future-output" : "missing-input";
806
- prerequisite.input = observation;
807
- if (outputs[0])
808
- prerequisite.producer = structuredClone(outputs[0].producer);
809
- }
810
- return {
811
- ok: false,
812
- outcome: { kind: "paused", code, prerequisite },
813
- };
814
- }
815
-
816
- #storedBaselineFailure(
817
- value: unknown,
818
- input: {
819
- resources: DurableExecutionResources;
820
- verification: StructuredVerificationContract;
821
- scope: "baseline-task-affected" | "baseline-full-suite";
822
- taskId?: string;
823
- },
824
- ): Extract<DurableObservedVerification, { ok: false }> {
825
- if (!isRecord(value))
826
- throw new Error("workflow-verification-baseline-failure-invalid");
827
- const contractIdentity = verificationContractIdentity(input.verification);
828
- const originalRevisionId = this.#originalBaselineRevisionId(
829
- input.resources,
830
- );
831
- const environmentIdentity = this.#observedEnvironmentIdentity(
832
- input.resources,
833
- );
834
- const ownerIdentity = this.#baselineOwnerIdentity(
835
- input.resources,
836
- input.scope,
837
- input.taskId,
838
- );
839
- const declaredPaths = verificationInputPaths(input.verification);
840
- let observation: { path: string; kind: "absent" | "unsafe" } | undefined;
841
- if (value.kind === "baseline-observation-failure-v2") {
842
- if (
843
- !hasExactKeys(
844
- value,
845
- [
846
- "kind",
847
- "policyIdentity",
848
- "ownerIdentity",
849
- "scope",
850
- "contractIdentity",
851
- "originalRevisionId",
852
- "environmentIdentity",
853
- "code",
854
- ],
855
- ["observation"],
856
- ) ||
857
- value.policyIdentity !==
858
- hash(
859
- "verification-failure-policy-v2",
860
- this.#options.verificationPolicy ?? "legacy",
861
- ) ||
862
- value.ownerIdentity !== ownerIdentity ||
863
- value.scope !== input.scope ||
864
- value.contractIdentity !== contractIdentity ||
865
- value.originalRevisionId !== originalRevisionId ||
866
- value.environmentIdentity !== environmentIdentity
867
- ) {
868
- throw new Error("workflow-verification-baseline-failure-invalid");
869
- }
870
- if (value.observation !== undefined) {
871
- if (
872
- !isRecord(value.observation) ||
873
- !hasExactKeys(value.observation, ["path", "kind"]) ||
874
- typeof value.observation.path !== "string" ||
875
- !declaredPaths.includes(value.observation.path) ||
876
- !["absent", "unsafe"].includes(String(value.observation.kind))
877
- )
878
- throw new Error("workflow-verification-baseline-failure-invalid");
879
- observation = {
880
- path: value.observation.path,
881
- kind: value.observation.kind as "absent" | "unsafe",
882
- };
883
- }
884
- } else if (value.kind === "baseline-observation-failure-v1") {
885
- const prerequisite = value.prerequisite;
886
- if (
887
- !hasExactKeys(value, [
888
- "kind",
889
- "policyIdentity",
890
- "code",
891
- "prerequisite",
892
- ]) ||
893
- value.policyIdentity !==
894
- hash(
895
- "verification-failure-policy-v1",
896
- this.#options.verificationPolicy ?? "legacy",
897
- ) ||
898
- !isRecord(prerequisite) ||
899
- !hasExactKeys(
900
- prerequisite,
901
- [
902
- "kind",
903
- "scope",
904
- "cause",
905
- "verificationId",
906
- "contractIdentity",
907
- "originalRevisionId",
908
- "environmentIdentity",
909
- ],
910
- ["taskId", "input", "producer"],
911
- ) ||
912
- prerequisite.kind !== "verification-prerequisite" ||
913
- prerequisite.scope !== input.scope ||
914
- typeof prerequisite.verificationId !== "string" ||
915
- !IDENTIFIER.test(prerequisite.verificationId) ||
916
- prerequisite.contractIdentity !== contractIdentity ||
917
- prerequisite.originalRevisionId !== originalRevisionId ||
918
- prerequisite.environmentIdentity !== environmentIdentity ||
919
- prerequisite.taskId !== input.taskId ||
920
- ![
921
- "future-output",
922
- "missing-input",
923
- "unsafe-input",
924
- "capability",
925
- "unknown",
926
- ].includes(String(prerequisite.cause))
927
- )
928
- throw new Error("workflow-verification-baseline-failure-invalid");
929
- if (prerequisite.input !== undefined) {
930
- if (
931
- !isRecord(prerequisite.input) ||
932
- !hasExactKeys(prerequisite.input, ["path", "kind"]) ||
933
- typeof prerequisite.input.path !== "string" ||
934
- !declaredPaths.includes(prerequisite.input.path) ||
935
- !["absent", "unsafe"].includes(String(prerequisite.input.kind))
936
- )
937
- throw new Error("workflow-verification-baseline-failure-invalid");
938
- observation = {
939
- path: prerequisite.input.path,
940
- kind: prerequisite.input.kind as "absent" | "unsafe",
941
- };
942
- }
943
- const cause = String(prerequisite.cause);
944
- if (
945
- ((cause === "future-output" || cause === "missing-input") &&
946
- observation?.kind !== "absent") ||
947
- (cause === "unsafe-input" && observation?.kind !== "unsafe") ||
948
- ((cause === "capability" || cause === "unknown") &&
949
- (observation !== undefined ||
950
- prerequisite.producer !== undefined ||
951
- (cause === "capability") !==
952
- BASELINE_CAPABILITY_CODES.has(String(value.code))))
953
- )
954
- throw new Error("workflow-verification-baseline-failure-invalid");
955
- if (prerequisite.producer !== undefined) {
956
- if (
957
- cause !== "future-output" ||
958
- !isRecord(prerequisite.producer) ||
959
- !hasExactKeys(prerequisite.producer, ["taskId", "phase"]) ||
960
- typeof prerequisite.producer.taskId !== "string" ||
961
- !IDENTIFIER.test(prerequisite.producer.taskId) ||
962
- !["red", "green", "refactor"].includes(
963
- String(prerequisite.producer.phase),
964
- )
965
- )
966
- throw new Error("workflow-verification-baseline-failure-invalid");
967
- } else if (cause === "future-output") {
968
- throw new Error("workflow-verification-baseline-failure-invalid");
969
- }
970
- } else {
971
- throw new Error("workflow-verification-baseline-failure-invalid");
972
- }
973
- if (
974
- typeof value.code !== "string" ||
975
- value.code.length < 1 ||
976
- value.code.length > 256
977
- )
978
- throw new Error("workflow-verification-baseline-failure-invalid");
979
- return this.#projectBaselineFailure(value.code, observation, input);
980
- }
981
-
982
- async #captureBaselineObservation(input: {
983
- resources: DurableExecutionResources;
984
- ledger: TaskLedger;
985
- verification: StructuredVerificationContract;
986
- scope: "baseline-task-affected" | "baseline-full-suite";
987
- signal: AbortSignal;
988
- taskId?: string;
989
- }): Promise<DurableObservedVerification> {
990
- const contractIdentity = verificationContractIdentity(input.verification);
991
- const ledger = input.resources.baselineLedger ?? input.ledger;
992
- const factKey = this.#baselineObservationFactKey(
993
- input.resources,
994
- input.verification,
995
- input.scope,
996
- input.taskId,
997
- );
998
- const failureFactKey = this.#baselineFailureFactKey(input);
999
- const legacyFailureFactKey = this.#legacyBaselineFailureFactKey(input);
1000
- const stored = ledger.durableFact(factKey);
1001
- const storedFailures = [
1002
- ledger.durableFact(failureFactKey),
1003
- ledger.durableFact(legacyFailureFactKey),
1004
- ...(input.ledger === ledger
1005
- ? []
1006
- : [input.ledger.durableFact(legacyFailureFactKey)]),
1007
- ].filter((value) => value !== undefined);
1008
- if (storedFailures.length > 1)
1009
- throw new Error("workflow-verification-baseline-fact-conflict");
1010
- const storedFailure = storedFailures[0];
1011
- if (stored !== undefined && storedFailure !== undefined) {
1012
- throw new Error("workflow-verification-baseline-fact-conflict");
1013
- }
1014
- if (stored !== undefined) {
1015
- const baseline = parseVerificationBaseline(
1016
- stored,
1017
- input.resources.artifacts,
1018
- );
1019
- if (
1020
- baseline.revisionId !==
1021
- this.#originalBaselineRevisionId(input.resources)
1022
- ) {
1023
- throw new Error("workflow-verification-baseline-conflict");
1024
- }
1025
- if (
1026
- baseline.fullSuite.contractIdentity !== contractIdentity ||
1027
- baseline.targetContracts.length !== 0 ||
1028
- baseline.affected.length !== 0
1029
- ) {
1030
- throw new Error("workflow-verification-baseline-conflict");
1031
- }
1032
- return {
1033
- ok: true,
1034
- observation: {
1035
- ...baseline.fullSuite,
1036
- verificationId: input.verification.id,
1037
- },
1038
- };
1039
- }
1040
- if (storedFailure !== undefined) {
1041
- return this.#storedBaselineFailure(storedFailure, input);
1042
- }
1043
- const pendingKey = `baseline-observation:${factKey}`;
1044
- let pending = input.resources.baselinePromises.get(pendingKey);
1045
- if (!pending) {
1046
- pending = this.#observeVerification({
1047
- resources: input.resources,
1048
- revisionId: this.#originalBaselineRevisionId(input.resources),
1049
- scope: input.scope,
1050
- verification: input.verification,
1051
- ...(input.taskId ? { taskId: input.taskId } : {}),
1052
- signal: input.signal,
1053
- }).then(
1054
- (observed): DurableBaselineResult =>
1055
- observed.ok
1056
- ? {
1057
- ok: true,
1058
- baseline: {
1059
- revisionId: this.#originalBaselineRevisionId(input.resources),
1060
- targetContracts: [],
1061
- affected: [],
1062
- fullSuite: observed.observation,
1063
- },
1064
- }
1065
- : observed,
1066
- );
1067
- const owned = pending;
1068
- pending = owned.then(
1069
- (result) => {
1070
- if (!result.ok) input.resources.baselinePromises.delete(pendingKey);
1071
- return result;
1072
- },
1073
- (error: unknown) => {
1074
- input.resources.baselinePromises.delete(pendingKey);
1075
- throw error;
1076
- },
1077
- );
1078
- input.resources.baselinePromises.set(pendingKey, pending);
1079
- }
1080
- const result = await pending;
1081
- if (!result.ok) {
1082
- if (result.outcome.kind === "paused") {
1083
- if (!result.outcome.prerequisite) {
1084
- throw new Error("workflow-verification-baseline-failure-invalid");
1085
- }
1086
- const prerequisite = result.outcome.prerequisite;
1087
- const storedFact = ledger.putDurableFact(failureFactKey, {
1088
- kind: "baseline-observation-failure-v2",
1089
- policyIdentity: hash(
1090
- "verification-failure-policy-v2",
1091
- this.#options.verificationPolicy ?? "legacy",
1092
- ),
1093
- ownerIdentity: this.#baselineOwnerIdentity(
1094
- input.resources,
1095
- input.scope,
1096
- input.taskId,
1097
- ),
1098
- scope: input.scope,
1099
- contractIdentity,
1100
- originalRevisionId: this.#originalBaselineRevisionId(input.resources),
1101
- environmentIdentity: this.#observedEnvironmentIdentity(
1102
- input.resources,
1103
- ),
1104
- code: result.outcome.code,
1105
- ...(prerequisite.input
1106
- ? { observation: structuredClone(prerequisite.input) }
1107
- : {}),
1108
- });
1109
- return this.#storedBaselineFailure(storedFact, input);
1110
- }
1111
- return result;
1112
- }
1113
- const fact = verificationBaselineFact(
1114
- result.baseline,
1115
- input.resources.artifacts,
1116
- );
1117
- const storedFact = ledger.putDurableFact(factKey, fact);
1118
- const baseline = parseVerificationBaseline(
1119
- storedFact,
1120
- input.resources.artifacts,
1121
- );
1122
- if (
1123
- baseline.revisionId !==
1124
- this.#originalBaselineRevisionId(input.resources) ||
1125
- baseline.fullSuite.contractIdentity !== contractIdentity ||
1126
- baseline.targetContracts.length !== 0 ||
1127
- baseline.affected.length !== 0
1128
- )
1129
- throw new Error("workflow-verification-baseline-conflict");
1130
- return {
1131
- ok: true,
1132
- observation: {
1133
- ...baseline.fullSuite,
1134
- verificationId: input.verification.id,
1135
- },
1136
- };
1137
- }
1138
-
1139
- async ensureVerificationBaseline(input: {
1140
- resources: DurableExecutionResources;
1141
- ledger: TaskLedger;
1142
- signal: AbortSignal;
1143
- taskId?: string;
1144
- }): Promise<DurableBaselineResult> {
1145
- try {
1146
- await this.#refreshEnvironment(input.resources, input.signal);
1147
- } catch {
1148
- return {
1149
- ok: false,
1150
- outcome: {
1151
- kind: "paused",
1152
- code: "verification-environment-unavailable",
1153
- },
1154
- };
1155
- }
1156
- return this.#captureVerificationBaseline(input);
1157
- }
1158
-
1159
- async verifyTaskAffected(input: {
1160
- resources: DurableExecutionResources;
1161
- baseline: DurableVerificationBaseline;
1162
- task: PlanTaskDraft;
1163
- revisionId: string;
1164
- signal: AbortSignal;
1165
- }): Promise<DurableAffectedResult> {
1166
- const baseline = input.baseline.affected.find(
1167
- (entry) => entry.taskId === input.task.taskId,
1168
- );
1169
- if (!baseline) throw new Error("workflow-task-baseline-unavailable");
1170
- const current = await this.#observeVerification({
1171
- resources: input.resources,
1172
- revisionId: input.revisionId,
1173
- scope: "task-affected",
1174
- verification: input.task.affectedVerification,
1175
- taskId: input.task.taskId,
1176
- signal: input.signal,
1177
- });
1178
- if (!current.ok) return current.outcome;
1179
- if (current.observation.status === "passed") {
1180
- return { kind: "verified", attribution: "none" };
1181
- }
1182
- const comparable =
1183
- typeof baseline.observation.contractIdentity === "string" &&
1184
- baseline.observation.contractIdentity ===
1185
- current.observation.contractIdentity;
1186
- if (
1187
- current.observation.attributionReliable === false &&
1188
- baseline.observation.status === "failed"
1189
- )
1190
- return { kind: "paused", code: "verification-attribution-unresolved" };
1191
- const prior = new Set(
1192
- comparable ? baseline.observation.failureIdentities : [],
1193
- );
1194
- const introduced = current.observation.failureIdentities.filter(
1195
- (identity) => !prior.has(identity),
1196
- );
1197
- if (introduced.length === 0) {
1198
- return { kind: "verified", attribution: "pre-existing" };
1199
- }
1200
- return {
1201
- kind: "repairable",
1202
- attribution: "introduced",
1203
- failureIdentities: introduced.slice(0, 256),
1204
- };
1205
- }
1206
-
1207
- #agentsManagedParts(
1208
- text: string,
1209
- ): { prefix: string; suffix: string } | undefined {
1210
- const startMarker = "<!-- ABEL:AGENTS-INDEX:START -->";
1211
- const endMarker = "<!-- ABEL:AGENTS-INDEX:END -->";
1212
- const lines = text.split("\n");
1213
- const starts: Array<{ line: number; start: number }> = [];
1214
- const ends: Array<{ line: number; end: number }> = [];
1215
- let offset = 0;
1216
- for (const [line, value] of lines.entries()) {
1217
- if (value === startMarker) starts.push({ line, start: offset });
1218
- if (value === endMarker) {
1219
- ends.push({ line, end: offset + value.length });
1220
- }
1221
- offset += value.length + 1;
1222
- }
1223
- if (
1224
- starts.length !== 1 ||
1225
- ends.length !== 1 ||
1226
- starts[0].line >= ends[0].line
1227
- ) {
1228
- return undefined;
1229
- }
1230
- return {
1231
- prefix: text.slice(0, starts[0].start),
1232
- suffix: text.slice(ends[0].end),
1233
- };
1234
- }
1235
-
1236
- #applyAgentsCheckpoint(
1237
- resources: DurableExecutionResources,
1238
- ): { ok: true; revision: WorkspaceRevision } | { ok: false; code: string } {
1239
- const checkpoint = resources.plan.verification.agentsCheckpoint;
1240
- const current = resources.workspaces.getRevision(
1241
- resources.currentRevisionId,
1242
- );
1243
- if (!checkpoint.required) return { ok: true, revision: current };
1244
- const changes: Record<
1245
- string,
1246
- { kind: "absent" } | { kind: "file"; bytes: Uint8Array; mode: number }
1247
- > = {};
1248
- for (const operation of checkpoint.operations) {
1249
- const entry = current.entries[operation.target];
1250
- let before: string | null;
1251
- if (!entry || entry.kind === "absent") {
1252
- before = null;
1253
- } else {
1254
- try {
1255
- before = new TextDecoder("utf-8", { fatal: true }).decode(
1256
- resources.artifacts.read(entry.hash),
1257
- );
1258
- } catch {
1259
- return { ok: false, code: "agents-checkpoint-contract-stale" };
1260
- }
1261
- }
1262
- if (operation.impact === "create-index") {
1263
- if (operation.managedBlock === null) {
1264
- throw new Error("workflow-agents-checkpoint-invalid");
1265
- }
1266
- const desired = `${operation.managedBlock}\n`;
1267
- if (before !== null && before !== desired) {
1268
- return { ok: false, code: "agents-checkpoint-contract-stale" };
1269
- }
1270
- if (before === null) {
1271
- changes[operation.target] = {
1272
- kind: "file",
1273
- bytes: new TextEncoder().encode(desired),
1274
- mode: 0o644,
1275
- };
1276
- }
1277
- continue;
1278
- }
1279
- if (operation.impact === "remove-index") {
1280
- if (operation.managedBlock !== null) {
1281
- throw new Error("workflow-agents-checkpoint-invalid");
1282
- }
1283
- if (before === null) continue;
1284
- const parts = this.#agentsManagedParts(before);
1285
- if (!parts) {
1286
- return { ok: false, code: "agents-checkpoint-contract-stale" };
1287
- }
1288
- const after = `${parts.prefix}${parts.suffix}`;
1289
- changes[operation.target] =
1290
- after.trim().length === 0
1291
- ? { kind: "absent" }
1292
- : {
1293
- kind: "file",
1294
- bytes: new TextEncoder().encode(after),
1295
- mode: entry?.kind === "file" ? entry.mode : 0o644,
1296
- };
1297
- continue;
1298
- }
1299
- if (operation.managedBlock === null || before === null) {
1300
- return { ok: false, code: "agents-checkpoint-contract-stale" };
1301
- }
1302
- const parts = this.#agentsManagedParts(before);
1303
- if (!parts) {
1304
- return { ok: false, code: "agents-checkpoint-contract-stale" };
1305
- }
1306
- const after = `${parts.prefix}${operation.managedBlock}${parts.suffix}`;
1307
- if (after !== before) {
1308
- changes[operation.target] = {
1309
- kind: "file",
1310
- bytes: new TextEncoder().encode(after),
1311
- mode: entry?.kind === "file" ? entry.mode : 0o644,
1312
- };
1313
- }
1314
- }
1315
- if (Object.keys(changes).length === 0) {
1316
- return { ok: true, revision: current };
1317
- }
1318
- return {
1319
- ok: true,
1320
- revision: resources.workspaces.createRevision({
1321
- parentRevisionId: current.revisionId,
1322
- changes,
1323
- }),
1324
- };
1325
- }
1326
-
1327
- baselineFactKey(resources: DurableExecutionResources): string {
1328
- return this.#options.verificationPolicy || resources.environmentIdentity
1329
- ? `verification-baseline-${hash(this.#options.verificationPolicy ?? "legacy", resources.environmentIdentity ?? "legacy").slice(0, 24)}`
1330
- : "verification-baseline";
1331
- }
1332
-
1333
- async verify(
1334
- input: Parameters<WorkflowChangeVerifier["verify"]>[0],
1335
- ): Promise<Awaited<ReturnType<WorkflowChangeVerifier["verify"]>>> {
1336
- if (!input.currentWorkspaceRevisionId) {
1337
- return { kind: "paused", code: "workspace-revision-unavailable" };
1338
- }
1339
- const resources = await this.#services.prepareResources(
1340
- {
1341
- runId: input.runId,
1342
- deliveryRevision: input.deliveryRevision,
1343
- plan: input.plan,
1344
- currentWorkspaceRevisionId: input.currentWorkspaceRevisionId,
1345
- baselineRevisionId:
1346
- input.baselineRevisionId ??
1347
- this.#services.run(input.runId)?.baselineRevisionId,
1348
- },
1349
- input.signal,
1350
- );
1351
- try {
1352
- await this.#refreshEnvironment(resources, input.signal);
1353
- } catch {
1354
- return { kind: "paused", code: "verification-environment-unavailable" };
1355
- }
1356
- for (const reference of input.taskEvidence ?? []) {
1357
- const task = input.plan.tasks.find(
1358
- (task) => task.taskId === reference.taskId,
1359
- );
1360
- if (!task) throw new Error("workflow-ledger-task-invalid");
1361
- const failure = await this.#services.revalidatePhasePolicy(
1362
- resources,
1363
- this.#services.ledger(resources, reference.deliveryRevision),
1364
- task,
1365
- input.signal,
1366
- );
1367
- if (failure)
1368
- return {
1369
- kind: "paused",
1370
- code:
1371
- "code" in failure ? failure.code : "verification-policy-rejected",
1372
- };
1373
- }
1374
- const cumulativeRevision = resources.workspaces.getRevision(
1375
- resources.currentRevisionId,
1376
- );
1377
- if (
1378
- input.plan.outputs.some(
1379
- (output) => cumulativeRevision.entries[output.path]?.kind !== "file",
1380
- )
1381
- ) {
1382
- return { kind: "paused", code: "producer-output-unavailable" };
1383
- }
1384
- if (hasVerificationLifecycle(input.plan)) {
1385
- const ledger = this.#services.ledger(resources, input.deliveryRevision);
1386
- const captured = await this.ensureVerificationBaseline({
1387
- resources,
1388
- ledger,
1389
- signal: input.signal,
1390
- });
1391
- if (!captured.ok) {
1392
- return captured.outcome.kind === "approval-needed"
1393
- ? captured.outcome
1394
- : captured.outcome.kind === "operation-cancelled"
1395
- ? { kind: "paused", code: "cancelled" }
1396
- : {
1397
- ...captured.outcome,
1398
- verification: {
1399
- scope: "baseline",
1400
- attribution: "environment",
1401
- },
1402
- };
1403
- }
1404
- for (const task of input.plan.tasks) {
1405
- const baseline = captured.baseline.affected.find(
1406
- (entry) => entry.taskId === task.taskId,
1407
- );
1408
- if (!baseline) throw new Error("workflow-task-baseline-unavailable");
1409
- const current = await this.#observeVerification({
1410
- resources,
1411
- revisionId: resources.currentRevisionId,
1412
- scope: "change-task-affected",
1413
- verification: task.affectedVerification,
1414
- taskId: task.taskId,
1415
- signal: input.signal,
1416
- });
1417
- if (!current.ok) {
1418
- return current.outcome.kind === "approval-needed"
1419
- ? current.outcome
1420
- : current.outcome.kind === "operation-cancelled"
1421
- ? { kind: "paused", code: "cancelled" }
1422
- : {
1423
- ...current.outcome,
1424
- verification: {
1425
- scope: "change-task-affected",
1426
- attribution: "environment",
1427
- taskId: task.taskId,
1428
- },
1429
- };
1430
- }
1431
- if (current.observation.status === "failed") {
1432
- const comparable =
1433
- typeof baseline.observation.contractIdentity === "string" &&
1434
- baseline.observation.contractIdentity ===
1435
- current.observation.contractIdentity;
1436
- if (
1437
- current.observation.attributionReliable === false &&
1438
- baseline.observation.status === "failed"
1439
- )
1440
- return {
1441
- kind: "paused",
1442
- code: "verification-attribution-unresolved",
1443
- };
1444
- const prior = new Set(
1445
- comparable ? baseline.observation.failureIdentities : [],
1446
- );
1447
- const introduced = current.observation.failureIdentities.filter(
1448
- (identity) => !prior.has(identity),
1449
- );
1450
- if (introduced.length > 0) {
1451
- return {
1452
- kind: "paused",
1453
- code: "verification-repair-required",
1454
- verification: {
1455
- scope: "change-task-affected",
1456
- attribution: "introduced",
1457
- taskId: task.taskId,
1458
- failureIdentities: introduced.slice(0, 256),
1459
- },
1460
- };
1461
- }
1462
- }
1463
- }
1464
- const fullSuite = await this.#observeVerification({
1465
- resources,
1466
- revisionId: resources.currentRevisionId,
1467
- scope: "change-full-suite",
1468
- verification: input.plan.verification.change.fullSuite,
1469
- signal: input.signal,
1470
- });
1471
- if (!fullSuite.ok) {
1472
- return fullSuite.outcome.kind === "approval-needed"
1473
- ? fullSuite.outcome
1474
- : fullSuite.outcome.kind === "operation-cancelled"
1475
- ? { kind: "paused", code: "cancelled" }
1476
- : {
1477
- ...fullSuite.outcome,
1478
- verification: {
1479
- scope: "change-full-suite",
1480
- attribution: "environment",
1481
- },
1482
- };
1483
- }
1484
- if (fullSuite.observation.status === "failed") {
1485
- const comparable =
1486
- typeof captured.baseline.fullSuite.contractIdentity === "string" &&
1487
- captured.baseline.fullSuite.contractIdentity ===
1488
- fullSuite.observation.contractIdentity;
1489
- if (
1490
- fullSuite.observation.attributionReliable === false &&
1491
- captured.baseline.fullSuite.status === "failed"
1492
- )
1493
- return {
1494
- kind: "paused",
1495
- code: "verification-attribution-unresolved",
1496
- };
1497
- const baselineFailures = new Set(
1498
- comparable ? captured.baseline.fullSuite.failureIdentities : [],
1499
- );
1500
- const introduced = fullSuite.observation.failureIdentities.filter(
1501
- (identity) => !baselineFailures.has(identity),
1502
- );
1503
- if (introduced.length > 0) {
1504
- return {
1505
- kind: "paused",
1506
- code: "verification-attribution-unresolved",
1507
- verification: {
1508
- scope: "change-full-suite",
1509
- attribution: "unresolved",
1510
- failureIdentities: introduced.slice(0, 256),
1511
- },
1512
- };
1513
- }
1514
- }
1515
- const checkpoint = input.plan.verification.agentsCheckpoint;
1516
- if (checkpoint.required && checkpoint.verification) {
1517
- const applied = this.#applyAgentsCheckpoint(resources);
1518
- if (!applied.ok) {
1519
- return { kind: "paused", code: applied.code };
1520
- }
1521
- const observed = await this.#observeVerification({
1522
- resources,
1523
- revisionId: applied.revision.revisionId,
1524
- scope: "agents-checkpoint",
1525
- verification: checkpoint.verification,
1526
- signal: input.signal,
1527
- });
1528
- if (!observed.ok) {
1529
- return observed.outcome.kind === "approval-needed"
1530
- ? observed.outcome
1531
- : observed.outcome.kind === "operation-cancelled"
1532
- ? { kind: "paused", code: "cancelled" }
1533
- : {
1534
- ...observed.outcome,
1535
- verification: {
1536
- scope: "agents-checkpoint",
1537
- attribution: "environment",
1538
- },
1539
- };
1540
- }
1541
- if (observed.observation.status === "failed") {
1542
- return {
1543
- kind: "paused",
1544
- code: "agents-checkpoint-verification-failed",
1545
- verification: {
1546
- scope: "agents-checkpoint",
1547
- attribution: "introduced",
1548
- failureIdentities: observed.observation.failureIdentities.slice(
1549
- 0,
1550
- 256,
1551
- ),
1552
- },
1553
- };
1554
- }
1555
- resources.currentRevisionId = applied.revision.revisionId;
1556
- }
1557
- const verificationId = `change-${hash(
1558
- input.plan.changeId,
1559
- String(input.deliveryRevision),
1560
- resources.currentRevisionId,
1561
- ...(resources.environmentIdentity
1562
- ? [resources.environmentIdentity]
1563
- : []),
1564
- ).slice(0, 40)}`;
1565
- const sealed = await verifyCumulativeRevision({
1566
- workspaceStore: resources.workspaces,
1567
- revisionId: resources.currentRevisionId,
1568
- verificationId,
1569
- signal: input.signal,
1570
- execute: async () => ({
1571
- ok: true as const,
1572
- exitCode: 0 as const,
1573
- classification: "lifecycle-verified",
1574
- }),
1575
- });
1576
- if (!sealed.ok) return { kind: "paused", code: sealed.code };
1577
- resources.verificationFact = sealed.fact;
1578
- return {
1579
- kind: "verified",
1580
- verificationId: sealed.verificationId,
1581
- currentWorkspaceRevisionId: resources.currentRevisionId,
1582
- };
1583
- }
1584
- const verificationId = `change-${hash(
1585
- input.plan.changeId,
1586
- String(input.deliveryRevision),
1587
- ...(resources.environmentIdentity ? [resources.environmentIdentity] : []),
1588
- ).slice(0, 40)}`;
1589
- const result = await verifyCumulativeRevision({
1590
- workspaceStore: resources.workspaces,
1591
- revisionId: resources.currentRevisionId,
1592
- verificationId,
1593
- signal: input.signal,
1594
- execute: ({ root, signal }) =>
1595
- this.#verifyChange({
1596
- runId: input.runId,
1597
- deliveryRevision: input.deliveryRevision,
1598
- root,
1599
- plan: structuredClone(input.plan),
1600
- signal: signal ?? input.signal,
1601
- }),
1602
- });
1603
- if (!result.ok) {
1604
- return {
1605
- kind:
1606
- result.kind === "approval-boundary"
1607
- ? "approval-needed"
1608
- : result.kind === "verification"
1609
- ? "retryable"
1610
- : "paused",
1611
- code: result.code,
1612
- };
1613
- }
1614
- resources.verificationFact = result.fact;
1615
- return { kind: "verified", verificationId: result.verificationId };
1616
- }
1617
- }