@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,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
|
+
};
|