@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.
Files changed (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +263 -0
  3. package/dist/agent/agent-resolution.errors.js +42 -0
  4. package/dist/agent/agent-resolution.layer.js +204 -0
  5. package/dist/agent/agent-resolution.service.js +2259 -0
  6. package/dist/agent/agent-resolution.types.js +1 -0
  7. package/dist/agent/controlled-executor.js +704 -0
  8. package/dist/agent/harness-adapters.js +85 -0
  9. package/dist/cli/cli.js +618 -0
  10. package/dist/cli/exit-codes.js +28 -0
  11. package/dist/cli/follower-commands.js +3 -0
  12. package/dist/cli/render.js +56 -0
  13. package/dist/cli/source-commands.js +5 -0
  14. package/dist/domain/brand.js +29 -0
  15. package/dist/domain/identity.js +31 -0
  16. package/dist/domain/npm-package-spec.js +186 -0
  17. package/dist/domain/profile.js +950 -0
  18. package/dist/domain/recipe-versions.js +297 -0
  19. package/dist/domain/resource.js +259 -0
  20. package/dist/domain/synchronization.js +346 -0
  21. package/dist/enrollment/enrollment.errors.js +43 -0
  22. package/dist/enrollment/enrollment.layer.js +724 -0
  23. package/dist/enrollment/enrollment.service.js +3 -0
  24. package/dist/enrollment/enrollment.types.js +59 -0
  25. package/dist/enrollment/follower-client.js +585 -0
  26. package/dist/enrollment/source-server.js +313 -0
  27. package/dist/machine/linux.layer.js +1183 -0
  28. package/dist/machine/machine-state.errors.js +52 -0
  29. package/dist/machine/machine-state.service.js +3 -0
  30. package/dist/machine/machine-state.types.js +1 -0
  31. package/dist/machine/macos.layer.js +470 -0
  32. package/dist/machine/windows.layer.js +879 -0
  33. package/dist/profile/discovery.js +740 -0
  34. package/dist/profile/profile-catalog.errors.js +50 -0
  35. package/dist/profile/profile-catalog.layer.js +20 -0
  36. package/dist/profile/profile-catalog.service.js +7 -0
  37. package/dist/profile/profile-codec.js +153 -0
  38. package/dist/profile/publication.js +298 -0
  39. package/dist/profile/tool-catalog.js +384 -0
  40. package/dist/runtime/doctor.js +306 -0
  41. package/dist/runtime/layers.js +706 -0
  42. package/dist/runtime/main.js +38 -0
  43. package/dist/schedule/linux-schedule.js +24 -0
  44. package/dist/schedule/macos-schedule.js +25 -0
  45. package/dist/schedule/schedule-manager.errors.js +17 -0
  46. package/dist/schedule/schedule-manager.layer.js +205 -0
  47. package/dist/schedule/schedule-manager.service.js +3 -0
  48. package/dist/schedule/schedule-manager.types.js +114 -0
  49. package/dist/schedule/windows-schedule.js +25 -0
  50. package/dist/state/state-repository.errors.js +55 -0
  51. package/dist/state/state-repository.layer.js +1507 -0
  52. package/dist/state/state-repository.service.js +3 -0
  53. package/dist/state/state-repository.types.js +1 -0
  54. package/dist/state/state-schema.js +298 -0
  55. package/dist/synchronization/config-codec.js +97 -0
  56. package/dist/synchronization/executor.js +700 -0
  57. package/dist/synchronization/follower-orchestration.js +939 -0
  58. package/dist/synchronization/follower-sync-config.js +81 -0
  59. package/dist/synchronization/npm-artifact.js +670 -0
  60. package/dist/synchronization/planner.js +378 -0
  61. package/dist/synchronization/recovery.js +397 -0
  62. package/dist/synchronization/resource-executors.js +1198 -0
  63. package/dist/synchronization/resource-plans.js +645 -0
  64. package/dist/synchronization/synchronization.errors.js +102 -0
  65. package/dist/synchronization/synchronization.layer.js +97 -0
  66. package/dist/synchronization/synchronization.service.js +11 -0
  67. package/dist/synchronization/synchronization.types.js +1 -0
  68. 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
+ });