@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,706 @@
1
+ import { createPrivateKey, createPublicKey, sign as signPayload, verify as verifyPayload, } from "node:crypto";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ import { Effect, Layer, Option, Redacted, Schema } from "effect";
6
+ import { SourceSignature } from "../domain/brand.js";
7
+ import { AgentPolicy } from "../domain/identity.js";
8
+ import { AgentResolutionLive } from "../agent/agent-resolution.layer.js";
9
+ import { AgentResolution } from "../agent/agent-resolution.service.js";
10
+ import { EnrollmentLive } from "../enrollment/enrollment.layer.js";
11
+ import { Enrollment } from "../enrollment/enrollment.service.js";
12
+ import { cancelFollowerEnrollment, enrollFollower, getRevisionMetadata, finalizeFollowerEnrollment, listRevisions, } from "../enrollment/follower-client.js";
13
+ import { startSourceServer } from "../enrollment/source-server.js";
14
+ import { decodeMachineProfileJsonc, } from "../domain/profile.js";
15
+ import { MachineState } from "../machine/machine-state.service.js";
16
+ import { linuxMachineStateLayer } from "../machine/linux.layer.js";
17
+ import { macosMachineStateLayer } from "../machine/macos.layer.js";
18
+ import { windowsMachineStateLayer } from "../machine/windows.layer.js";
19
+ import { ProfileCatalog } from "../profile/profile-catalog.service.js";
20
+ import { PublicationSigningError, } from "../profile/profile-catalog.errors.js";
21
+ import { scanDiscovery, } from "../profile/discovery.js";
22
+ import { acceptPublicationProposal, makePublication, } from "../profile/publication.js";
23
+ import { ScheduleManager } from "../schedule/schedule-manager.service.js";
24
+ import { scheduleManagerLayer } from "../schedule/schedule-manager.layer.js";
25
+ import { StateRepository } from "../state/state-repository.service.js";
26
+ import { stateRepositoryLayer } from "../state/state-repository.layer.js";
27
+ import { SynchronizationLive } from "../synchronization/synchronization.layer.js";
28
+ import { Synchronization } from "../synchronization/synchronization.service.js";
29
+ import { defaultScheduledInvocation, } from "../synchronization/follower-sync-config.js";
30
+ import { recoverFollower, synchronizeFollower, } from "../synchronization/follower-orchestration.js";
31
+ import { FollowerCommands, } from "../cli/follower-commands.js";
32
+ import { CliCommandFailure, SourceCommands, } from "../cli/source-commands.js";
33
+ import { doctorFailureCategory, runDoctorProbes, } from "./doctor.js";
34
+ const doctorSourceFromEnvironment = () => {
35
+ const endpoint = process.env.CANONFIG_SOURCE_ENDPOINT;
36
+ const tlsFingerprint = process.env.CANONFIG_SOURCE_TLS_FINGERPRINT;
37
+ const credentialReference = process.env.CANONFIG_SOURCE_CREDENTIAL_REFERENCE;
38
+ if (endpoint === undefined
39
+ && tlsFingerprint === undefined
40
+ && credentialReference === undefined)
41
+ return undefined;
42
+ return {
43
+ endpoint: endpoint ?? "",
44
+ tlsFingerprint: tlsFingerprint ?? "",
45
+ credentialReference: credentialReference ?? "",
46
+ };
47
+ };
48
+ const doctorAgentFromEnvironment = () => {
49
+ const adapter = process.env.CANONFIG_AGENT_ADAPTER;
50
+ const executable = process.env.CANONFIG_AGENT_EXECUTABLE;
51
+ if (adapter === undefined && executable === undefined)
52
+ return undefined;
53
+ return {
54
+ adapter: adapter ?? "",
55
+ executable: executable ?? "",
56
+ };
57
+ };
58
+ const payload = (value) => Schema.decodeUnknownSync(Schema.MutableJson)(JSON.parse(JSON.stringify(value)));
59
+ const categoryForError = (error) => {
60
+ const tag = error._tag ?? "";
61
+ if (/HumanAction/u.test(tag))
62
+ return "human-action-required";
63
+ if (/Drift|Conflict|Immutable|ActiveRun/u.test(tag))
64
+ return "conflict-or-drift";
65
+ if (/Credential|Revoked|Unauthorized|Fingerprint|SourceMismatch/u.test(tag)) {
66
+ return "authentication-or-revocation";
67
+ }
68
+ if (/Transport|Invitation/u.test(tag))
69
+ return "transport";
70
+ if (/Verification|Execution|Apply|Process/u.test(tag)) {
71
+ return "verification-or-apply-failure";
72
+ }
73
+ if (/Invalid|Configuration|NotConfigured|NotInitialized|NotFound/u.test(tag)) {
74
+ return "usage-or-configuration";
75
+ }
76
+ return "internal";
77
+ };
78
+ const commandFailure = (error) => new CliCommandFailure({
79
+ category: categoryForError(error),
80
+ message: error instanceof Error ? error.message : String(error),
81
+ });
82
+ const emptyDiscoveryProposal = {
83
+ resources: [],
84
+ tools: [],
85
+ skills: [],
86
+ evidence: [],
87
+ agentTasks: [],
88
+ scannedPaths: [],
89
+ };
90
+ const readAuthoredProfile = (path) => Effect.tryPromise({
91
+ try: () => readFile(path, "utf8"),
92
+ catch: () => new CliCommandFailure({
93
+ category: "usage-or-configuration",
94
+ message: "authored profile file could not be read",
95
+ }),
96
+ }).pipe(Effect.flatMap((text) => Effect.try({
97
+ try: () => decodeMachineProfileJsonc(text),
98
+ catch: () => new CliCommandFailure({
99
+ category: "usage-or-configuration",
100
+ message: "authored profile file is malformed or invalid",
101
+ }),
102
+ })));
103
+ const mapFailure = (effect) => effect.pipe(Effect.mapError(commandFailure));
104
+ const sourceCommandsLayer = Layer.effect(SourceCommands, Effect.gen(function* () {
105
+ const enrollment = yield* Enrollment;
106
+ const machine = yield* MachineState;
107
+ const profiles = yield* ProfileCatalog;
108
+ const repository = yield* StateRepository;
109
+ const service = {
110
+ initialize: () => mapFailure(enrollment.initializeSource()).pipe(Effect.map(payload)),
111
+ scan: (input) => mapFailure(profiles.scan(input)).pipe(Effect.map(payload)),
112
+ publish: (input) => Effect.gen(function* () {
113
+ const authored = input.profilePath === undefined
114
+ ? undefined
115
+ : yield* readAuthoredProfile(input.profilePath);
116
+ const proposal = input.proposalPath === undefined
117
+ ? emptyDiscoveryProposal
118
+ : yield* mapFailure(profiles.scan({
119
+ files: [{ path: input.proposalPath }],
120
+ }));
121
+ if (authored === undefined && (input.profile === undefined || input.name === undefined)) {
122
+ return yield* new CliCommandFailure({
123
+ category: "usage-or-configuration",
124
+ message: "source publish requires profile metadata or an authored profile file",
125
+ });
126
+ }
127
+ if (authored !== undefined
128
+ && input.profile !== undefined
129
+ && input.profile !== authored.id) {
130
+ return yield* new CliCommandFailure({
131
+ category: "usage-or-configuration",
132
+ message: "authored profile id conflicts with --profile",
133
+ });
134
+ }
135
+ if (authored !== undefined
136
+ && input.name !== undefined
137
+ && input.name !== authored.name) {
138
+ return yield* new CliCommandFailure({
139
+ category: "usage-or-configuration",
140
+ message: "authored profile name conflicts with --name",
141
+ });
142
+ }
143
+ const profile = authored === undefined
144
+ ? {
145
+ id: input.profile,
146
+ name: input.name,
147
+ }
148
+ : {
149
+ id: authored.id,
150
+ name: authored.name,
151
+ groups: authored.groups,
152
+ resources: authored.resources,
153
+ scheduleDefault: authored.scheduleDefault,
154
+ };
155
+ const now = new Date().toISOString();
156
+ const revision = yield* mapFailure(profiles.publish({
157
+ proposal,
158
+ profile,
159
+ review: acceptPublicationProposal(proposal, input.reviewer, now),
160
+ publishedAt: now,
161
+ }));
162
+ return payload(revision);
163
+ }),
164
+ serve: (input) => mapFailure(startSourceServer(input).pipe(Effect.provideService(Enrollment, enrollment), Effect.provideService(MachineState, machine))).pipe(Effect.map((handle) => payload({
165
+ endpoint: handle.endpoint,
166
+ fingerprint: handle.fingerprint,
167
+ }))),
168
+ invite: (input) => mapFailure(enrollment.createInvitation(input)).pipe(Effect.map((grant) => payload({
169
+ invite: Buffer.from(JSON.stringify(grant)).toString("base64url"),
170
+ endpoint: grant.endpoint,
171
+ expiresAt: grant.expiresAt,
172
+ groups: grant.groups,
173
+ }))),
174
+ revoke: (follower) => mapFailure(enrollment.revokeFollower(follower)).pipe(Effect.as(payload({ follower, revoked: true }))),
175
+ listProfiles: () => mapFailure(repository.listRevisions()).pipe(Effect.map((revisions) => payload({
176
+ revisions: revisions.map((revision) => ({
177
+ id: revision.id,
178
+ profileId: revision.profileId,
179
+ sequence: revision.sequence,
180
+ digest: revision.digest,
181
+ publishedAt: revision.publishedAt,
182
+ })),
183
+ }))),
184
+ inspectProfile: (revision) => mapFailure(profiles.getRevision(revision)).pipe(Effect.map(payload)),
185
+ };
186
+ return SourceCommands.of(service);
187
+ }));
188
+ const runtimeProfileCatalogLayer = Layer.effect(ProfileCatalog, Effect.gen(function* () {
189
+ const enrollment = yield* Enrollment;
190
+ const machine = yield* MachineState;
191
+ const repository = yield* StateRepository;
192
+ return ProfileCatalog.of({
193
+ scan: scanDiscovery,
194
+ publish: (input) => Effect.gen(function* () {
195
+ const material = yield* enrollment.source().pipe(Effect.mapError((error) => new PublicationSigningError({
196
+ operation: "sign",
197
+ reason: error.message,
198
+ })));
199
+ const encodedKey = yield* machine.loadCredential({
200
+ reference: material.signingKeyReference,
201
+ }).pipe(Effect.mapError((error) => new PublicationSigningError({
202
+ operation: "sign",
203
+ reason: error.message,
204
+ })));
205
+ const privateKey = yield* Effect.try({
206
+ try: () => createPrivateKey(Redacted.value(encodedKey)),
207
+ catch: (error) => new PublicationSigningError({
208
+ operation: "sign",
209
+ reason: String(error),
210
+ }),
211
+ });
212
+ const publicKey = createPublicKey(privateKey);
213
+ const signer = {
214
+ keyId: material.source.keyId,
215
+ sign: (value) => Effect.try({
216
+ try: () => Schema.decodeUnknownSync(SourceSignature)(`ed25519:${signPayload(null, Buffer.from(value), privateKey).toString("base64url")}`),
217
+ catch: (error) => new PublicationSigningError({
218
+ operation: "sign",
219
+ reason: String(error),
220
+ }),
221
+ }),
222
+ verify: (value, signature) => Effect.try({
223
+ try: () => signature.startsWith("ed25519:")
224
+ && verifyPayload(null, Buffer.from(value), publicKey, Buffer.from(signature.slice("ed25519:".length), "base64url")),
225
+ catch: (error) => new PublicationSigningError({
226
+ operation: "verify",
227
+ reason: String(error),
228
+ }),
229
+ }),
230
+ };
231
+ return yield* makePublication(signer, repository).publish(input);
232
+ }),
233
+ getRevision: repository.getRevision,
234
+ });
235
+ }));
236
+ const policyFile = (path) => ({
237
+ get: () => Effect.tryPromise({
238
+ try: () => readFile(path, "utf8"),
239
+ catch: () => new CliCommandFailure({
240
+ category: "usage-or-configuration",
241
+ message: "agent policy is not configured",
242
+ }),
243
+ }).pipe(Effect.flatMap((text) => {
244
+ let decodedJson;
245
+ try {
246
+ decodedJson = JSON.parse(text);
247
+ }
248
+ catch {
249
+ return Effect.fail(new CliCommandFailure({
250
+ category: "usage-or-configuration",
251
+ message: "agent policy configuration is malformed",
252
+ }));
253
+ }
254
+ const decoded = Schema.decodeUnknownOption(Schema.Struct({ policy: AgentPolicy }))(decodedJson);
255
+ return Option.isSome(decoded)
256
+ ? Effect.succeed(decoded.value.policy)
257
+ : Effect.fail(new CliCommandFailure({
258
+ category: "usage-or-configuration",
259
+ message: "agent policy configuration is invalid",
260
+ }));
261
+ })),
262
+ set: (policy) => Effect.tryPromise({
263
+ try: async () => {
264
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
265
+ await writeFile(path, `${JSON.stringify({ policy })}\n`, {
266
+ encoding: "utf8",
267
+ mode: 0o600,
268
+ });
269
+ return policy;
270
+ },
271
+ catch: () => new CliCommandFailure({
272
+ category: "usage-or-configuration",
273
+ message: "agent policy configuration could not be written",
274
+ }),
275
+ }),
276
+ });
277
+ const followerCommandsLayer = (statePath, policyPath, doctorSource, doctorAgent) => Layer.effect(FollowerCommands, Effect.gen(function* () {
278
+ const machine = yield* MachineState;
279
+ const schedules = yield* ScheduleManager;
280
+ const repository = yield* StateRepository;
281
+ const synchronization = yield* Synchronization;
282
+ const agentResolution = yield* AgentResolution;
283
+ const policies = policyFile(policyPath);
284
+ const outcomePayload = (value) => {
285
+ const outcome = value.outcome?.outcome;
286
+ if (outcome === "HumanActionRequired") {
287
+ return Effect.fail(new CliCommandFailure({
288
+ category: "human-action-required",
289
+ message: "synchronization requires human action",
290
+ details: payload(value),
291
+ }));
292
+ }
293
+ if (outcome === "FollowerDrift") {
294
+ return Effect.fail(new CliCommandFailure({
295
+ category: "conflict-or-drift",
296
+ message: "follower drift conflicts with the selected revision",
297
+ details: payload(value),
298
+ }));
299
+ }
300
+ if (outcome === "Failed" || outcome === "Interrupted") {
301
+ return Effect.fail(new CliCommandFailure({
302
+ category: "verification-or-apply-failure",
303
+ message: outcome === "Interrupted"
304
+ ? "synchronization was interrupted"
305
+ : "synchronization failed",
306
+ details: payload(value),
307
+ }));
308
+ }
309
+ return Effect.succeed(payload(value));
310
+ };
311
+ const authorizedOverlayResource = (configuration, resourceId) => Effect.gen(function* () {
312
+ const revisions = yield* listRevisions({
313
+ endpoint: configuration.source.endpoint,
314
+ tlsFingerprint: configuration.source.tlsFingerprint,
315
+ sourceFingerprint: configuration.source.signingFingerprint,
316
+ credentialReference: configuration.credentialReference,
317
+ timeoutMilliseconds: configuration.scheduledInvocation.timeoutMilliseconds,
318
+ }).pipe(Effect.provideService(MachineState, machine), Effect.mapError(commandFailure));
319
+ const revision = revisions.revisions
320
+ .filter((candidate) => candidate.profileId === configuration.selectedProfile)
321
+ .sort((left, right) => right.sequence - left.sequence)[0];
322
+ if (revision === undefined) {
323
+ return yield* new CliCommandFailure({
324
+ category: "usage-or-configuration",
325
+ message: `selected profile ${configuration.selectedProfile} has no authorized revision`,
326
+ });
327
+ }
328
+ const metadata = yield* getRevisionMetadata({
329
+ endpoint: configuration.source.endpoint,
330
+ tlsFingerprint: configuration.source.tlsFingerprint,
331
+ sourceFingerprint: configuration.source.signingFingerprint,
332
+ credentialReference: configuration.credentialReference,
333
+ revisionId: revision.id,
334
+ timeoutMilliseconds: configuration.scheduledInvocation.timeoutMilliseconds,
335
+ }).pipe(Effect.provideService(MachineState, machine), Effect.mapError(commandFailure));
336
+ const resource = metadata.resources.find((candidate) => candidate.id === resourceId);
337
+ if (resource === undefined) {
338
+ return yield* new CliCommandFailure({
339
+ category: "usage-or-configuration",
340
+ message: `resource ${resourceId} is not authorized in the selected profile`,
341
+ });
342
+ }
343
+ if (resource.kind !== "config" || resource.policy !== "merge") {
344
+ return yield* new CliCommandFailure({
345
+ category: "usage-or-configuration",
346
+ message: `resource ${resourceId} does not support Local Overlay ownership`,
347
+ });
348
+ }
349
+ return resource;
350
+ });
351
+ const normalizedOverlay = (configuration, input) => Effect.gen(function* () {
352
+ const resource = yield* authorizedOverlayResource(configuration, input.resource);
353
+ const canonicalTarget = yield* machine.normalizePath({ path: resource.target }).pipe(Effect.mapError(commandFailure));
354
+ const requestedTarget = yield* machine.normalizePath({ path: input.target }).pipe(Effect.mapError(commandFailure));
355
+ if (canonicalTarget.platform !== requestedTarget.platform
356
+ || canonicalTarget.absolute !== requestedTarget.absolute) {
357
+ return yield* new CliCommandFailure({
358
+ category: "usage-or-configuration",
359
+ message: `overlay target must match the authorized target for resource ${input.resource}`,
360
+ });
361
+ }
362
+ const keys = [...new Set(input.keys.map((key) => key.trim()))].sort();
363
+ if (keys.length === 0
364
+ || keys.some((key) => key.length === 0
365
+ || key !== key.trim()
366
+ || key.includes("\0")
367
+ || key.split(".").some((segment) => segment.length === 0)
368
+ || /\p{Cc}/u.test(key))) {
369
+ return yield* new CliCommandFailure({
370
+ category: "usage-or-configuration",
371
+ message: "Local Overlay keys must be non-empty normalized config paths",
372
+ });
373
+ }
374
+ return {
375
+ resource: resource.id,
376
+ target: canonicalTarget.absolute,
377
+ keys,
378
+ };
379
+ });
380
+ const service = {
381
+ enroll: (input) => input.selectedProfile === undefined
382
+ ? Effect.fail(new CliCommandFailure({
383
+ category: "usage-or-configuration",
384
+ message: "follower enrollment requires an explicit --profile",
385
+ }))
386
+ : Effect.gen(function* () {
387
+ const selectedProfile = input.selectedProfile;
388
+ if (selectedProfile === undefined) {
389
+ return yield* new CliCommandFailure({
390
+ category: "usage-or-configuration",
391
+ message: "follower enrollment requires an explicit --profile",
392
+ });
393
+ }
394
+ const existing = yield* mapFailure(repository.getFollowerSynchronizationConfiguration());
395
+ if (existing?.enrollmentPending === true) {
396
+ const resumed = yield* finalizeFollowerEnrollment({
397
+ endpoint: existing.source.endpoint,
398
+ tlsFingerprint: existing.source.tlsFingerprint,
399
+ credentialReference: existing.credentialReference,
400
+ }).pipe(Effect.provideService(MachineState, machine), Effect.match({
401
+ onSuccess: () => ({ ok: true }),
402
+ onFailure: (error) => ({ ok: false, error }),
403
+ }));
404
+ if (!resumed.ok) {
405
+ const tag = resumed.error._tag ?? "";
406
+ if (tag !== "InvalidFollowerCredentialError") {
407
+ return yield* mapFailure(Effect.fail(resumed.error));
408
+ }
409
+ // The source restarted after the prepare phase and discarded
410
+ // its ambiguous marker. Discard the local half as well; the
411
+ // invitation can now be safely retried.
412
+ yield* machine.removeCredential(existing.credentialReference).pipe(Effect.ignore);
413
+ }
414
+ else {
415
+ const state = yield* mapFailure(repository.loadState(existing.follower.id));
416
+ if (state.sourceIdentity === undefined) {
417
+ return yield* new CliCommandFailure({
418
+ category: "usage-or-configuration",
419
+ message: "follower source identity is not configured",
420
+ });
421
+ }
422
+ yield* mapFailure(repository.saveFollowerSynchronizationConfiguration({
423
+ sourceIdentity: state.sourceIdentity,
424
+ configuration: {
425
+ ...existing,
426
+ enrollmentPending: undefined,
427
+ updatedAt: new Date().toISOString(),
428
+ },
429
+ }));
430
+ return payload({
431
+ follower: existing.follower,
432
+ selectedProfile: existing.selectedProfile,
433
+ source: state.sourceIdentity,
434
+ resumed: true,
435
+ });
436
+ }
437
+ }
438
+ const prepared = yield* mapFailure(enrollFollower({
439
+ ...input,
440
+ finalize: false,
441
+ }).pipe(Effect.provideService(MachineState, machine)));
442
+ const follower = {
443
+ ...prepared.follower,
444
+ credentialReference: prepared.credentialReference,
445
+ };
446
+ const authorizedProfiles = prepared.authorizedProfiles
447
+ ?? (yield* mapFailure(listRevisions({
448
+ endpoint: input.invitation.endpoint,
449
+ tlsFingerprint: prepared.tlsFingerprint,
450
+ sourceFingerprint: prepared.source.publicKeyFingerprint,
451
+ credentialReference: prepared.credentialReference,
452
+ timeoutMilliseconds: defaultScheduledInvocation.timeoutMilliseconds,
453
+ }).pipe(Effect.provideService(MachineState, machine)))).revisions;
454
+ if (!authorizedProfiles.some((revision) => revision.profileId === selectedProfile)) {
455
+ yield* cancelFollowerEnrollment({
456
+ endpoint: input.invitation.endpoint,
457
+ tlsFingerprint: prepared.tlsFingerprint,
458
+ credentialReference: prepared.credentialReference,
459
+ }).pipe(Effect.provideService(MachineState, machine), Effect.ignore);
460
+ return yield* new CliCommandFailure({
461
+ category: "usage-or-configuration",
462
+ message: `profile ${selectedProfile} has no authorized revision`,
463
+ });
464
+ }
465
+ const source = prepared.source;
466
+ const configuration = {
467
+ schemaVersion: 1,
468
+ follower,
469
+ selectedProfile,
470
+ source: {
471
+ endpoint: input.invitation.endpoint,
472
+ tlsFingerprint: prepared.tlsFingerprint,
473
+ signingFingerprint: prepared.source.publicKeyFingerprint,
474
+ },
475
+ credentialReference: prepared.credentialReference,
476
+ cacheDirectory: join(dirname(statePath), "cache"),
477
+ stateLocation: statePath,
478
+ agentPolicy: "deterministic-only",
479
+ enrollmentPending: true,
480
+ scheduledInvocation: defaultScheduledInvocation,
481
+ updatedAt: new Date().toISOString(),
482
+ };
483
+ const stateIdentity = source;
484
+ const saved = yield* mapFailure(repository.saveFollowerSynchronizationConfiguration({
485
+ sourceIdentity: stateIdentity,
486
+ configuration,
487
+ })).pipe(Effect.match({
488
+ onSuccess: () => ({ ok: true }),
489
+ onFailure: (error) => ({ ok: false, error }),
490
+ }));
491
+ if (!saved.ok) {
492
+ yield* cancelFollowerEnrollment({
493
+ endpoint: input.invitation.endpoint,
494
+ tlsFingerprint: prepared.tlsFingerprint,
495
+ credentialReference: prepared.credentialReference,
496
+ }).pipe(Effect.provideService(MachineState, machine), Effect.ignore);
497
+ return yield* saved.error;
498
+ }
499
+ const finalized = yield* finalizeFollowerEnrollment({
500
+ endpoint: input.invitation.endpoint,
501
+ tlsFingerprint: prepared.tlsFingerprint,
502
+ credentialReference: prepared.credentialReference,
503
+ }).pipe(Effect.provideService(MachineState, machine), Effect.match({
504
+ onSuccess: () => ({ ok: true }),
505
+ onFailure: (error) => ({ ok: false, error }),
506
+ }));
507
+ if (!finalized.ok)
508
+ return yield* mapFailure(Effect.fail(finalized.error));
509
+ const cleared = yield* mapFailure(repository.saveFollowerSynchronizationConfiguration({
510
+ sourceIdentity: source,
511
+ configuration: {
512
+ ...configuration,
513
+ enrollmentPending: undefined,
514
+ updatedAt: new Date().toISOString(),
515
+ },
516
+ })).pipe(Effect.match({
517
+ onSuccess: () => ({ ok: true }),
518
+ onFailure: (error) => ({ ok: false, error }),
519
+ }));
520
+ if (!cleared.ok)
521
+ return yield* cleared.error;
522
+ return payload({
523
+ follower,
524
+ selectedProfile,
525
+ source: prepared.source,
526
+ });
527
+ }),
528
+ synchronize: (input) => mapFailure(synchronizeFollower(statePath, input.mode, undefined, input.noInput).pipe(Effect.provideService(StateRepository, repository), Effect.provideService(MachineState, machine), Effect.provideService(Synchronization, synchronization), Effect.provideService(AgentResolution, agentResolution), Effect.provideService(ScheduleManager, schedules))).pipe(Effect.flatMap(outcomePayload)),
529
+ recover: () => mapFailure(recoverFollower(statePath).pipe(Effect.provideService(StateRepository, repository), Effect.provideService(MachineState, machine), Effect.provideService(Synchronization, synchronization), Effect.provideService(ScheduleManager, schedules))).pipe(Effect.flatMap(outcomePayload)),
530
+ status: (follower) => follower === undefined
531
+ ? mapFailure(repository.getFollowerSynchronizationConfiguration()).pipe(Effect.flatMap((configuration) => configuration === undefined
532
+ ? Effect.fail(new CliCommandFailure({
533
+ category: "usage-or-configuration",
534
+ message: "follower synchronization configuration is not enrolled",
535
+ }))
536
+ : mapFailure(repository.loadState(configuration.follower.id)).pipe(Effect.map((state) => ({
537
+ ...state,
538
+ localOverlay: configuration.localOverlay ?? [],
539
+ })))), Effect.map(payload))
540
+ : mapFailure(repository.loadState(follower)).pipe(Effect.flatMap((state) => mapFailure(repository.getFollowerSynchronizationConfiguration()).pipe(Effect.map((configuration) => ({
541
+ ...state,
542
+ localOverlay: configuration?.follower.id === follower
543
+ ? configuration.localOverlay ?? []
544
+ : [],
545
+ })))), Effect.map(payload)),
546
+ setLocalOverlay: (input) => mapFailure(repository.getFollowerSynchronizationConfiguration()).pipe(Effect.flatMap((configuration) => configuration === undefined
547
+ ? Effect.fail(new CliCommandFailure({
548
+ category: "usage-or-configuration",
549
+ message: "follower synchronization configuration is not enrolled",
550
+ }))
551
+ : normalizedOverlay(configuration, input).pipe(Effect.flatMap((entry) => mapFailure(repository.saveLocalOverlay({
552
+ entry,
553
+ updatedAt: new Date().toISOString(),
554
+ })).pipe(Effect.as(entry))), Effect.map((entry) => payload({ ...entry, saved: true }))))),
555
+ listLocalOverlays: () => mapFailure(repository.getFollowerSynchronizationConfiguration()).pipe(Effect.flatMap((configuration) => configuration === undefined
556
+ ? Effect.fail(new CliCommandFailure({
557
+ category: "usage-or-configuration",
558
+ message: "follower synchronization configuration is not enrolled",
559
+ }))
560
+ : mapFailure(repository.listLocalOverlays())), Effect.map((overlays) => payload({
561
+ overlays: overlays.map((overlay) => ({
562
+ resource: overlay.resource,
563
+ target: overlay.target,
564
+ keys: overlay.keys,
565
+ })),
566
+ }))),
567
+ removeLocalOverlay: (resource) => mapFailure(repository.getFollowerSynchronizationConfiguration()).pipe(Effect.flatMap((configuration) => configuration === undefined
568
+ ? Effect.fail(new CliCommandFailure({
569
+ category: "usage-or-configuration",
570
+ message: "follower synchronization configuration is not enrolled",
571
+ }))
572
+ : mapFailure(repository.removeLocalOverlay({
573
+ resource,
574
+ updatedAt: new Date().toISOString(),
575
+ }))), Effect.map(() => payload({ resource, removed: true }))),
576
+ setAgentPolicy: (policy) => mapFailure(repository.getFollowerSynchronizationConfiguration()).pipe(Effect.flatMap((configuration) => {
577
+ if (configuration === undefined) {
578
+ return policies.set(policy).pipe(Effect.map(payload));
579
+ }
580
+ return mapFailure(repository.loadState(configuration.follower.id)).pipe(Effect.flatMap((state) => state.sourceIdentity === undefined
581
+ ? Effect.fail(new CliCommandFailure({
582
+ category: "usage-or-configuration",
583
+ message: "follower source identity is not configured",
584
+ }))
585
+ : mapFailure(repository.saveFollowerSynchronizationConfiguration({
586
+ sourceIdentity: state.sourceIdentity,
587
+ configuration: {
588
+ ...configuration,
589
+ agentPolicy: policy,
590
+ updatedAt: new Date().toISOString(),
591
+ },
592
+ }))), Effect.as(payload(policy)));
593
+ })),
594
+ getAgentPolicy: () => mapFailure(repository.getFollowerSynchronizationConfiguration()).pipe(Effect.flatMap((configuration) => configuration === undefined
595
+ ? policies.get()
596
+ : Effect.succeed(configuration.agentPolicy)), Effect.map(payload)),
597
+ setAgentHarness: (agentHarness) => mapFailure(repository.getFollowerSynchronizationConfiguration()).pipe(Effect.flatMap((configuration) => {
598
+ if (configuration === undefined) {
599
+ return Effect.fail(new CliCommandFailure({
600
+ category: "usage-or-configuration",
601
+ message: "follower synchronization configuration is not enrolled",
602
+ }));
603
+ }
604
+ return mapFailure(repository.loadState(configuration.follower.id)).pipe(Effect.flatMap((state) => state.sourceIdentity === undefined
605
+ ? Effect.fail(new CliCommandFailure({
606
+ category: "usage-or-configuration",
607
+ message: "follower source identity is not configured",
608
+ }))
609
+ : mapFailure(repository.saveFollowerSynchronizationConfiguration({
610
+ sourceIdentity: state.sourceIdentity,
611
+ configuration: {
612
+ ...configuration,
613
+ agentHarness,
614
+ updatedAt: new Date().toISOString(),
615
+ },
616
+ }))), Effect.as(payload(agentHarness)));
617
+ })),
618
+ getAgentHarness: () => mapFailure(repository.getFollowerSynchronizationConfiguration()).pipe(Effect.flatMap((configuration) => configuration?.agentHarness === undefined
619
+ ? Effect.fail(new CliCommandFailure({
620
+ category: "usage-or-configuration",
621
+ message: "agent harness is not configured",
622
+ }))
623
+ : Effect.succeed(configuration.agentHarness)), Effect.map(payload)),
624
+ selectProfile: (profile) => mapFailure(repository.getFollowerSynchronizationConfiguration()).pipe(Effect.flatMap((configuration) => {
625
+ if (configuration === undefined) {
626
+ return Effect.fail(new CliCommandFailure({
627
+ category: "usage-or-configuration",
628
+ message: "follower synchronization configuration is not enrolled",
629
+ }));
630
+ }
631
+ return mapFailure(listRevisions({
632
+ endpoint: configuration.source.endpoint,
633
+ tlsFingerprint: configuration.source.tlsFingerprint,
634
+ sourceFingerprint: configuration.source.signingFingerprint,
635
+ credentialReference: configuration.credentialReference,
636
+ timeoutMilliseconds: configuration.scheduledInvocation.timeoutMilliseconds,
637
+ }).pipe(Effect.provideService(MachineState, machine))).pipe(Effect.flatMap((revisions) => revisions.revisions.some((revision) => revision.profileId === profile)
638
+ ? mapFailure(repository.loadState(configuration.follower.id))
639
+ : Effect.fail(new CliCommandFailure({
640
+ category: "usage-or-configuration",
641
+ message: `profile ${profile} has no authorized revision`,
642
+ }))), Effect.flatMap((state) => state.sourceIdentity === undefined
643
+ ? Effect.fail(new CliCommandFailure({
644
+ category: "usage-or-configuration",
645
+ message: "follower source identity is not configured",
646
+ }))
647
+ : mapFailure(repository.saveFollowerSynchronizationConfiguration({
648
+ sourceIdentity: state.sourceIdentity,
649
+ configuration: {
650
+ ...configuration,
651
+ selectedProfile: profile,
652
+ updatedAt: new Date().toISOString(),
653
+ },
654
+ }))), Effect.as(payload({ selectedProfile: profile })));
655
+ })),
656
+ setSchedule: (input) => mapFailure(schedules.update(input)).pipe(Effect.map(payload)),
657
+ scheduleStatus: () => mapFailure(schedules.status()).pipe(Effect.map(payload)),
658
+ removeSchedule: () => mapFailure(schedules.remove()).pipe(Effect.map(payload)),
659
+ doctor: (input) => runDoctorProbes({
660
+ ...input,
661
+ statePath,
662
+ policyPath,
663
+ source: doctorSource,
664
+ agent: doctorAgent,
665
+ }).pipe(Effect.provideService(MachineState, machine), Effect.provideService(ScheduleManager, schedules), Effect.provideService(StateRepository, repository), Effect.flatMap((report) => {
666
+ const category = doctorFailureCategory(report);
667
+ return category === undefined
668
+ ? Effect.succeed(payload(report))
669
+ : Effect.fail(new CliCommandFailure({
670
+ category,
671
+ message: "one or more doctor probes failed",
672
+ details: payload(report),
673
+ }));
674
+ })),
675
+ };
676
+ return FollowerCommands.of(service);
677
+ }));
678
+ const credentialPolicyFromEnvironment = () => {
679
+ const root = process.env.CANONFIG_LOCAL_CREDENTIAL_ROOT;
680
+ return root === undefined
681
+ ? undefined
682
+ : { kind: "local-file", path: root };
683
+ };
684
+ const machineLayer = () => {
685
+ const credentialPolicy = credentialPolicyFromEnvironment();
686
+ switch (process.platform) {
687
+ case "darwin":
688
+ return macosMachineStateLayer({ credentialPolicy });
689
+ case "win32":
690
+ return windowsMachineStateLayer({ credentialPolicy });
691
+ default:
692
+ return linuxMachineStateLayer({ credentialPolicy });
693
+ }
694
+ };
695
+ export const runtimeLayer = (options = {}) => {
696
+ const root = join(homedir(), ".canonfig");
697
+ const statePath = options.statePath ?? join(root, "state.sqlite");
698
+ const state = Layer.unwrap(Effect.promise(() => mkdir(dirname(statePath), { recursive: true, mode: 0o700 })).pipe(Effect.as(stateRepositoryLayer(statePath))));
699
+ const machine = machineLayer();
700
+ const enrollment = EnrollmentLive.pipe(Layer.provide(Layer.merge(state, machine)));
701
+ const profiles = runtimeProfileCatalogLayer.pipe(Layer.provide(Layer.mergeAll(state, machine, enrollment)));
702
+ const schedule = scheduleManagerLayer.pipe(Layer.provide(machine));
703
+ const synchronization = SynchronizationLive.pipe(Layer.provide(Layer.merge(state, machine)));
704
+ const dependencies = Layer.mergeAll(state, machine, enrollment, profiles, schedule, synchronization, AgentResolutionLive);
705
+ return Layer.merge(sourceCommandsLayer.pipe(Layer.provide(dependencies)), followerCommandsLayer(statePath, options.policyPath ?? join(root, "policy.json"), options.doctorSource ?? doctorSourceFromEnvironment(), options.doctorAgent ?? doctorAgentFromEnvironment()).pipe(Layer.provide(dependencies)));
706
+ };