@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,378 @@
1
+ import { Effect, Schema } from "effect";
2
+ import { ActionId, AgentTaskId, BlobId, ResourceId, } from "../domain/brand.js";
3
+ import { validateResourcePathConflicts, } from "../domain/profile.js";
4
+ import { policyCompatibleWithKind } from "../domain/resource.js";
5
+ import { canonicalJson, sha256Hex } from "../profile/profile-codec.js";
6
+ import { DuplicatePlannerInputError, MissingBlobMetadataError, MissingDesiredResourceError, MissingObservedResourceError, PlannerDependencyCycleError, PlannerConflictingResourcePathError, PlannerInvalidRecipeError, PlannerInvalidResourcePathError, PlannerMissingDependencyError, PlannerPolicyKindMismatchError, PlannerResourceKindMismatchError, PlannerVerificationContentMismatchError, PlannerVerificationKindMismatchError, } from "./synchronization.errors.js";
7
+ import { isMissingAutomaticRecipeVersion, recipeValidationError, } from "../domain/recipe-versions.js";
8
+ import { planRemoved, planResource, } from "./resource-plans.js";
9
+ const compareText = (left, right) => {
10
+ if (left < right)
11
+ return -1;
12
+ if (left > right)
13
+ return 1;
14
+ return 0;
15
+ };
16
+ const sortedUnique = (values) => [...new Set(values)].sort(compareText);
17
+ const actionId = (value) => Schema.decodeUnknownSync(ActionId)(value);
18
+ const agentTaskId = (value) => Schema.decodeUnknownSync(AgentTaskId)(value);
19
+ const indexUnique = (collection, entries) => {
20
+ const indexed = new Map();
21
+ for (const [id, value] of entries) {
22
+ if (indexed.has(id))
23
+ return new DuplicatePlannerInputError({ collection, id });
24
+ indexed.set(id, value);
25
+ }
26
+ return indexed;
27
+ };
28
+ const isDuplicateInputError = (value) => value instanceof DuplicatePlannerInputError;
29
+ const indexInput = (input) => {
30
+ const resources = indexUnique("revision.resources", input.revision.resources.map((resource) => [resource.id, resource]));
31
+ if (isDuplicateInputError(resources))
32
+ return { ok: false, error: resources };
33
+ const desired = indexUnique("revision.desired", input.revision.desired.map((entry) => [entry.resource, entry.desired]));
34
+ if (isDuplicateInputError(desired))
35
+ return { ok: false, error: desired };
36
+ const verification = indexUnique("revision.desired", input.revision.desired.map((entry) => [entry.resource, entry.verification]));
37
+ if (isDuplicateInputError(verification))
38
+ return { ok: false, error: verification };
39
+ const observed = indexUnique("observedState.resources", input.observedState.resources.map((entry) => [entry.resource, entry.observed]));
40
+ if (isDuplicateInputError(observed))
41
+ return { ok: false, error: observed };
42
+ const overlays = indexUnique("localOverlay", input.localOverlay.map((entry) => [entry.resource, entry.keys]));
43
+ if (isDuplicateInputError(overlays))
44
+ return { ok: false, error: overlays };
45
+ const applied = indexUnique("appliedResources", input.appliedResources.map((entry) => [entry.resource, entry]));
46
+ if (isDuplicateInputError(applied))
47
+ return { ok: false, error: applied };
48
+ const blobs = indexUnique("revision.blobs", input.revision.blobs.map((entry) => [entry.id, entry]));
49
+ if (isDuplicateInputError(blobs))
50
+ return { ok: false, error: blobs };
51
+ return {
52
+ ok: true,
53
+ indexed: { resources, desired, verification, observed, overlays, applied, blobs },
54
+ };
55
+ };
56
+ const verificationCompatibleWithDesired = (kind, desired, method) => {
57
+ if (kind === "file") {
58
+ if (desired.kind !== "file")
59
+ return false;
60
+ return desired.symlinkTo === undefined
61
+ ? method === "digest"
62
+ : method === "symlink";
63
+ }
64
+ switch (kind) {
65
+ case "directory":
66
+ case "config":
67
+ case "skill":
68
+ return method === "digest" || method === "command";
69
+ case "tool":
70
+ return method === "executable-present" || method === "command";
71
+ case "credential":
72
+ return method === "credential-present" || method === "command";
73
+ case "schedule":
74
+ return method === "command";
75
+ }
76
+ };
77
+ const orderResources = (resources) => {
78
+ for (const resource of resources.values()) {
79
+ for (const dependency of resource.dependsOn) {
80
+ if (!resources.has(dependency)) {
81
+ return {
82
+ ok: false,
83
+ error: new PlannerMissingDependencyError({
84
+ resource: resource.id,
85
+ dependency,
86
+ }),
87
+ };
88
+ }
89
+ }
90
+ }
91
+ const active = [];
92
+ const complete = new Set();
93
+ const ordered = [];
94
+ const visit = (id) => {
95
+ if (complete.has(id))
96
+ return undefined;
97
+ const activeIndex = active.indexOf(id);
98
+ if (activeIndex >= 0) {
99
+ return new PlannerDependencyCycleError({
100
+ cycle: [...active.slice(activeIndex), id],
101
+ });
102
+ }
103
+ active.push(id);
104
+ const resource = resources.get(id);
105
+ if (resource !== undefined) {
106
+ const dependencies = sortedUnique(resource.dependsOn);
107
+ for (const dependency of dependencies) {
108
+ const error = visit(dependency);
109
+ if (error !== undefined)
110
+ return error;
111
+ }
112
+ ordered.push(resource);
113
+ }
114
+ active.pop();
115
+ complete.add(id);
116
+ return undefined;
117
+ };
118
+ for (const id of [...resources.keys()].sort(compareText)) {
119
+ const error = visit(id);
120
+ if (error !== undefined)
121
+ return { ok: false, error };
122
+ }
123
+ return { ok: true, resources: ordered };
124
+ };
125
+ const validateResourceInputs = (resources, indexed, platform) => {
126
+ for (const resource of resources) {
127
+ if (!policyCompatibleWithKind(resource.kind, resource.policy)) {
128
+ return new PlannerPolicyKindMismatchError({
129
+ resource: resource.id,
130
+ kind: resource.kind,
131
+ policy: resource.policy,
132
+ });
133
+ }
134
+ const desired = indexed.desired.get(resource.id);
135
+ if (desired === undefined)
136
+ return new MissingDesiredResourceError({ resource: resource.id });
137
+ if (desired.kind !== resource.kind) {
138
+ return new PlannerResourceKindMismatchError({
139
+ resource: resource.id,
140
+ publishedKind: resource.kind,
141
+ desiredKind: desired.kind,
142
+ });
143
+ }
144
+ const verification = indexed.verification.get(resource.id);
145
+ if (verification === undefined
146
+ || !verificationCompatibleWithDesired(resource.kind, desired, verification.method)) {
147
+ return new PlannerVerificationKindMismatchError({
148
+ resource: resource.id,
149
+ kind: resource.kind,
150
+ method: verification?.method ?? "missing",
151
+ });
152
+ }
153
+ if (verification !== undefined
154
+ && desired.kind === "file"
155
+ && (desired.symlinkTo === undefined
156
+ && verification.method === "digest"
157
+ && verification.digest !== desired.digest
158
+ || desired.symlinkTo !== undefined
159
+ && verification.method === "symlink"
160
+ && verification.target !== desired.symlinkTo)) {
161
+ return new PlannerVerificationContentMismatchError({
162
+ resource: resource.id,
163
+ kind: resource.kind,
164
+ method: verification.method,
165
+ reason: desired.symlinkTo === undefined
166
+ ? "digest verification does not match authored file content"
167
+ : "symlink verification target does not match authored symlink target",
168
+ });
169
+ }
170
+ if (desired.kind === "tool") {
171
+ for (const recipe of desired.recipes) {
172
+ const reason = recipeValidationError(recipe);
173
+ if (reason !== undefined && !isMissingAutomaticRecipeVersion(recipe)) {
174
+ return new PlannerInvalidRecipeError({
175
+ resource: resource.id,
176
+ method: recipe.method,
177
+ package: recipe.package,
178
+ reason,
179
+ });
180
+ }
181
+ }
182
+ }
183
+ if (!indexed.observed.has(resource.id)) {
184
+ return new MissingObservedResourceError({ resource: resource.id });
185
+ }
186
+ }
187
+ const pathErrors = validateResourcePathConflicts(resources.map((resource) => {
188
+ const desired = indexed.desired.get(resource.id);
189
+ const entries = desired?.kind === "directory" || desired?.kind === "skill"
190
+ ? desired.files.map((file) => file.path)
191
+ : [];
192
+ return {
193
+ id: resource.id,
194
+ kind: resource.kind,
195
+ target: resource.target,
196
+ entries,
197
+ };
198
+ }), platform);
199
+ const pathError = pathErrors[0];
200
+ if (pathError?._tag === "InvalidTargetError") {
201
+ return new PlannerInvalidResourcePathError({
202
+ resource: pathError.id,
203
+ path: pathError.target,
204
+ reason: pathError.reason,
205
+ });
206
+ }
207
+ if (pathError?._tag === "ConflictingResourceTargetError") {
208
+ return new PlannerConflictingResourcePathError({
209
+ resource: pathError.id,
210
+ path: pathError.target,
211
+ conflictsWith: pathError.conflictsWith,
212
+ reason: pathError.reason,
213
+ });
214
+ }
215
+ return undefined;
216
+ };
217
+ const planTransfers = (resources, indexed, availableBlobs) => {
218
+ const blobOwners = new Map();
219
+ for (const resource of resources) {
220
+ for (const blob of resource.blobs) {
221
+ const owner = blobOwners.get(blob);
222
+ if (owner === undefined || compareText(resource.id, owner) < 0) {
223
+ blobOwners.set(blob, resource.id);
224
+ }
225
+ }
226
+ }
227
+ const required = [...blobOwners.keys()]
228
+ .filter((blob) => !availableBlobs.has(blob))
229
+ .sort(compareText);
230
+ const actions = [];
231
+ const byBlob = new Map();
232
+ for (const blob of required) {
233
+ const metadata = indexed.blobs.get(blob);
234
+ const owner = blobOwners.get(blob);
235
+ if (metadata === undefined || owner === undefined) {
236
+ return {
237
+ ok: false,
238
+ error: new MissingBlobMetadataError({
239
+ resource: owner ?? "$unknown",
240
+ blob,
241
+ }),
242
+ };
243
+ }
244
+ const id = actionId(`transfer:${blob}`);
245
+ byBlob.set(blob, id);
246
+ actions.push({
247
+ id,
248
+ resource: Schema.decodeUnknownSync(ResourceId)(owner),
249
+ kind: "transfer-blob",
250
+ detail: { kind: "transfer-blob", blob: metadata.id, bytes: metadata.bytes },
251
+ before: [],
252
+ });
253
+ }
254
+ return {
255
+ ok: true,
256
+ transfer: {
257
+ actions,
258
+ byBlob,
259
+ requiredBlobs: required.map((blob) => Schema.decodeUnknownSync(BlobId)(blob)),
260
+ },
261
+ };
262
+ };
263
+ const materializeDraft = (draft, resource, ordinal, prerequisites) => {
264
+ const id = actionId(`action:${resource.id}:${ordinal}:${draft.kind}`);
265
+ if (draft.detail.kind === "agent-task") {
266
+ const taskId = agentTaskId(`agent:${resource.id}:${ordinal}`);
267
+ const task = draft.task;
268
+ const detail = {
269
+ kind: "agent-task",
270
+ taskId,
271
+ summary: draft.detail.summary,
272
+ };
273
+ return {
274
+ action: {
275
+ id,
276
+ resource: resource.id,
277
+ kind: "agent-task",
278
+ detail,
279
+ before: sortedUnique(prerequisites),
280
+ },
281
+ task: task === undefined
282
+ ? undefined
283
+ : {
284
+ ...task,
285
+ id: taskId,
286
+ },
287
+ };
288
+ }
289
+ return {
290
+ action: {
291
+ id,
292
+ resource: resource.id,
293
+ kind: draft.detail.kind,
294
+ detail: draft.detail,
295
+ before: sortedUnique(prerequisites),
296
+ },
297
+ };
298
+ };
299
+ const encodePlan = (body) => {
300
+ const json = JSON.parse(JSON.stringify(body));
301
+ return canonicalJson(Schema.decodeUnknownSync(Schema.MutableJson)(json));
302
+ };
303
+ const buildPlan = (input, indexed, orderedResources, transfer) => {
304
+ const actions = [...transfer.actions];
305
+ const tasks = [];
306
+ const terminalByResource = new Map();
307
+ const removedResources = new Set(input.revision.removedResources ?? []);
308
+ for (const resource of orderedResources) {
309
+ const desired = indexed.desired.get(resource.id);
310
+ const observed = indexed.observed.get(resource.id);
311
+ if (desired === undefined || observed === undefined)
312
+ continue;
313
+ const context = {
314
+ resource,
315
+ desired,
316
+ observed,
317
+ overlayKeys: indexed.overlays.get(resource.id) ?? [],
318
+ applied: indexed.applied.get(resource.id),
319
+ platform: input.observedState.platform,
320
+ };
321
+ const drafts = removedResources.has(resource.id)
322
+ ? planRemoved(context)
323
+ : planResource(context);
324
+ const resourcePrerequisites = [];
325
+ for (const dependency of sortedUnique(resource.dependsOn)) {
326
+ const terminal = terminalByResource.get(dependency);
327
+ if (terminal !== undefined)
328
+ resourcePrerequisites.push(terminal);
329
+ }
330
+ for (const blob of sortedUnique(resource.blobs)) {
331
+ const transferAction = transfer.byBlob.get(blob);
332
+ if (transferAction !== undefined)
333
+ resourcePrerequisites.push(transferAction);
334
+ }
335
+ let previous;
336
+ for (let ordinal = 0; ordinal < drafts.length; ordinal += 1) {
337
+ const prerequisites = previous === undefined
338
+ ? resourcePrerequisites
339
+ : [...resourcePrerequisites, previous];
340
+ const materialized = materializeDraft(drafts[ordinal], resource, ordinal, prerequisites);
341
+ actions.push(materialized.action);
342
+ if (materialized.task !== undefined)
343
+ tasks.push(materialized.task);
344
+ previous = materialized.action.id;
345
+ }
346
+ if (previous !== undefined)
347
+ terminalByResource.set(resource.id, previous);
348
+ }
349
+ const body = {
350
+ revision: input.revision.id,
351
+ follower: input.follower,
352
+ requiredBlobs: transfer.requiredBlobs,
353
+ actions,
354
+ agentTasks: tasks,
355
+ };
356
+ const encoded = encodePlan(body);
357
+ return {
358
+ ...body,
359
+ digest: sha256Hex(encoded),
360
+ encoded,
361
+ };
362
+ };
363
+ /** Pure planner entry point. Expected contract failures use the typed error channel. */
364
+ export const planSynchronization = (input) => Effect.suspend(() => {
365
+ const indexResult = indexInput(input);
366
+ if (!indexResult.ok)
367
+ return Effect.fail(indexResult.error);
368
+ const orderResult = orderResources(indexResult.indexed.resources);
369
+ if (!orderResult.ok)
370
+ return Effect.fail(orderResult.error);
371
+ const validationError = validateResourceInputs(orderResult.resources, indexResult.indexed, input.observedState.platform);
372
+ if (validationError !== undefined)
373
+ return Effect.fail(validationError);
374
+ const transferResult = planTransfers(orderResult.resources, indexResult.indexed, new Set(input.observedState.availableBlobs));
375
+ if (!transferResult.ok)
376
+ return Effect.fail(transferResult.error);
377
+ return Effect.succeed(buildPlan(input, indexResult.indexed, orderResult.resources, transferResult.transfer));
378
+ });