@microck/canonfig 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.
- package/LICENSE +21 -0
- package/README.md +263 -0
- package/dist/agent/agent-resolution.errors.js +42 -0
- package/dist/agent/agent-resolution.layer.js +204 -0
- package/dist/agent/agent-resolution.service.js +2259 -0
- package/dist/agent/agent-resolution.types.js +1 -0
- package/dist/agent/controlled-executor.js +704 -0
- package/dist/agent/harness-adapters.js +85 -0
- package/dist/cli/cli.js +618 -0
- package/dist/cli/exit-codes.js +28 -0
- package/dist/cli/follower-commands.js +3 -0
- package/dist/cli/render.js +56 -0
- package/dist/cli/source-commands.js +5 -0
- package/dist/domain/brand.js +29 -0
- package/dist/domain/identity.js +31 -0
- package/dist/domain/npm-package-spec.js +186 -0
- package/dist/domain/profile.js +950 -0
- package/dist/domain/recipe-versions.js +297 -0
- package/dist/domain/resource.js +259 -0
- package/dist/domain/synchronization.js +346 -0
- package/dist/enrollment/enrollment.errors.js +43 -0
- package/dist/enrollment/enrollment.layer.js +724 -0
- package/dist/enrollment/enrollment.service.js +3 -0
- package/dist/enrollment/enrollment.types.js +59 -0
- package/dist/enrollment/follower-client.js +585 -0
- package/dist/enrollment/source-server.js +313 -0
- package/dist/machine/linux.layer.js +1183 -0
- package/dist/machine/machine-state.errors.js +52 -0
- package/dist/machine/machine-state.service.js +3 -0
- package/dist/machine/machine-state.types.js +1 -0
- package/dist/machine/macos.layer.js +470 -0
- package/dist/machine/windows.layer.js +879 -0
- package/dist/profile/discovery.js +740 -0
- package/dist/profile/profile-catalog.errors.js +50 -0
- package/dist/profile/profile-catalog.layer.js +20 -0
- package/dist/profile/profile-catalog.service.js +7 -0
- package/dist/profile/profile-codec.js +153 -0
- package/dist/profile/publication.js +298 -0
- package/dist/profile/tool-catalog.js +384 -0
- package/dist/runtime/doctor.js +306 -0
- package/dist/runtime/layers.js +706 -0
- package/dist/runtime/main.js +38 -0
- package/dist/schedule/linux-schedule.js +24 -0
- package/dist/schedule/macos-schedule.js +25 -0
- package/dist/schedule/schedule-manager.errors.js +17 -0
- package/dist/schedule/schedule-manager.layer.js +205 -0
- package/dist/schedule/schedule-manager.service.js +3 -0
- package/dist/schedule/schedule-manager.types.js +114 -0
- package/dist/schedule/windows-schedule.js +25 -0
- package/dist/state/state-repository.errors.js +55 -0
- package/dist/state/state-repository.layer.js +1507 -0
- package/dist/state/state-repository.service.js +3 -0
- package/dist/state/state-repository.types.js +1 -0
- package/dist/state/state-schema.js +298 -0
- package/dist/synchronization/config-codec.js +97 -0
- package/dist/synchronization/executor.js +700 -0
- package/dist/synchronization/follower-orchestration.js +939 -0
- package/dist/synchronization/follower-sync-config.js +81 -0
- package/dist/synchronization/npm-artifact.js +670 -0
- package/dist/synchronization/planner.js +378 -0
- package/dist/synchronization/recovery.js +397 -0
- package/dist/synchronization/resource-executors.js +1198 -0
- package/dist/synchronization/resource-plans.js +645 -0
- package/dist/synchronization/synchronization.errors.js +102 -0
- package/dist/synchronization/synchronization.layer.js +97 -0
- package/dist/synchronization/synchronization.service.js +11 -0
- package/dist/synchronization/synchronization.types.js +1 -0
- package/package.json +66 -0
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import { Clock, Effect, Option, Schema } from "effect";
|
|
2
|
+
import { AgentTaskId, BlobId, ContentDigest, FollowerId, ProfileRevisionId, ResourceId, } from "../domain/brand.js";
|
|
3
|
+
import { ExecutableAuthorizationSchema, PlannedAction as PlannedActionSchema, } from "../domain/synchronization.js";
|
|
4
|
+
import { ScheduleManager } from "../schedule/schedule-manager.service.js";
|
|
5
|
+
import { canonicalJson, sha256Hex } from "../profile/profile-codec.js";
|
|
6
|
+
import { StateRepository } from "../state/state-repository.service.js";
|
|
7
|
+
import { executionContexts, executionLimits, executeSynchronizationAction, restoreScheduleRollbackReference, } from "./executor.js";
|
|
8
|
+
import { desiredResourceDigest } from "./resource-plans.js";
|
|
9
|
+
import { restoreRollbackReference, verifyResource, } from "./resource-executors.js";
|
|
10
|
+
import { RecoveryIntegrityError, RecoveryRunNotFoundError, } from "./synchronization.errors.js";
|
|
11
|
+
const PersistedPlanBody = Schema.Struct({
|
|
12
|
+
revision: ProfileRevisionId,
|
|
13
|
+
follower: FollowerId,
|
|
14
|
+
requiredBlobs: Schema.Array(BlobId),
|
|
15
|
+
actions: Schema.Array(PlannedActionSchema),
|
|
16
|
+
agentTasks: Schema.Array(Schema.Struct({
|
|
17
|
+
id: AgentTaskId,
|
|
18
|
+
resource: ResourceId,
|
|
19
|
+
summary: Schema.NonEmptyString,
|
|
20
|
+
desiredOutcome: Schema.NonEmptyString,
|
|
21
|
+
observedEvidence: Schema.Array(Schema.String),
|
|
22
|
+
allowedPaths: Schema.Array(Schema.String),
|
|
23
|
+
allowedExecutables: Schema.Array(Schema.String),
|
|
24
|
+
executableAuthorizations: Schema.optional(Schema.Array(ExecutableAuthorizationSchema)),
|
|
25
|
+
allowedOrigins: Schema.Array(Schema.String),
|
|
26
|
+
forbidden: Schema.Array(Schema.Literals([
|
|
27
|
+
"elevation",
|
|
28
|
+
"login",
|
|
29
|
+
"restart",
|
|
30
|
+
"reboot",
|
|
31
|
+
])),
|
|
32
|
+
timeLimitSeconds: Schema.Int,
|
|
33
|
+
outputLimitBytes: Schema.Int,
|
|
34
|
+
verification: Schema.Struct({
|
|
35
|
+
command: Schema.Array(Schema.String),
|
|
36
|
+
}),
|
|
37
|
+
})),
|
|
38
|
+
});
|
|
39
|
+
const now = Effect.map(Clock.currentTimeMillis, (milliseconds) => new Date(milliseconds).toISOString());
|
|
40
|
+
const integrityError = (recovery, message) => new RecoveryIntegrityError({ run: recovery.run.id, message });
|
|
41
|
+
const hydratePlan = (recovery) => Effect.gen(function* () {
|
|
42
|
+
const decoded = yield* Effect.try({
|
|
43
|
+
try: () => JSON.parse(recovery.run.plan.encoded),
|
|
44
|
+
catch: (error) => integrityError(recovery, `persisted plan encoding is malformed: ${String(error)}`),
|
|
45
|
+
}).pipe(Effect.flatMap(Schema.decodeUnknownEffect(PersistedPlanBody)), Effect.mapError((error) => error instanceof RecoveryIntegrityError
|
|
46
|
+
? error
|
|
47
|
+
: integrityError(recovery, `persisted plan encoding is invalid: ${String(error)}`)));
|
|
48
|
+
const canonical = canonicalJson(Schema.decodeUnknownSync(Schema.MutableJson)(decoded));
|
|
49
|
+
const computedDigest = Schema.decodeUnknownSync(ContentDigest)(sha256Hex(recovery.run.plan.encoded));
|
|
50
|
+
if (canonical !== recovery.run.plan.encoded
|
|
51
|
+
|| recovery.run.plan.digest !== computedDigest
|
|
52
|
+
|| decoded.revision !== recovery.run.revision
|
|
53
|
+
|| decoded.follower !== recovery.run.follower
|
|
54
|
+
|| JSON.stringify(decoded.actions) !== JSON.stringify(recovery.run.plan.actions)) {
|
|
55
|
+
return yield* integrityError(recovery, "persisted plan identity or actions do not match its canonical encoding");
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
...recovery.run.plan,
|
|
59
|
+
digest: computedDigest,
|
|
60
|
+
requiredBlobs: decoded.requiredBlobs,
|
|
61
|
+
agentTasks: decoded.agentTasks,
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
const validateJournal = (recovery, actions) => Effect.gen(function* () {
|
|
65
|
+
const planned = new Set(actions.map((action) => action.id));
|
|
66
|
+
const events = new Map();
|
|
67
|
+
for (let index = 0; index < recovery.actions.length; index += 1) {
|
|
68
|
+
const event = recovery.actions[index];
|
|
69
|
+
if (event.ordinal !== index || !planned.has(event.action)) {
|
|
70
|
+
return yield* integrityError(recovery, "action journal order or action identity is invalid");
|
|
71
|
+
}
|
|
72
|
+
const actionEvents = events.get(event.action) ?? [];
|
|
73
|
+
actionEvents.push(event);
|
|
74
|
+
events.set(event.action, actionEvents);
|
|
75
|
+
}
|
|
76
|
+
const latest = new Map();
|
|
77
|
+
for (const action of actions) {
|
|
78
|
+
const actionEvents = events.get(action.id);
|
|
79
|
+
if (actionEvents === undefined
|
|
80
|
+
|| actionEvents[0]?.state !== "pending"
|
|
81
|
+
|| actionEvents[0]?.attempt !== 0
|
|
82
|
+
|| actionEvents.filter((event) => event.state === "pending").length !== 1) {
|
|
83
|
+
return yield* integrityError(recovery, `action ${action.id} does not have one valid initial journal event`);
|
|
84
|
+
}
|
|
85
|
+
const terminal = actionEvents.findIndex((event) => event.state === "succeeded" || event.state === "skipped");
|
|
86
|
+
if (terminal >= 0 && terminal !== actionEvents.length - 1) {
|
|
87
|
+
return yield* integrityError(recovery, `terminal action ${action.id} has later journal events`);
|
|
88
|
+
}
|
|
89
|
+
const last = actionEvents.at(-1);
|
|
90
|
+
if (last.state === "succeeded"
|
|
91
|
+
&& last.verification?.status !== "passed") {
|
|
92
|
+
return yield* integrityError(recovery, `succeeded action ${action.id} lacks passing verification evidence`);
|
|
93
|
+
}
|
|
94
|
+
latest.set(action.id, last);
|
|
95
|
+
}
|
|
96
|
+
return latest;
|
|
97
|
+
});
|
|
98
|
+
const baseRevision = (revision) => ({
|
|
99
|
+
id: revision.id,
|
|
100
|
+
profileId: revision.profileId,
|
|
101
|
+
sequence: revision.sequence,
|
|
102
|
+
canonicalBytes: revision.canonicalBytes,
|
|
103
|
+
digest: revision.digest,
|
|
104
|
+
signature: revision.signature,
|
|
105
|
+
publishedAt: revision.publishedAt,
|
|
106
|
+
resources: revision.resources
|
|
107
|
+
.filter((resource) => !revision.removedResources?.includes(resource.id))
|
|
108
|
+
.map((resource) => ({
|
|
109
|
+
id: resource.id,
|
|
110
|
+
kind: resource.kind,
|
|
111
|
+
policy: resource.policy,
|
|
112
|
+
target: resource.target,
|
|
113
|
+
groups: resource.groups,
|
|
114
|
+
dependsOn: resource.dependsOn,
|
|
115
|
+
blobs: resource.blobs,
|
|
116
|
+
})),
|
|
117
|
+
groups: revision.groups,
|
|
118
|
+
scheduleDefault: revision.scheduleDefault,
|
|
119
|
+
});
|
|
120
|
+
const evidence = (verification) => {
|
|
121
|
+
const base = {
|
|
122
|
+
status: verification.passed ? "passed" : "failed",
|
|
123
|
+
method: verification.method,
|
|
124
|
+
};
|
|
125
|
+
const withDigest = verification.observedDigest === undefined
|
|
126
|
+
? base
|
|
127
|
+
: {
|
|
128
|
+
...base,
|
|
129
|
+
observedDigest: Schema.decodeUnknownSync(ContentDigest)(verification.observedDigest),
|
|
130
|
+
};
|
|
131
|
+
return verification.exitCode === undefined
|
|
132
|
+
? withDigest
|
|
133
|
+
: { ...withDigest, exitCode: verification.exitCode };
|
|
134
|
+
};
|
|
135
|
+
const appendJournal = (run, action, state, attempt, verification, rollbackReference) => Effect.gen(function* () {
|
|
136
|
+
const repository = yield* StateRepository;
|
|
137
|
+
const base = {
|
|
138
|
+
run,
|
|
139
|
+
action: action.id,
|
|
140
|
+
state,
|
|
141
|
+
recordedAt: yield* now,
|
|
142
|
+
attempt,
|
|
143
|
+
};
|
|
144
|
+
yield* repository.journalAction({
|
|
145
|
+
...base,
|
|
146
|
+
verification,
|
|
147
|
+
rollbackReference,
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
const preservedOutcome = (input, action) => {
|
|
151
|
+
const detail = action.detail;
|
|
152
|
+
if (detail.kind === "human-action") {
|
|
153
|
+
return {
|
|
154
|
+
kind: "human",
|
|
155
|
+
human: {
|
|
156
|
+
reason: detail.reason,
|
|
157
|
+
instructions: detail.instructions,
|
|
158
|
+
resource: action.resource,
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
if (detail.kind === "agent-task") {
|
|
163
|
+
return {
|
|
164
|
+
kind: "human",
|
|
165
|
+
human: {
|
|
166
|
+
reason: `Bounded agent task requires resolution: ${detail.summary}`,
|
|
167
|
+
instructions: `Resolve task ${detail.taskId} under the configured agent policy, then rerun synchronization.`,
|
|
168
|
+
resource: action.resource,
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
if (detail.kind === "drift-conflict") {
|
|
173
|
+
const previous = input.appliedResources?.find((record) => record.resource === action.resource);
|
|
174
|
+
return {
|
|
175
|
+
kind: "drift",
|
|
176
|
+
drift: {
|
|
177
|
+
resource: action.resource,
|
|
178
|
+
target: detail.target,
|
|
179
|
+
desiredDigest: detail.desiredDigest,
|
|
180
|
+
observedDigest: detail.observedDigest,
|
|
181
|
+
lastAppliedDigest: previous?.digest ?? detail.desiredDigest,
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
return { kind: "failed", reason: `invalid skipped action ${action.id}` };
|
|
186
|
+
};
|
|
187
|
+
const uncertainInstaller = (input, state, action, attempt) => verifyResource(state).pipe(Effect.flatMap((verification) => {
|
|
188
|
+
const observed = evidence(verification);
|
|
189
|
+
if (verification.passed) {
|
|
190
|
+
return appendJournal(input.id, action, "succeeded", attempt, observed).pipe(Effect.as({
|
|
191
|
+
kind: "verified",
|
|
192
|
+
resource: action.resource,
|
|
193
|
+
}));
|
|
194
|
+
}
|
|
195
|
+
return appendJournal(input.id, action, "skipped", attempt, observed).pipe(Effect.as({
|
|
196
|
+
kind: "human",
|
|
197
|
+
human: {
|
|
198
|
+
reason: `Installer state is uncertain for ${action.resource}`,
|
|
199
|
+
instructions: "Verify or complete the external installation manually, then rerun recovery.",
|
|
200
|
+
resource: action.resource,
|
|
201
|
+
},
|
|
202
|
+
}));
|
|
203
|
+
}), Effect.catch(() => Effect.succeed({
|
|
204
|
+
kind: "human",
|
|
205
|
+
human: {
|
|
206
|
+
reason: `Installer state is uncertain for ${action.resource}`,
|
|
207
|
+
instructions: "Verify or complete the external installation manually, then rerun recovery.",
|
|
208
|
+
resource: action.resource,
|
|
209
|
+
},
|
|
210
|
+
})));
|
|
211
|
+
/**
|
|
212
|
+
* Resume one active run from repository evidence. The persisted plan remains
|
|
213
|
+
* authoritative; hydrated revision content and artifacts are integrity inputs.
|
|
214
|
+
*/
|
|
215
|
+
export const recoverSynchronizationPlan = (recoveryInput) => Effect.gen(function* () {
|
|
216
|
+
const repository = yield* StateRepository;
|
|
217
|
+
const recovery = yield* repository.loadRecovery(recoveryInput.follower);
|
|
218
|
+
if (recovery === undefined) {
|
|
219
|
+
return yield* new RecoveryRunNotFoundError({
|
|
220
|
+
follower: recoveryInput.follower,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
const plan = yield* hydratePlan(recovery);
|
|
224
|
+
const persistedRevision = yield* repository.getRevision(recovery.run.revision);
|
|
225
|
+
if (recoveryInput.revision.id !== recovery.run.revision
|
|
226
|
+
|| JSON.stringify(baseRevision(recoveryInput.revision))
|
|
227
|
+
!== JSON.stringify(persistedRevision)) {
|
|
228
|
+
return yield* integrityError(recovery, "hydrated revision does not match the persisted run revision");
|
|
229
|
+
}
|
|
230
|
+
const latest = yield* validateJournal(recovery, plan.actions);
|
|
231
|
+
const recoveryAppliedResources = [
|
|
232
|
+
...new Map([
|
|
233
|
+
...recovery.appliedResources,
|
|
234
|
+
...recovery.removedResources,
|
|
235
|
+
].map((record) => [record.resource, record])).values(),
|
|
236
|
+
];
|
|
237
|
+
const input = {
|
|
238
|
+
id: recovery.run.id,
|
|
239
|
+
plan,
|
|
240
|
+
revision: recoveryInput.revision,
|
|
241
|
+
appliedResources: recoveryAppliedResources,
|
|
242
|
+
artifacts: recoveryInput.artifacts,
|
|
243
|
+
knownSecrets: recoveryInput.knownSecrets,
|
|
244
|
+
limits: recoveryInput.limits,
|
|
245
|
+
agent: recoveryInput.agent,
|
|
246
|
+
agentResolution: recoveryInput.agentResolution,
|
|
247
|
+
};
|
|
248
|
+
const states = yield* executionContexts(input, executionLimits(input));
|
|
249
|
+
const verified = new Set();
|
|
250
|
+
const removedResources = new Set();
|
|
251
|
+
const human = [];
|
|
252
|
+
const drift = recovery.drift.map((entry) => entry.conflict);
|
|
253
|
+
let failedReason;
|
|
254
|
+
for (const state of states) {
|
|
255
|
+
const last = latest.get(state.action.id);
|
|
256
|
+
const attempt = Math.max(1, last.attempt + 1);
|
|
257
|
+
const scheduleManager = state.context.resource.kind === "schedule"
|
|
258
|
+
? Option.getOrUndefined(yield* Effect.serviceOption(ScheduleManager))
|
|
259
|
+
: undefined;
|
|
260
|
+
let result;
|
|
261
|
+
if (state.action.detail.kind === "schedule-default") {
|
|
262
|
+
// A native scheduler mutation is an external side effect. Re-run the
|
|
263
|
+
// idempotent action on recovery, including after a previously
|
|
264
|
+
// journaled success, so a restart cannot trust stale scheduler state.
|
|
265
|
+
if (last.state === "running" || last.state === "failed") {
|
|
266
|
+
const rollbackReference = [...recovery.actions].reverse().find((event) => event.action === state.action.id && event.rollbackReference !== undefined)?.rollbackReference;
|
|
267
|
+
if (rollbackReference !== undefined) {
|
|
268
|
+
const manager = Option.getOrUndefined(yield* Effect.serviceOption(ScheduleManager));
|
|
269
|
+
if (manager === undefined) {
|
|
270
|
+
return yield* integrityError(recovery, `cannot restore schedule action ${state.action.id}: native scheduler is unavailable`);
|
|
271
|
+
}
|
|
272
|
+
yield* restoreScheduleRollbackReference(state.context, rollbackReference, manager).pipe(Effect.mapError((error) => integrityError(recovery, `cannot restore schedule action ${state.action.id}: ${String(error)}`)));
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
result = yield* executeSynchronizationAction(input, state, attempt);
|
|
276
|
+
}
|
|
277
|
+
else if (last.state === "skipped") {
|
|
278
|
+
result = preservedOutcome(input, state.action);
|
|
279
|
+
}
|
|
280
|
+
else if (last.state === "succeeded"
|
|
281
|
+
&& state.action.detail.kind === "transfer-blob") {
|
|
282
|
+
result = { kind: "verified" };
|
|
283
|
+
}
|
|
284
|
+
else if (state.action.detail.kind === "install-tool"
|
|
285
|
+
&& last.state !== "pending") {
|
|
286
|
+
result = yield* uncertainInstaller(input, state.context, state.action, attempt);
|
|
287
|
+
}
|
|
288
|
+
else if (state.action.detail.kind === "remove-resource"
|
|
289
|
+
&& last.state === "succeeded") {
|
|
290
|
+
result = yield* executeSynchronizationAction(input, state, attempt);
|
|
291
|
+
}
|
|
292
|
+
else if (last.state === "succeeded") {
|
|
293
|
+
const verification = yield* verifyResource(state.context, scheduleManager).pipe(Effect.mapError((error) => integrityError(recovery, `cannot reverify ${state.action.id}: ${String(error)}`)));
|
|
294
|
+
if (verification.passed) {
|
|
295
|
+
result = { kind: "verified", resource: state.action.resource };
|
|
296
|
+
}
|
|
297
|
+
else if (last.rollbackReference !== undefined) {
|
|
298
|
+
yield* restoreRollbackReference(state.context, last.rollbackReference).pipe(Effect.mapError((error) => integrityError(recovery, `cannot restore ${state.action.id}: ${String(error)}`)));
|
|
299
|
+
result = yield* executeSynchronizationAction(input, state, attempt);
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
result = {
|
|
303
|
+
kind: "failed",
|
|
304
|
+
reason: `previously completed action ${state.action.id} no longer verifies`,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
const rollbackReference = [...recovery.actions].reverse().find((event) => event.action === state.action.id && event.rollbackReference !== undefined)?.rollbackReference;
|
|
310
|
+
if ((last.state === "running" || last.state === "failed")
|
|
311
|
+
&& rollbackReference !== undefined) {
|
|
312
|
+
yield* restoreRollbackReference(state.context, rollbackReference).pipe(Effect.mapError((error) => integrityError(recovery, `cannot restore ${state.action.id}: ${String(error)}`)));
|
|
313
|
+
}
|
|
314
|
+
result = yield* executeSynchronizationAction(input, state, attempt);
|
|
315
|
+
}
|
|
316
|
+
if (result.resource !== undefined)
|
|
317
|
+
verified.add(result.resource);
|
|
318
|
+
if (result.resource !== undefined
|
|
319
|
+
&& recoveryInput.revision.removedResources?.includes(result.resource) === true) {
|
|
320
|
+
removedResources.add(result.resource);
|
|
321
|
+
}
|
|
322
|
+
if (result.human !== undefined)
|
|
323
|
+
human.push(result.human);
|
|
324
|
+
if (result.drift !== undefined
|
|
325
|
+
&& !drift.some((entry) => entry.resource === result.drift?.resource)) {
|
|
326
|
+
drift.push(result.drift);
|
|
327
|
+
}
|
|
328
|
+
if (result.reason !== undefined)
|
|
329
|
+
failedReason = result.reason;
|
|
330
|
+
if (result.kind !== "verified")
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
const outcome = failedReason !== undefined
|
|
334
|
+
? { outcome: "Failed", run: input.id, reason: failedReason }
|
|
335
|
+
: drift.length > 0
|
|
336
|
+
? { outcome: "FollowerDrift", run: input.id, conflicts: drift }
|
|
337
|
+
: human.length > 0
|
|
338
|
+
? { outcome: "HumanActionRequired", run: input.id, actions: human }
|
|
339
|
+
: {
|
|
340
|
+
outcome: "Converged",
|
|
341
|
+
run: input.id,
|
|
342
|
+
verified: [...verified].sort(),
|
|
343
|
+
};
|
|
344
|
+
const appliedResources = [];
|
|
345
|
+
if (outcome.outcome === "Converged") {
|
|
346
|
+
const appliedAt = yield* now;
|
|
347
|
+
const desired = new Map(recoveryInput.revision.desired.map((entry) => [
|
|
348
|
+
entry.resource,
|
|
349
|
+
entry.desired,
|
|
350
|
+
]));
|
|
351
|
+
const resourceById = new Map(recoveryInput.revision.resources.map((resource) => [
|
|
352
|
+
resource.id,
|
|
353
|
+
resource,
|
|
354
|
+
]));
|
|
355
|
+
for (const resource of outcome.verified) {
|
|
356
|
+
if (removedResources.has(resource))
|
|
357
|
+
continue;
|
|
358
|
+
const value = desired.get(resource);
|
|
359
|
+
const digest = value === undefined
|
|
360
|
+
? undefined
|
|
361
|
+
: desiredResourceDigest(value);
|
|
362
|
+
if (digest !== undefined) {
|
|
363
|
+
const ownedFiles = value?.kind === "directory" || value?.kind === "skill"
|
|
364
|
+
? value.files.map((file) => ({
|
|
365
|
+
path: file.path,
|
|
366
|
+
digest: file.digest,
|
|
367
|
+
executable: file.executable,
|
|
368
|
+
}))
|
|
369
|
+
: undefined;
|
|
370
|
+
appliedResources.push({
|
|
371
|
+
resource,
|
|
372
|
+
revision: recoveryInput.revision.id,
|
|
373
|
+
digest,
|
|
374
|
+
appliedAt,
|
|
375
|
+
kind: resourceById.get(resource)?.kind,
|
|
376
|
+
policy: resourceById.get(resource)?.policy,
|
|
377
|
+
target: resourceById.get(resource)?.target,
|
|
378
|
+
executable: value?.kind === "file" ? value.executable : undefined,
|
|
379
|
+
symlinkTo: value?.kind === "file" ? value.symlinkTo : undefined,
|
|
380
|
+
ownedFiles,
|
|
381
|
+
ownedKeys: value?.kind === "config" ? value.keys : undefined,
|
|
382
|
+
configFormat: value?.kind === "config" ? value.format : undefined,
|
|
383
|
+
schedule: value?.kind === "schedule"
|
|
384
|
+
? value.schedule
|
|
385
|
+
: undefined,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return {
|
|
391
|
+
outcome,
|
|
392
|
+
appliedResources,
|
|
393
|
+
removedResources: outcome.outcome === "Converged"
|
|
394
|
+
? [...removedResources].sort()
|
|
395
|
+
: [],
|
|
396
|
+
};
|
|
397
|
+
});
|