harnery 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +91 -2
- package/dist/core/work/state.d.ts +7 -0
- package/dist/core/work/state.d.ts.map +1 -1
- package/dist/core/work/state.js +112 -23
- package/dist/core/workflow/child-env.d.ts +10 -6
- package/dist/core/workflow/child-env.d.ts.map +1 -1
- package/dist/core/workflow/child-env.js +15 -7
- package/dist/core/workflow/durable-record.d.ts +7 -0
- package/dist/core/workflow/durable-record.d.ts.map +1 -0
- package/dist/core/workflow/durable-record.js +83 -0
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +208 -29
- package/dist/core/workflow/index.d.ts +2 -0
- package/dist/core/workflow/index.d.ts.map +1 -1
- package/dist/core/workflow/index.js +1 -0
- package/dist/core/workflow/proof.d.ts +4 -0
- package/dist/core/workflow/proof.d.ts.map +1 -1
- package/dist/core/workflow/proof.js +50 -15
- package/dist/core/workflow/run-state.d.ts +3 -0
- package/dist/core/workflow/run-state.d.ts.map +1 -1
- package/dist/core/workflow/run-state.js +25 -13
- package/dist/core/workflow/types.d.ts +10 -0
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/validate.d.ts +10 -1
- package/dist/core/workflow/validate.d.ts.map +1 -1
- package/dist/core/workflow/validate.js +75 -1
- package/dist/core/workflow/workspaces/attestation-error.d.ts +7 -0
- package/dist/core/workflow/workspaces/attestation-error.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/attestation-error.js +25 -0
- package/dist/core/workflow/workspaces/cancellation.d.ts +8 -0
- package/dist/core/workflow/workspaces/cancellation.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/cancellation.js +95 -0
- package/dist/core/workflow/workspaces/cleanup.d.ts +9 -0
- package/dist/core/workflow/workspaces/cleanup.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/cleanup.js +223 -0
- package/dist/core/workflow/workspaces/execution.d.ts +25 -0
- package/dist/core/workflow/workspaces/execution.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/execution.js +316 -0
- package/dist/core/workflow/workspaces/git.d.ts +25 -0
- package/dist/core/workflow/workspaces/git.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/git.js +102 -0
- package/dist/core/workflow/workspaces/index.d.ts +15 -0
- package/dist/core/workflow/workspaces/index.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/index.js +7 -0
- package/dist/core/workflow/workspaces/inspect.d.ts +85 -0
- package/dist/core/workflow/workspaces/inspect.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/inspect.js +255 -0
- package/dist/core/workflow/workspaces/integration.d.ts +24 -0
- package/dist/core/workflow/workspaces/integration.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/integration.js +617 -0
- package/dist/core/workflow/workspaces/leases.d.ts +37 -0
- package/dist/core/workflow/workspaces/leases.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/leases.js +380 -0
- package/dist/core/workflow/workspaces/lifecycle.d.ts +43 -0
- package/dist/core/workflow/workspaces/lifecycle.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/lifecycle.js +622 -0
- package/dist/core/workflow/workspaces/local-git.d.ts +8 -0
- package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/local-git.js +1474 -0
- package/dist/core/workflow/workspaces/paths.d.ts +23 -0
- package/dist/core/workflow/workspaces/paths.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/paths.js +191 -0
- package/dist/core/workflow/workspaces/recovery.d.ts +3 -0
- package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/recovery.js +50 -0
- package/dist/core/workflow/workspaces/state.d.ts +60 -0
- package/dist/core/workflow/workspaces/state.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/state.js +296 -0
- package/dist/core/workflow/workspaces/types.d.ts +445 -0
- package/dist/core/workflow/workspaces/types.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/types.js +2 -0
- package/dist/core/workflow/workspaces/validate.d.ts +14 -0
- package/dist/core/workflow/workspaces/validate.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/validate.js +476 -0
- package/package.json +5 -3
- package/src/commands/workflow.ts +110 -4
- package/src/core/work/state.ts +156 -28
- package/src/core/workflow/child-env.ts +18 -7
- package/src/core/workflow/durable-record.ts +94 -0
- package/src/core/workflow/engine.ts +230 -30
- package/src/core/workflow/index.ts +73 -0
- package/src/core/workflow/proof.ts +70 -22
- package/src/core/workflow/run-state.ts +46 -11
- package/src/core/workflow/types.ts +14 -0
- package/src/core/workflow/validate.ts +75 -1
- package/src/core/workflow/workspaces/attestation-error.ts +30 -0
- package/src/core/workflow/workspaces/cancellation.ts +137 -0
- package/src/core/workflow/workspaces/cleanup.ts +308 -0
- package/src/core/workflow/workspaces/execution.ts +412 -0
- package/src/core/workflow/workspaces/git.ts +125 -0
- package/src/core/workflow/workspaces/index.ts +85 -0
- package/src/core/workflow/workspaces/inspect.ts +421 -0
- package/src/core/workflow/workspaces/integration.ts +829 -0
- package/src/core/workflow/workspaces/leases.ts +495 -0
- package/src/core/workflow/workspaces/lifecycle.ts +792 -0
- package/src/core/workflow/workspaces/local-git.ts +1892 -0
- package/src/core/workflow/workspaces/paths.ts +247 -0
- package/src/core/workflow/workspaces/recovery.ts +56 -0
- package/src/core/workflow/workspaces/state.ts +481 -0
- package/src/core/workflow/workspaces/types.ts +517 -0
- package/src/core/workflow/workspaces/validate.ts +595 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { readWorkflowProof } from "../proof.ts";
|
|
3
|
+
import { readWorkflowRunManifest } from "../run-state.ts";
|
|
4
|
+
import { acquireNoClobberLease } from "./leases.ts";
|
|
5
|
+
import { containsPath } from "./paths.ts";
|
|
6
|
+
import {
|
|
7
|
+
appendCleanupAttempt,
|
|
8
|
+
appendWorkflowJournalEvent,
|
|
9
|
+
readCleanupAttempts,
|
|
10
|
+
readWorkflowSupplement,
|
|
11
|
+
stableDigest,
|
|
12
|
+
workflowRunDir,
|
|
13
|
+
writeWorkflowSupplement,
|
|
14
|
+
} from "./state.ts";
|
|
15
|
+
import type {
|
|
16
|
+
WorkspaceBinding,
|
|
17
|
+
WorkspaceCleanupAttempt,
|
|
18
|
+
WorkspaceCleanupIntent,
|
|
19
|
+
WorkspaceCleanupMode,
|
|
20
|
+
WorkspaceCleanupReceipt,
|
|
21
|
+
WorkspaceCleanupResult,
|
|
22
|
+
WorkspaceProvider,
|
|
23
|
+
} from "./types.ts";
|
|
24
|
+
import { isWorkspaceAttestation, isWorkspaceBoundExecutionEvidence } from "./validate.ts";
|
|
25
|
+
|
|
26
|
+
const CLEANUP_LEASE_STALE_MS = 5 * 60 * 1_000;
|
|
27
|
+
|
|
28
|
+
export interface CleanupWorkspaceInput {
|
|
29
|
+
coordRoot: string;
|
|
30
|
+
runId: string;
|
|
31
|
+
provider: WorkspaceProvider;
|
|
32
|
+
mode?: WorkspaceCleanupMode;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function cleanupWorkspace(
|
|
36
|
+
input: CleanupWorkspaceInput,
|
|
37
|
+
): Promise<WorkspaceCleanupAttempt | WorkspaceCleanupReceipt> {
|
|
38
|
+
const release = acquireCleanupLease(input.coordRoot, input.runId);
|
|
39
|
+
try {
|
|
40
|
+
return await cleanupWorkspaceUnderLease(input);
|
|
41
|
+
} finally {
|
|
42
|
+
release();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function cleanupWorkspaceUnderLease(
|
|
47
|
+
input: CleanupWorkspaceInput,
|
|
48
|
+
): Promise<WorkspaceCleanupAttempt | WorkspaceCleanupReceipt> {
|
|
49
|
+
const manifest = readWorkflowRunManifest(input.coordRoot, input.runId);
|
|
50
|
+
const proof = readWorkflowProof(input.coordRoot, input.runId);
|
|
51
|
+
const binding = manifest.execution.workspace_binding;
|
|
52
|
+
if (
|
|
53
|
+
!binding ||
|
|
54
|
+
!isWorkspaceBoundExecutionEvidence(proof.execution, input.runId) ||
|
|
55
|
+
stableDigest(binding) !== stableDigest(proof.execution.binding)
|
|
56
|
+
) {
|
|
57
|
+
throw new Error("workspace cleanup requires matching terminal execution evidence");
|
|
58
|
+
}
|
|
59
|
+
assertCleanupPathAuthority(binding, manifest.execution.policy?.allowed_paths ?? []);
|
|
60
|
+
|
|
61
|
+
const mode = input.mode ?? "normal";
|
|
62
|
+
const current = await input.provider.attest(binding);
|
|
63
|
+
if (!isWorkspaceAttestation(current, binding)) {
|
|
64
|
+
throw new Error("workspace cleanup provider returned an invalid current attestation");
|
|
65
|
+
}
|
|
66
|
+
const intent = resolveCleanupIntent(input, binding, current, mode);
|
|
67
|
+
for (const attempt of readCleanupAttempts(input.coordRoot, input.runId)) {
|
|
68
|
+
if (
|
|
69
|
+
attempt.operation_id !== intent.operation_id ||
|
|
70
|
+
attempt.binding_id !== binding.binding_id ||
|
|
71
|
+
attempt.binding_sha256 !== intent.binding_sha256 ||
|
|
72
|
+
attempt.mode !== intent.mode ||
|
|
73
|
+
(attempt.attestation !== undefined && !isWorkspaceAttestation(attempt.attestation, binding))
|
|
74
|
+
) {
|
|
75
|
+
throw new Error("workspace cleanup attempt does not match the immutable intent");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const receipt = readWorkflowSupplement<WorkspaceCleanupReceipt>(
|
|
80
|
+
input.coordRoot,
|
|
81
|
+
input.runId,
|
|
82
|
+
"cleanup/receipt.json",
|
|
83
|
+
);
|
|
84
|
+
if (receipt) {
|
|
85
|
+
validateCleanupReceipt(receipt, binding, intent);
|
|
86
|
+
if (!isReleasedAttestation(current)) {
|
|
87
|
+
throw new Error("existing cleanup receipt no longer reattests exact resource absence");
|
|
88
|
+
}
|
|
89
|
+
return receipt;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
appendCleanupAttempt(input.coordRoot, input.runId, {
|
|
93
|
+
schema_version: 1,
|
|
94
|
+
operation_id: intent.operation_id,
|
|
95
|
+
binding_id: binding.binding_id,
|
|
96
|
+
binding_sha256: intent.binding_sha256,
|
|
97
|
+
mode,
|
|
98
|
+
status: "started",
|
|
99
|
+
recorded_at: new Date().toISOString(),
|
|
100
|
+
});
|
|
101
|
+
const result = await input.provider.cleanup(binding, intent);
|
|
102
|
+
validateCleanupResult(result, binding);
|
|
103
|
+
const attempt = appendCleanupAttempt(input.coordRoot, input.runId, {
|
|
104
|
+
schema_version: 1,
|
|
105
|
+
operation_id: intent.operation_id,
|
|
106
|
+
binding_id: binding.binding_id,
|
|
107
|
+
binding_sha256: intent.binding_sha256,
|
|
108
|
+
mode,
|
|
109
|
+
status: result.status,
|
|
110
|
+
branch_deleted: result.branch_deleted,
|
|
111
|
+
attestation: result.attestation,
|
|
112
|
+
reason: result.reason,
|
|
113
|
+
recorded_at: result.recorded_at,
|
|
114
|
+
});
|
|
115
|
+
appendWorkflowJournalEvent(input.coordRoot, input.runId, "workspace.cleanup", {
|
|
116
|
+
binding_id: binding.binding_id,
|
|
117
|
+
provider_id: binding.provider.id,
|
|
118
|
+
operation_id: intent.operation_id,
|
|
119
|
+
attempt: attempt.seq,
|
|
120
|
+
status: attempt.status,
|
|
121
|
+
});
|
|
122
|
+
if (result.status !== "released" && result.status !== "already_released") {
|
|
123
|
+
return attempt;
|
|
124
|
+
}
|
|
125
|
+
if (!result.branch_deleted || !isReleasedAttestation(result.attestation)) {
|
|
126
|
+
throw new Error("workspace cleanup cannot receipt an incomplete resource release");
|
|
127
|
+
}
|
|
128
|
+
const terminal: WorkspaceCleanupReceipt = {
|
|
129
|
+
schema_version: 1,
|
|
130
|
+
receipt_id: `cleanup-${stableDigest({
|
|
131
|
+
operation: intent.operation_id,
|
|
132
|
+
binding: intent.binding_sha256,
|
|
133
|
+
}).slice(0, 24)}`,
|
|
134
|
+
operation_id: intent.operation_id,
|
|
135
|
+
binding_id: binding.binding_id,
|
|
136
|
+
binding_sha256: intent.binding_sha256,
|
|
137
|
+
mode,
|
|
138
|
+
status: result.status,
|
|
139
|
+
branch_deleted: true,
|
|
140
|
+
attestation: result.attestation,
|
|
141
|
+
recorded_at: result.recorded_at,
|
|
142
|
+
};
|
|
143
|
+
writeWorkflowSupplement(input.coordRoot, input.runId, "cleanup/receipt.json", terminal);
|
|
144
|
+
return terminal;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function assertCleanupPathAuthority(
|
|
148
|
+
binding: WorkspaceBinding,
|
|
149
|
+
allowedPaths: readonly string[],
|
|
150
|
+
): void {
|
|
151
|
+
if (
|
|
152
|
+
allowedPaths.length > 0 &&
|
|
153
|
+
(!allowedPaths.some((root) => containsPath(root, binding.workspace_root)) ||
|
|
154
|
+
!allowedPaths.some((root) => containsPath(root, binding.active_root)) ||
|
|
155
|
+
(binding.repository !== undefined &&
|
|
156
|
+
(!allowedPaths.some((root) =>
|
|
157
|
+
containsPath(root, binding.repository!.source_root.realpath),
|
|
158
|
+
) ||
|
|
159
|
+
!allowedPaths.some((root) =>
|
|
160
|
+
containsPath(root, binding.repository!.common_dir.realpath),
|
|
161
|
+
))))
|
|
162
|
+
) {
|
|
163
|
+
throw new Error("workspace cleanup path is outside frozen policy path authority");
|
|
164
|
+
}
|
|
165
|
+
if (
|
|
166
|
+
!containsPath(binding.writable_root.realpath, binding.workspace_root) ||
|
|
167
|
+
!containsPath(binding.writable_root.realpath, binding.active_root)
|
|
168
|
+
) {
|
|
169
|
+
throw new Error("workspace cleanup path is outside frozen writable-root authority");
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function acquireCleanupLease(coordRoot: string, runId: string): () => void {
|
|
174
|
+
const authoritySha256 = stableDigest({ run_id: runId, operation: "cleanup" });
|
|
175
|
+
const lease = acquireNoClobberLease({
|
|
176
|
+
path: join(workflowRunDir(coordRoot, runId), "cleanup", "operation.lease"),
|
|
177
|
+
scope: "cleanup",
|
|
178
|
+
authoritySha256,
|
|
179
|
+
staleAfterMs: CLEANUP_LEASE_STALE_MS,
|
|
180
|
+
metadata: { run_id: runId },
|
|
181
|
+
validateStaleOwner: (owner) =>
|
|
182
|
+
owner.authority_sha256 === authoritySha256 && owner.metadata?.run_id === runId,
|
|
183
|
+
});
|
|
184
|
+
return () => lease.release();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function resolveCleanupIntent(
|
|
188
|
+
input: CleanupWorkspaceInput,
|
|
189
|
+
binding: WorkspaceBinding,
|
|
190
|
+
attestation: WorkspaceCleanupResult["attestation"],
|
|
191
|
+
mode: WorkspaceCleanupMode,
|
|
192
|
+
): WorkspaceCleanupIntent {
|
|
193
|
+
if (!binding.repository) throw new Error("workspace cleanup requires repository identity");
|
|
194
|
+
const bindingSha256 = stableDigest(binding);
|
|
195
|
+
const operationId = `cleanup-${stableDigest({ binding: bindingSha256, mode }).slice(0, 24)}`;
|
|
196
|
+
const prior = readWorkflowSupplement<WorkspaceCleanupIntent>(
|
|
197
|
+
input.coordRoot,
|
|
198
|
+
input.runId,
|
|
199
|
+
"cleanup/intent.json",
|
|
200
|
+
);
|
|
201
|
+
if (prior) {
|
|
202
|
+
if (
|
|
203
|
+
prior.schema_version !== 1 ||
|
|
204
|
+
prior.run_id !== input.runId ||
|
|
205
|
+
prior.binding_id !== binding.binding_id ||
|
|
206
|
+
prior.binding_sha256 !== bindingSha256 ||
|
|
207
|
+
prior.mode !== mode ||
|
|
208
|
+
prior.operation_id !== operationId
|
|
209
|
+
) {
|
|
210
|
+
throw new Error("existing cleanup intent does not match current workspace authority");
|
|
211
|
+
}
|
|
212
|
+
validateCleanupIntentExpected(prior, binding);
|
|
213
|
+
return prior;
|
|
214
|
+
}
|
|
215
|
+
const workspaceRefOid = attestation.repository?.workspace_ref_oid;
|
|
216
|
+
const targetRefOid = attestation.repository?.target_ref_oid;
|
|
217
|
+
if (!workspaceRefOid || !targetRefOid) {
|
|
218
|
+
throw new Error("workspace cleanup cannot freeze exact source and target ref identities");
|
|
219
|
+
}
|
|
220
|
+
const proposed: WorkspaceCleanupIntent = {
|
|
221
|
+
schema_version: 1,
|
|
222
|
+
run_id: input.runId,
|
|
223
|
+
operation_id: operationId,
|
|
224
|
+
binding_id: binding.binding_id,
|
|
225
|
+
binding_sha256: bindingSha256,
|
|
226
|
+
mode,
|
|
227
|
+
expected: {
|
|
228
|
+
worktree_path: binding.workspace_root,
|
|
229
|
+
gitdir: binding.repository.gitdir,
|
|
230
|
+
workspace_ref: binding.repository.workspace_ref,
|
|
231
|
+
workspace_ref_oid: workspaceRefOid,
|
|
232
|
+
target_ref: binding.repository.target_ref,
|
|
233
|
+
target_ref_oid: targetRefOid,
|
|
234
|
+
},
|
|
235
|
+
created_at: new Date().toISOString(),
|
|
236
|
+
};
|
|
237
|
+
writeWorkflowSupplement(input.coordRoot, input.runId, "cleanup/intent.json", proposed);
|
|
238
|
+
return proposed;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function validateCleanupIntentExpected(
|
|
242
|
+
intent: WorkspaceCleanupIntent,
|
|
243
|
+
binding: WorkspaceBinding,
|
|
244
|
+
): void {
|
|
245
|
+
const repository = binding.repository;
|
|
246
|
+
if (
|
|
247
|
+
!repository ||
|
|
248
|
+
intent.expected.worktree_path !== binding.workspace_root ||
|
|
249
|
+
stableDigest(intent.expected.gitdir) !== stableDigest(repository.gitdir) ||
|
|
250
|
+
intent.expected.workspace_ref !== repository.workspace_ref ||
|
|
251
|
+
intent.expected.target_ref !== repository.target_ref ||
|
|
252
|
+
!/^[0-9a-f]{40,64}$/.test(intent.expected.workspace_ref_oid) ||
|
|
253
|
+
!/^[0-9a-f]{40,64}$/.test(intent.expected.target_ref_oid)
|
|
254
|
+
) {
|
|
255
|
+
throw new Error("cleanup intent expected identities do not match the workspace binding");
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function validateCleanupResult(result: WorkspaceCleanupResult, binding: WorkspaceBinding): void {
|
|
260
|
+
if (
|
|
261
|
+
result.schema_version !== 1 ||
|
|
262
|
+
result.binding_id !== binding.binding_id ||
|
|
263
|
+
![
|
|
264
|
+
"released",
|
|
265
|
+
"already_released",
|
|
266
|
+
"preserved_dirty",
|
|
267
|
+
"blocked",
|
|
268
|
+
"unsupported",
|
|
269
|
+
"partial",
|
|
270
|
+
].includes(result.status) ||
|
|
271
|
+
!Number.isFinite(Date.parse(result.recorded_at)) ||
|
|
272
|
+
typeof result.branch_deleted !== "boolean" ||
|
|
273
|
+
!isWorkspaceAttestation(result.attestation, binding) ||
|
|
274
|
+
((result.status === "released" || result.status === "already_released") &&
|
|
275
|
+
!isReleasedAttestation(result.attestation))
|
|
276
|
+
) {
|
|
277
|
+
throw new Error("workspace cleanup provider returned an invalid result");
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function validateCleanupReceipt(
|
|
282
|
+
receipt: WorkspaceCleanupReceipt,
|
|
283
|
+
binding: WorkspaceBinding,
|
|
284
|
+
intent: WorkspaceCleanupIntent,
|
|
285
|
+
): void {
|
|
286
|
+
if (
|
|
287
|
+
receipt.schema_version !== 1 ||
|
|
288
|
+
receipt.operation_id !== intent.operation_id ||
|
|
289
|
+
receipt.binding_id !== binding.binding_id ||
|
|
290
|
+
receipt.binding_sha256 !== intent.binding_sha256 ||
|
|
291
|
+
receipt.mode !== intent.mode ||
|
|
292
|
+
(receipt.status !== "released" && receipt.status !== "already_released") ||
|
|
293
|
+
receipt.branch_deleted !== true ||
|
|
294
|
+
!isWorkspaceAttestation(receipt.attestation, binding) ||
|
|
295
|
+
!isReleasedAttestation(receipt.attestation)
|
|
296
|
+
) {
|
|
297
|
+
throw new Error("workspace cleanup receipt is invalid or mismatched");
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function isReleasedAttestation(attestation: WorkspaceCleanupResult["attestation"]): boolean {
|
|
302
|
+
return (
|
|
303
|
+
attestation.status === "ok" &&
|
|
304
|
+
attestation.provider_drift.length === 0 &&
|
|
305
|
+
attestation.resource_state === "released" &&
|
|
306
|
+
!attestation.workspace_exists
|
|
307
|
+
);
|
|
308
|
+
}
|