@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,306 @@
1
+ import { createHash, X509Certificate } from "node:crypto";
2
+ import { constants as filesystemConstants } from "node:fs";
3
+ import { access, open } from "node:fs/promises";
4
+ import { request as httpsRequest } from "node:https";
5
+ import { connect as tlsConnect } from "node:tls";
6
+ import { Effect, Option, Redacted, Schema } from "effect";
7
+ import { programVersion } from "../cli/cli.js";
8
+ import { CertificateFingerprint, CredentialReference, } from "../domain/brand.js";
9
+ import { MachineState } from "../machine/machine-state.service.js";
10
+ import { ScheduleManager } from "../schedule/schedule-manager.service.js";
11
+ import { StateRepository } from "../state/state-repository.service.js";
12
+ export const doctorProbeNames = [
13
+ "runtime",
14
+ "state",
15
+ "credentials",
16
+ "source",
17
+ "scheduler",
18
+ "package-managers",
19
+ "agent-adapter",
20
+ ];
21
+ const PolicyFile = Schema.Struct({
22
+ policy: Schema.Literals(["deterministic-only", "agent-propose", "agent-apply"]),
23
+ });
24
+ const pass = (name, message, details) => details === undefined
25
+ ? { name, status: "pass", message }
26
+ : { name, status: "pass", message, details };
27
+ const warning = (name, message, details) => details === undefined
28
+ ? { name, status: "warning", message }
29
+ : { name, status: "warning", message, details };
30
+ const skipped = (name, message) => ({ name, status: "skipped", message });
31
+ const failed = (name, category, message, details) => details === undefined
32
+ ? { name, status: "fail", category, message }
33
+ : { name, status: "fail", category, message, details };
34
+ class DoctorSourceProbeError extends Error {
35
+ kind;
36
+ _tag = "DoctorSourceProbeError";
37
+ constructor(kind) {
38
+ super(`source probe failed: ${kind}`);
39
+ this.kind = kind;
40
+ }
41
+ }
42
+ const categoryForSourceError = (error) => {
43
+ switch (error.kind) {
44
+ case "configuration": return "usage-or-configuration";
45
+ case "transport": return "transport";
46
+ case "tls-pin":
47
+ case "authentication": return "authentication-or-revocation";
48
+ }
49
+ };
50
+ const sourceFailureMessage = (error) => {
51
+ switch (error.kind) {
52
+ case "configuration": return "source probe configuration is invalid";
53
+ case "transport": return "configured source is unreachable";
54
+ case "tls-pin": return "source TLS pin validation failed";
55
+ case "authentication": return "source authentication failed";
56
+ }
57
+ };
58
+ const isolated = (name, timeoutMilliseconds, operation, onFailure, timeoutCategory) => operation.pipe(Effect.catch((error) => Effect.succeed(onFailure(error))), Effect.timeoutOption(timeoutMilliseconds), Effect.matchCause({
59
+ onFailure: () => failed(name, "internal", `${name} probe failed unexpectedly`),
60
+ onSuccess: Option.match({
61
+ onNone: () => failed(name, timeoutCategory, `${name} probe timed out`, {
62
+ timeoutMilliseconds,
63
+ }),
64
+ onSome: (result) => result,
65
+ }),
66
+ }));
67
+ const runtimeProbe = () => Effect.sync(() => pass("runtime", "runtime is supported", {
68
+ runtime: "node",
69
+ runtimeVersion: process.versions.node,
70
+ canonfigVersion: programVersion,
71
+ platform: process.platform,
72
+ architecture: process.arch,
73
+ }));
74
+ const stateProbe = (statePath, repository) => Effect.gen(function* () {
75
+ yield* Effect.tryPromise({
76
+ try: async () => {
77
+ await access(statePath, filesystemConstants.R_OK | filesystemConstants.W_OK);
78
+ const file = await open(statePath, "r+");
79
+ try {
80
+ const header = Buffer.alloc(16);
81
+ await file.read(header, 0, header.byteLength, 0);
82
+ if (header.toString("utf8") !== "SQLite format 3\0") {
83
+ throw new Error("state is not a SQLite database");
84
+ }
85
+ await file.write(Buffer.alloc(0), 0, 0, 0);
86
+ }
87
+ finally {
88
+ await file.close();
89
+ }
90
+ },
91
+ catch: () => new Error("SQLite file health check failed"),
92
+ });
93
+ yield* repository.listRevisions();
94
+ return pass("state", "SQLite state is migrated, readable, and writable", {
95
+ header: "valid",
96
+ migrations: "current",
97
+ readWrite: true,
98
+ });
99
+ });
100
+ const credentialProbe = (machine) => machine.credentialCapability().pipe(Effect.map((capability) => {
101
+ switch (capability.kind) {
102
+ case "secure-noninteractive":
103
+ return pass("credentials", "noninteractive credential storage is available", { kind: capability.kind, provider: capability.provider });
104
+ case "local-file":
105
+ return warning("credentials", "credential storage uses an explicitly configured local file", { kind: capability.kind });
106
+ case "unavailable":
107
+ return warning("credentials", "noninteractive credential storage is unavailable", { kind: capability.kind });
108
+ }
109
+ }));
110
+ const sourceProbe = (machine, source) => {
111
+ if (source === undefined) {
112
+ return Effect.succeed(skipped("source", "source reachability, TLS pin, and authentication are not configured"));
113
+ }
114
+ return Effect.gen(function* () {
115
+ const tlsFingerprint = yield* Schema.decodeUnknownEffect(CertificateFingerprint)(source.tlsFingerprint).pipe(Effect.mapError(() => new DoctorSourceProbeError("configuration")));
116
+ const credentialReference = yield* Schema.decodeUnknownEffect(CredentialReference)(source.credentialReference).pipe(Effect.mapError(() => new DoctorSourceProbeError("configuration")));
117
+ const credential = yield* machine.loadCredential({
118
+ reference: credentialReference,
119
+ }).pipe(Effect.mapError(() => new DoctorSourceProbeError("authentication")));
120
+ yield* Effect.tryPromise({
121
+ try: (signal) => new Promise((resolveProbe, rejectProbe) => {
122
+ let endpoint;
123
+ try {
124
+ endpoint = new URL(source.endpoint);
125
+ const loopback = endpoint.hostname === "127.0.0.1"
126
+ || endpoint.hostname === "[::1]"
127
+ || endpoint.hostname === "::1";
128
+ if (endpoint.protocol !== "https:" || !loopback) {
129
+ throw new Error("invalid endpoint");
130
+ }
131
+ }
132
+ catch {
133
+ rejectProbe(new DoctorSourceProbeError("configuration"));
134
+ return;
135
+ }
136
+ const host = endpoint.hostname.replaceAll("[", "").replaceAll("]", "");
137
+ const socket = tlsConnect({
138
+ host,
139
+ port: Number(endpoint.port),
140
+ rejectUnauthorized: false,
141
+ minVersion: "TLSv1.2",
142
+ });
143
+ const abort = () => {
144
+ socket.destroy(new Error("source probe aborted"));
145
+ };
146
+ signal.addEventListener("abort", abort, { once: true });
147
+ socket.once("secureConnect", () => {
148
+ const peer = socket.getPeerCertificate();
149
+ if (peer.raw === undefined) {
150
+ socket.destroy();
151
+ rejectProbe(new DoctorSourceProbeError("transport"));
152
+ return;
153
+ }
154
+ const fingerprint = createHash("sha256").update(peer.raw).digest("hex");
155
+ if (fingerprint !== tlsFingerprint) {
156
+ socket.destroy();
157
+ rejectProbe(new DoctorSourceProbeError("tls-pin"));
158
+ return;
159
+ }
160
+ const certificate = new X509Certificate(peer.raw).toString();
161
+ socket.end();
162
+ const request = httpsRequest({
163
+ protocol: "https:",
164
+ hostname: host,
165
+ port: endpoint.port,
166
+ path: "/v1/enrollment/authenticate",
167
+ method: "GET",
168
+ ca: certificate,
169
+ rejectUnauthorized: true,
170
+ minVersion: "TLSv1.2",
171
+ headers: {
172
+ authorization: `Bearer ${Redacted.value(credential)}`,
173
+ accept: "application/json",
174
+ },
175
+ }, (response) => {
176
+ response.resume();
177
+ response.once("end", () => {
178
+ signal.removeEventListener("abort", abortRequest);
179
+ if (response.statusCode === 200)
180
+ resolveProbe();
181
+ else
182
+ rejectProbe(new DoctorSourceProbeError("authentication"));
183
+ });
184
+ });
185
+ const abortRequest = () => {
186
+ request.destroy(new Error("source probe aborted"));
187
+ };
188
+ signal.removeEventListener("abort", abort);
189
+ signal.addEventListener("abort", abortRequest, { once: true });
190
+ request.once("error", () => {
191
+ signal.removeEventListener("abort", abortRequest);
192
+ rejectProbe(new DoctorSourceProbeError("transport"));
193
+ });
194
+ request.end();
195
+ });
196
+ socket.once("error", () => {
197
+ signal.removeEventListener("abort", abort);
198
+ rejectProbe(new DoctorSourceProbeError("transport"));
199
+ });
200
+ }),
201
+ catch: (error) => error instanceof DoctorSourceProbeError
202
+ ? error
203
+ : new DoctorSourceProbeError("transport"),
204
+ });
205
+ return pass("source", "source is reachable, TLS-pinned, and authenticated", {
206
+ reachable: true,
207
+ tlsPinned: true,
208
+ authenticated: true,
209
+ });
210
+ });
211
+ };
212
+ const schedulerProbe = (schedules) => schedules.status({ executable: process.argv[1] }).pipe(Effect.map((status) => {
213
+ const details = {
214
+ state: status.state,
215
+ platform: status.platform,
216
+ mechanism: status.definition.mechanism,
217
+ };
218
+ return status.state === "current" || status.state === "not-installed"
219
+ ? pass("scheduler", `scheduler state is ${status.state}`, details)
220
+ : warning("scheduler", `scheduler state is ${status.state}`, details);
221
+ }));
222
+ const packageManagerProbe = Effect.fn("Doctor.packageManagers")(function* (machine) {
223
+ const available = [];
224
+ for (const name of ["npm", "pnpm", "yarn"]) {
225
+ const found = yield* machine.findExecutable({ name }).pipe(Effect.as(true), Effect.catch(() => Effect.succeed(false)));
226
+ if (found)
227
+ available.push(name);
228
+ }
229
+ return available.length === 0
230
+ ? failed("package-managers", "usage-or-configuration", "no supported package manager is available")
231
+ : pass("package-managers", "package manager capability is available", {
232
+ available: available.join(","),
233
+ });
234
+ });
235
+ const executableAvailable = (machine, executable) => executable.includes("/") || executable.includes("\\")
236
+ ? Effect.tryPromise({
237
+ try: () => access(executable, filesystemConstants.X_OK).then(() => true),
238
+ catch: () => false,
239
+ }).pipe(Effect.catch(() => Effect.succeed(false)))
240
+ : machine.findExecutable({ name: executable }).pipe(Effect.as(true), Effect.catch(() => Effect.succeed(false)));
241
+ const readPolicy = (policyPath) => Effect.tryPromise({
242
+ try: async () => {
243
+ const { readFile } = await import("node:fs/promises");
244
+ return Schema.decodeUnknownSync(PolicyFile)(JSON.parse(await readFile(policyPath, "utf8")));
245
+ },
246
+ catch: (error) => error,
247
+ }).pipe(Effect.catch(() => Effect.succeed(undefined)));
248
+ const agentProbe = Effect.fn("Doctor.agentAdapter")(function* (machine, policyPath, agent) {
249
+ const configuredPolicy = yield* readPolicy(policyPath);
250
+ if (agent === undefined) {
251
+ if (configuredPolicy?.policy === "deterministic-only") {
252
+ return pass("agent-adapter", "deterministic-only policy requires no adapter", {
253
+ policy: configuredPolicy.policy,
254
+ configured: false,
255
+ });
256
+ }
257
+ if (configuredPolicy === undefined) {
258
+ return skipped("agent-adapter", "agent policy and adapter are not configured");
259
+ }
260
+ return failed("agent-adapter", "usage-or-configuration", "configured agent policy requires an adapter", { policy: configuredPolicy.policy, configured: false });
261
+ }
262
+ if (!["codex", "claude", "gemini"].includes(agent.adapter)) {
263
+ return failed("agent-adapter", "usage-or-configuration", "configured agent adapter is unsupported");
264
+ }
265
+ const executable = yield* executableAvailable(machine, agent.executable);
266
+ return executable
267
+ ? pass("agent-adapter", "configured agent adapter executable is available", {
268
+ adapter: agent.adapter,
269
+ executableAvailable: true,
270
+ })
271
+ : failed("agent-adapter", "usage-or-configuration", "configured agent adapter executable is unavailable", { adapter: agent.adapter, executableAvailable: false });
272
+ });
273
+ export const runDoctorProbes = Effect.fn("runDoctorProbes")(function* (input) {
274
+ const machine = yield* MachineState;
275
+ const schedules = yield* ScheduleManager;
276
+ const repository = yield* StateRepository;
277
+ const timeout = input.timeoutMilliseconds;
278
+ const probes = yield* Effect.all([
279
+ isolated("runtime", timeout, runtimeProbe(), () => failed("runtime", "internal", "runtime probe failed"), "internal"),
280
+ isolated("state", timeout, stateProbe(input.statePath, repository), () => failed("state", "internal", "SQLite state health check failed"), "internal"),
281
+ isolated("credentials", timeout, credentialProbe(machine), () => failed("credentials", "human-action-required", "credential capability check failed"), "human-action-required"),
282
+ isolated("source", timeout, sourceProbe(machine, input.source), (error) => failed("source", categoryForSourceError(error), sourceFailureMessage(error)), "transport"),
283
+ isolated("scheduler", timeout, schedulerProbe(schedules), () => failed("scheduler", "verification-or-apply-failure", "scheduler state check failed"), "verification-or-apply-failure"),
284
+ isolated("package-managers", timeout, packageManagerProbe(machine), () => failed("package-managers", "internal", "package manager capability check failed"), "internal"),
285
+ isolated("agent-adapter", timeout, agentProbe(machine, input.policyPath, input.agent), () => failed("agent-adapter", "internal", "agent adapter capability check failed"), "internal"),
286
+ ], { concurrency: "unbounded" });
287
+ const failedCount = probes.filter((probe) => probe.status === "fail").length;
288
+ const degraded = probes.some((probe) => probe.status === "warning" || probe.status === "skipped");
289
+ return {
290
+ schema: "canonfig.doctor/v1",
291
+ status: failedCount > 0 ? "unhealthy" : degraded ? "degraded" : "healthy",
292
+ noInput: input.noInput,
293
+ timeoutMilliseconds: timeout,
294
+ probes,
295
+ };
296
+ });
297
+ const categoryPriority = [
298
+ "internal",
299
+ "verification-or-apply-failure",
300
+ "authentication-or-revocation",
301
+ "transport",
302
+ "human-action-required",
303
+ "conflict-or-drift",
304
+ "usage-or-configuration",
305
+ ];
306
+ export const doctorFailureCategory = (report) => categoryPriority.find((category) => report.probes.some((probe) => probe.status === "fail" && probe.category === category));