@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,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));
|