@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,950 @@
1
+ import { Schema } from "effect";
2
+ import { defaultPolicyForKind, ApplyPolicy as ApplyPolicySchema, ResourceKind as ResourceKindSchema, ToolRecipeRef, policyCompatibleWithKind, } from "./resource.js";
3
+ import { canonicalRecipeIndexUrl, recipeValidationError, } from "./recipe-versions.js";
4
+ import { BlobId, ContentDigest as ContentDigestSchema, CredentialReference as CredentialReferenceSchema, GroupName, ProfileId as ProfileIdSchema, ProfileRevisionId as ProfileRevisionIdSchema, ResourceId as ResourceIdSchema, SourceSignature, Timestamp, ToolId, } from "./brand.js";
5
+ import { canonicalJson, decodeJsonc, digestOf, sha256Hex, } from "../profile/profile-codec.js";
6
+ const ConfigValueInputSchema = Schema.Union([
7
+ Schema.String,
8
+ Schema.Number,
9
+ Schema.Boolean,
10
+ Schema.Array(Schema.String),
11
+ ]);
12
+ const AuthoringFileSchema = Schema.Struct({
13
+ kind: Schema.Literal("file"),
14
+ content: Schema.String,
15
+ executable: Schema.optional(Schema.Boolean),
16
+ symlinkTo: Schema.optional(Schema.NonEmptyString),
17
+ });
18
+ const AuthoringDirectoryFileSchema = Schema.Struct({
19
+ path: Schema.NonEmptyString,
20
+ content: Schema.String,
21
+ executable: Schema.optional(Schema.Boolean),
22
+ });
23
+ const AuthoringLoginSchema = Schema.Union([
24
+ Schema.Struct({ required: Schema.Literal(false) }),
25
+ Schema.Struct({
26
+ required: Schema.Literal(true),
27
+ howTo: Schema.NonEmptyString,
28
+ }),
29
+ ]);
30
+ export const ResourceSpecInputSchema = Schema.Union([
31
+ AuthoringFileSchema,
32
+ Schema.Struct({
33
+ kind: Schema.Literal("directory"),
34
+ files: Schema.Array(AuthoringDirectoryFileSchema),
35
+ }),
36
+ Schema.Struct({
37
+ kind: Schema.Literal("config"),
38
+ format: Schema.Literals(["toml", "json", "yaml"]),
39
+ keys: Schema.Array(Schema.Struct({
40
+ path: Schema.NonEmptyString,
41
+ value: ConfigValueInputSchema,
42
+ })),
43
+ }),
44
+ Schema.Struct({
45
+ kind: Schema.Literal("skill"),
46
+ name: Schema.NonEmptyString,
47
+ files: Schema.Array(AuthoringDirectoryFileSchema),
48
+ }),
49
+ Schema.Struct({
50
+ kind: Schema.Literal("tool"),
51
+ toolId: ToolId,
52
+ recipes: Schema.Array(ToolRecipeRef),
53
+ login: Schema.optional(AuthoringLoginSchema),
54
+ }),
55
+ Schema.Struct({
56
+ kind: Schema.Literal("credential"),
57
+ reference: CredentialReferenceSchema,
58
+ }),
59
+ Schema.Struct({
60
+ kind: Schema.Literal("schedule"),
61
+ calendar: Schema.Union([
62
+ Schema.Struct({ type: Schema.Literal("daily"), at: Schema.NonEmptyString }),
63
+ Schema.Struct({
64
+ type: Schema.Literal("weekly"),
65
+ days: Schema.Array(Schema.NonEmptyString),
66
+ at: Schema.NonEmptyString,
67
+ }),
68
+ Schema.Struct({
69
+ type: Schema.Literal("custom"),
70
+ expression: Schema.NonEmptyString,
71
+ }),
72
+ ]),
73
+ timezone: Schema.NonEmptyString,
74
+ }),
75
+ ]);
76
+ export const VerificationInputSchema = Schema.Union([
77
+ Schema.Struct({ method: Schema.Literal("digest"), digest: ContentDigestSchema }),
78
+ Schema.Struct({
79
+ method: Schema.Literal("command"),
80
+ command: Schema.Array(Schema.NonEmptyString),
81
+ expectContains: Schema.optional(Schema.String),
82
+ }),
83
+ Schema.Struct({
84
+ method: Schema.Literal("executable-present"),
85
+ executable: Schema.NonEmptyString,
86
+ }),
87
+ Schema.Struct({
88
+ method: Schema.Literal("credential-present"),
89
+ reference: CredentialReferenceSchema,
90
+ }),
91
+ Schema.Struct({ method: Schema.Literal("symlink"), target: Schema.NonEmptyString }),
92
+ ]);
93
+ const verificationAllowedForSpec = (kind, spec, method) => {
94
+ if (spec.kind !== kind)
95
+ return verificationAllowed(kind, method);
96
+ if (spec.kind === "file") {
97
+ return spec.symlinkTo === undefined
98
+ ? method === "digest"
99
+ : method === "symlink";
100
+ }
101
+ return verificationAllowed(kind, method);
102
+ };
103
+ const verificationContentIssue = (resource) => {
104
+ if (resource.kind !== "file" || resource.spec.kind !== "file")
105
+ return undefined;
106
+ if (resource.spec.symlinkTo === undefined) {
107
+ return resource.verify.method === "digest"
108
+ && resource.verify.digest !== sha256Hex(resource.spec.content)
109
+ ? "digest verification does not match authored file content"
110
+ : undefined;
111
+ }
112
+ return resource.verify.method === "symlink"
113
+ && resource.verify.target !== resource.spec.symlinkTo
114
+ ? "symlink verification target does not match authored symlink target"
115
+ : undefined;
116
+ };
117
+ export const ProfileResourceInputSchema = Schema.Struct({
118
+ id: ResourceIdSchema,
119
+ kind: ResourceKindSchema,
120
+ policy: Schema.optional(ApplyPolicySchema),
121
+ target: Schema.NonEmptyString,
122
+ groups: Schema.optional(Schema.Array(GroupName)),
123
+ dependsOn: Schema.optional(Schema.Array(ResourceIdSchema)),
124
+ spec: ResourceSpecInputSchema,
125
+ verify: VerificationInputSchema,
126
+ }).check(Schema.makeFilter((resource) => {
127
+ const policy = resource.policy ?? defaultPolicyForKind[resource.kind];
128
+ if (!policyCompatibleWithKind(resource.kind, policy)) {
129
+ return {
130
+ path: ["policy"],
131
+ issue: `policy ${policy} is not compatible with resource kind ${resource.kind}`,
132
+ };
133
+ }
134
+ const verificationIssue = verificationContentIssue(resource);
135
+ if (verificationIssue !== undefined) {
136
+ return {
137
+ path: ["verify"],
138
+ issue: verificationIssue,
139
+ };
140
+ }
141
+ return undefined;
142
+ }));
143
+ export const ProfileGroupSchema = Schema.Struct({
144
+ name: GroupName,
145
+ description: Schema.optional(Schema.NonEmptyString),
146
+ });
147
+ export const ScheduleDefaultSchema = Schema.Union([
148
+ Schema.Struct({
149
+ type: Schema.Literal("daily"),
150
+ at: Schema.NonEmptyString,
151
+ timezone: Schema.NonEmptyString,
152
+ }),
153
+ Schema.Struct({
154
+ type: Schema.Literal("weekly"),
155
+ days: Schema.Array(Schema.NonEmptyString),
156
+ at: Schema.NonEmptyString,
157
+ timezone: Schema.NonEmptyString,
158
+ }),
159
+ Schema.Struct({
160
+ type: Schema.Literal("custom"),
161
+ expression: Schema.NonEmptyString,
162
+ timezone: Schema.NonEmptyString,
163
+ }),
164
+ ]);
165
+ /** Strict schema for the normalized v2 authoring contract. */
166
+ export const MachineProfileSchema = Schema.Struct({
167
+ id: ProfileIdSchema,
168
+ version: Schema.Literal(2),
169
+ name: Schema.NonEmptyString,
170
+ groups: Schema.Array(ProfileGroupSchema),
171
+ resources: Schema.Array(ProfileResourceInputSchema),
172
+ scheduleDefault: ScheduleDefaultSchema,
173
+ });
174
+ /** Authoring schema permits only documented fields and fills omissions in normalization. */
175
+ export const MachineProfileAuthoringSchema = Schema.Struct({
176
+ id: ProfileIdSchema,
177
+ version: Schema.optional(Schema.Literal(2)),
178
+ name: Schema.NonEmptyString,
179
+ groups: Schema.optional(Schema.Array(ProfileGroupSchema)),
180
+ resources: Schema.optional(Schema.Array(ProfileResourceInputSchema)),
181
+ scheduleDefault: Schema.optional(ScheduleDefaultSchema),
182
+ });
183
+ export const PublishedResourceSchema = Schema.Struct({
184
+ id: ResourceIdSchema,
185
+ kind: ResourceKindSchema,
186
+ policy: ApplyPolicySchema,
187
+ target: Schema.NonEmptyString,
188
+ groups: Schema.optional(Schema.Array(GroupName)),
189
+ dependsOn: Schema.Array(ResourceIdSchema),
190
+ blobs: Schema.Array(BlobId),
191
+ });
192
+ export const ProfileRevisionSchema = Schema.Struct({
193
+ id: ProfileRevisionIdSchema,
194
+ profileId: ProfileIdSchema,
195
+ sequence: Schema.Natural,
196
+ canonicalBytes: Schema.String,
197
+ digest: ContentDigestSchema,
198
+ signature: SourceSignature,
199
+ publishedAt: Timestamp,
200
+ resources: Schema.Array(PublishedResourceSchema),
201
+ groups: Schema.Array(ProfileGroupSchema),
202
+ scheduleDefault: Schema.optional(ScheduleDefaultSchema),
203
+ });
204
+ export const DiscoveryEvidenceRecordSchema = Schema.Struct({
205
+ source: Schema.NonEmptyString,
206
+ line: Schema.Int.check(Schema.isGreaterThan(0)),
207
+ excerpt: Schema.String,
208
+ kind: Schema.Literals([
209
+ "invocation",
210
+ "config",
211
+ "hook",
212
+ "mcp",
213
+ "package-metadata",
214
+ "prose",
215
+ ]),
216
+ });
217
+ export const ProfileChangeProposalSchema = Schema.Struct({
218
+ createdAt: Timestamp,
219
+ reason: Schema.NonEmptyString,
220
+ additions: Schema.Array(ProfileResourceInputSchema),
221
+ modifications: Schema.Array(ProfileResourceInputSchema),
222
+ removals: Schema.Array(ResourceIdSchema),
223
+ evidence: Schema.Array(DiscoveryEvidenceRecordSchema),
224
+ });
225
+ export const CredentialDescriptorSchema = Schema.Struct({
226
+ reference: CredentialReferenceSchema,
227
+ description: Schema.NonEmptyString,
228
+ loginRequired: Schema.Boolean,
229
+ });
230
+ /** Runtime schema aliases share names with their corresponding domain types. */
231
+ export const ResourceSpecInput = ResourceSpecInputSchema;
232
+ export const VerificationInput = VerificationInputSchema;
233
+ export const ProfileResourceInput = ProfileResourceInputSchema;
234
+ export const ProfileGroup = ProfileGroupSchema;
235
+ export const ScheduleDefault = ScheduleDefaultSchema;
236
+ export const MachineProfile = MachineProfileSchema;
237
+ export const PublishedResource = PublishedResourceSchema;
238
+ export const ProfileRevision = ProfileRevisionSchema;
239
+ export const DiscoveryEvidenceRecord = DiscoveryEvidenceRecordSchema;
240
+ export const ProfileChangeProposal = ProfileChangeProposalSchema;
241
+ export const CredentialDescriptor = CredentialDescriptorSchema;
242
+ /**
243
+ * Validation failures as tagged errors.
244
+ */
245
+ export class DuplicateResourceError extends Schema.TaggedError()("DuplicateResourceError", { id: Schema.String }) {
246
+ }
247
+ export class MissingDependencyError extends Schema.TaggedError()("MissingDependencyError", { id: Schema.String, dependsOn: Schema.String }) {
248
+ }
249
+ export class DependencyCycleError extends Schema.TaggedError()("DependencyCycleError", { cycle: Schema.Array(Schema.String) }) {
250
+ }
251
+ export class PolicyKindMismatchError extends Schema.TaggedError()("PolicyKindMismatchError", { id: Schema.String, kind: Schema.String, policy: Schema.String }) {
252
+ }
253
+ export class InvalidTargetError extends Schema.TaggedError()("InvalidTargetError", { id: Schema.String, target: Schema.String, reason: Schema.String }) {
254
+ }
255
+ export class ConflictingResourceTargetError extends Schema.TaggedError()("ConflictingResourceTargetError", {
256
+ id: Schema.String,
257
+ target: Schema.String,
258
+ conflictsWith: Schema.String,
259
+ reason: Schema.String,
260
+ }) {
261
+ }
262
+ export class InvalidScheduleError extends Schema.TaggedError()("InvalidScheduleError", { id: Schema.String, reason: Schema.String }) {
263
+ }
264
+ export class DuplicateGroupError extends Schema.TaggedError()("DuplicateGroupError", { name: Schema.String }) {
265
+ }
266
+ export class MissingGroupReferenceError extends Schema.TaggedError()("MissingGroupReferenceError", { id: Schema.String, group: Schema.String }) {
267
+ }
268
+ export class ResourceSpecKindMismatchError extends Schema.TaggedError()("ResourceSpecKindMismatchError", { id: Schema.String, kind: Schema.String, specKind: Schema.String }) {
269
+ }
270
+ export class VerificationKindMismatchError extends Schema.TaggedError()("VerificationKindMismatchError", { id: Schema.String, kind: Schema.String, method: Schema.String }) {
271
+ }
272
+ export class VerificationContentMismatchError extends Schema.TaggedError()("VerificationContentMismatchError", { id: Schema.String, method: Schema.String, reason: Schema.String }) {
273
+ }
274
+ export class InvalidBuildPolicyError extends Schema.TaggedError()("InvalidBuildPolicyError", { id: Schema.String, reason: Schema.String }) {
275
+ }
276
+ export class InvalidRecipeError extends Schema.TaggedError()("InvalidRecipeError", { id: Schema.String, reason: Schema.String }) {
277
+ }
278
+ /** Aggregate contract failure preserving all precise tagged graph errors. */
279
+ export class ProfileContractError extends Error {
280
+ errors;
281
+ constructor(errors) {
282
+ super(errors.map((error) => error._tag).join(", "));
283
+ this.name = "ProfileContractError";
284
+ this.errors = errors;
285
+ }
286
+ }
287
+ export const validateMachineProfile = (profile, platform = "windows") => {
288
+ const errors = [];
289
+ const groups = new Set();
290
+ for (const group of profile.groups) {
291
+ if (groups.has(group.name)) {
292
+ errors.push(new DuplicateGroupError({ name: group.name }));
293
+ }
294
+ groups.add(group.name);
295
+ }
296
+ errors.push(...validateProfileResources(profile.resources, groups, platform));
297
+ const scheduleError = validateScheduleDefault(profile.scheduleDefault);
298
+ if (scheduleError !== null)
299
+ errors.push(scheduleError);
300
+ return errors;
301
+ };
302
+ /** Validate resource graph: returns every violation, not just the first. */
303
+ export const validateProfileResources = (resources, declaredGroups, platform = "windows") => {
304
+ const errors = [];
305
+ const seen = new Set();
306
+ for (const resource of resources) {
307
+ if (seen.has(resource.id)) {
308
+ errors.push(new DuplicateResourceError({ id: resource.id }));
309
+ }
310
+ seen.add(resource.id);
311
+ }
312
+ const byId = new Map(resources.map((resource) => [resource.id, resource]));
313
+ for (const resource of resources) {
314
+ if (resource.spec.kind !== resource.kind) {
315
+ errors.push(new ResourceSpecKindMismatchError({
316
+ id: resource.id,
317
+ kind: resource.kind,
318
+ specKind: resource.spec.kind,
319
+ }));
320
+ }
321
+ for (const dep of resource.dependsOn ?? []) {
322
+ if (!byId.has(dep)) {
323
+ errors.push(new MissingDependencyError({ id: resource.id, dependsOn: dep }));
324
+ }
325
+ }
326
+ if (declaredGroups !== undefined) {
327
+ for (const group of resource.groups ?? []) {
328
+ if (!declaredGroups.has(group)) {
329
+ errors.push(new MissingGroupReferenceError({ id: resource.id, group }));
330
+ }
331
+ }
332
+ }
333
+ const kind = resource.kind;
334
+ const policy = resource.policy ?? defaultPolicy(kind);
335
+ if (!policyAllowed(kind, policy)) {
336
+ errors.push(new PolicyKindMismatchError({ id: resource.id, kind, policy }));
337
+ }
338
+ if (resource.spec.kind === "schedule") {
339
+ const scheduleError = validateSchedule(resource);
340
+ if (scheduleError !== null)
341
+ errors.push(scheduleError);
342
+ }
343
+ errors.push(...validateRecipes(resource));
344
+ errors.push(...validateVerificationContent(resource));
345
+ errors.push(...validateBuildPolicies(resource));
346
+ if (!verificationAllowedForSpec(resource.kind, resource.spec, resource.verify.method)) {
347
+ errors.push(new VerificationKindMismatchError({
348
+ id: resource.id,
349
+ kind: resource.kind,
350
+ method: resource.verify.method,
351
+ }));
352
+ }
353
+ }
354
+ errors.push(...validateResourceTargetConflicts(resources, platform));
355
+ const cycle = findDependencyCycle(resources);
356
+ if (cycle !== null)
357
+ errors.push(new DependencyCycleError({ cycle }));
358
+ return errors;
359
+ };
360
+ const validateRecipes = (resource) => {
361
+ if (resource.spec.kind !== "tool")
362
+ return [];
363
+ return resource.spec.recipes.flatMap((recipe) => {
364
+ const reason = recipeValidationError(recipe);
365
+ return reason === undefined
366
+ ? []
367
+ : [new InvalidRecipeError({
368
+ id: resource.id,
369
+ reason: `recipe ${recipe.method}/${recipe.package}: ${reason}`,
370
+ })];
371
+ });
372
+ };
373
+ const validateVerificationContent = (resource) => {
374
+ const reason = verificationContentIssue(resource);
375
+ return reason !== undefined
376
+ ? [new VerificationContentMismatchError({
377
+ id: resource.id,
378
+ method: resource.verify.method,
379
+ reason,
380
+ })]
381
+ : [];
382
+ };
383
+ const validateBuildPolicies = (resource) => {
384
+ if (resource.spec.kind !== "tool")
385
+ return [];
386
+ const errors = [];
387
+ for (const recipe of resource.spec.recipes) {
388
+ const policy = recipe.buildPolicy ?? { mode: "scripts-disabled" };
389
+ if (policy.mode === "scripts-disabled")
390
+ continue;
391
+ if (!Number.isFinite(Date.parse(policy.reviewedAt))) {
392
+ errors.push(new InvalidBuildPolicyError({
393
+ id: resource.id,
394
+ reason: `recipe ${recipe.method}/${recipe.package} has an invalid review timestamp`,
395
+ }));
396
+ }
397
+ if (policy.executables.length === 0
398
+ || policy.paths.length === 0
399
+ || policy.steps.length === 0) {
400
+ errors.push(new InvalidBuildPolicyError({
401
+ id: resource.id,
402
+ reason: `recipe ${recipe.method}/${recipe.package} requires executable, path, and build-step bounds`,
403
+ }));
404
+ }
405
+ if (!policy.capabilities.includes("execute")) {
406
+ errors.push(new InvalidBuildPolicyError({
407
+ id: resource.id,
408
+ reason: `recipe ${recipe.method}/${recipe.package} must explicitly allow execute`,
409
+ }));
410
+ }
411
+ if (policy.steps.some((step) => !policy.executables.includes(step.executable))) {
412
+ errors.push(new InvalidBuildPolicyError({
413
+ id: resource.id,
414
+ reason: `recipe ${recipe.method}/${recipe.package} has an unbounded build executable`,
415
+ }));
416
+ }
417
+ for (const origin of policy.origins) {
418
+ try {
419
+ const url = new URL(origin);
420
+ if (url.protocol !== "https:" || url.origin !== origin) {
421
+ errors.push(new InvalidBuildPolicyError({
422
+ id: resource.id,
423
+ reason: `recipe ${recipe.method}/${recipe.package} has a non-exact HTTPS origin`,
424
+ }));
425
+ }
426
+ }
427
+ catch {
428
+ errors.push(new InvalidBuildPolicyError({
429
+ id: resource.id,
430
+ reason: `recipe ${recipe.method}/${recipe.package} has an invalid origin`,
431
+ }));
432
+ }
433
+ }
434
+ }
435
+ return errors;
436
+ };
437
+ const defaultPolicy = (kind) => {
438
+ return defaultPolicyForKind[kind];
439
+ };
440
+ const policyAllowed = (kind, policy) => {
441
+ return policyCompatibleWithKind(kind, policy);
442
+ };
443
+ const verificationAllowed = (kind, method) => {
444
+ switch (kind) {
445
+ case "file":
446
+ return method === "digest" || method === "symlink" || method === "command";
447
+ case "directory":
448
+ case "config":
449
+ case "skill":
450
+ return method === "digest" || method === "command";
451
+ case "tool":
452
+ return method === "executable-present" || method === "command";
453
+ case "credential":
454
+ return method === "credential-present" || method === "command";
455
+ case "schedule":
456
+ return method === "command";
457
+ }
458
+ };
459
+ const invalidTargetReason = (target) => {
460
+ if (target.trim().length === 0) {
461
+ return "empty target";
462
+ }
463
+ if (target.includes("\0")) {
464
+ return "null byte in target";
465
+ }
466
+ const pathSegments = target.replaceAll("\\", "/").split("/");
467
+ if (pathSegments.some((segment) => segment === "..")) {
468
+ return "parent traversal in target";
469
+ }
470
+ if (/[*?[\]]/u.test(target)) {
471
+ return "glob in target";
472
+ }
473
+ return undefined;
474
+ };
475
+ const normalizedTargetPath = (value, platform = "windows") => {
476
+ const slashSeparated = value.replaceAll("\\", "/");
477
+ const drive = /^([A-Za-z]):(?=\/|$)/u.exec(slashSeparated);
478
+ const prefix = drive === null
479
+ ? slashSeparated.startsWith("/")
480
+ ? "/"
481
+ : ""
482
+ : `${drive[1].toLowerCase()}:`;
483
+ const body = drive === null
484
+ ? slashSeparated
485
+ : slashSeparated.slice(2);
486
+ const segments = [];
487
+ for (const segment of body.split("/")) {
488
+ if (segment === "" || segment === ".")
489
+ continue;
490
+ segments.push(segment.normalize("NFC"));
491
+ }
492
+ const normalized = segments.join("/");
493
+ const normalizedValue = prefix === "/"
494
+ ? normalized.length === 0 ? "/" : `/${normalized}`
495
+ : prefix.length > 0
496
+ ? normalized.length === 0 ? `${prefix}/` : `${prefix}/${normalized}`
497
+ : normalized.length === 0 ? "." : normalized;
498
+ return platform === "windows" ? normalizedValue.toLowerCase() : normalizedValue;
499
+ };
500
+ const invalidRelativeTargetReason = (path, platform) => {
501
+ if (path.trim().length === 0)
502
+ return "empty managed file path";
503
+ if (path.includes("\0"))
504
+ return "null byte in managed file path";
505
+ if (path.startsWith("/")
506
+ || path.startsWith("\\")
507
+ || /^[A-Za-z]:/u.test(path)) {
508
+ return "managed file path must be relative to its resource target";
509
+ }
510
+ if (path.replaceAll("\\", "/").split("/").some((segment) => segment === "..")) {
511
+ return "parent traversal in managed file path";
512
+ }
513
+ if (path.includes("\\"))
514
+ return "alternate path separator in managed file path";
515
+ const segments = path.split("/");
516
+ if (segments.some((segment) => segment.length === 0 || segment === ".")) {
517
+ return "managed file path is not canonical";
518
+ }
519
+ if (path.normalize("NFC") !== path) {
520
+ return "managed file path is not canonical";
521
+ }
522
+ if (platform === "windows") {
523
+ for (const segment of segments) {
524
+ if (/[<>:"|?*]/u.test(segment)) {
525
+ return "reserved character in managed file path";
526
+ }
527
+ if (segment.endsWith(".") || segment.endsWith(" ")) {
528
+ return "trailing dot or space in managed file path";
529
+ }
530
+ if (/^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/iu.test(segment)) {
531
+ return "reserved Windows name in managed file path";
532
+ }
533
+ }
534
+ }
535
+ if (/[*?[\]]/u.test(path))
536
+ return "glob in managed file path";
537
+ return undefined;
538
+ };
539
+ const resourceEntries = (resource) => {
540
+ if (resource.spec.kind !== "directory" && resource.spec.kind !== "skill")
541
+ return [];
542
+ return resource.spec.files.map((file) => file.path);
543
+ };
544
+ const canonicalResourcePathClaims = (resource, platform) => {
545
+ const claims = [];
546
+ const errors = [];
547
+ for (const rawPath of [...resource.entries].sort(compareText)) {
548
+ const reason = invalidRelativeTargetReason(rawPath, platform);
549
+ if (reason !== undefined) {
550
+ errors.push(new InvalidTargetError({
551
+ id: resource.id,
552
+ target: rawPath,
553
+ reason,
554
+ }));
555
+ continue;
556
+ }
557
+ claims.push({
558
+ resource,
559
+ rawPath,
560
+ path: normalizedTargetPath(`${resource.target}/${rawPath}`, platform),
561
+ });
562
+ }
563
+ return { claims, errors };
564
+ };
565
+ /**
566
+ * Validate filesystem claims at a profile or planner boundary. The resource
567
+ * target itself is the explicitly represented directory ancestry; every
568
+ * declared entry is otherwise a file or symlink leaf.
569
+ */
570
+ export const validateResourcePathConflicts = (resources, platform = "windows") => {
571
+ const errors = [];
572
+ const orderedResources = [...resources].sort((left, right) => compareText(left.id, right.id));
573
+ const claimsByResource = new Map();
574
+ for (const resource of orderedResources) {
575
+ const targetReason = invalidTargetReason(resource.target);
576
+ if (targetReason !== undefined) {
577
+ errors.push(new InvalidTargetError({
578
+ id: resource.id,
579
+ target: resource.target,
580
+ reason: targetReason,
581
+ }));
582
+ }
583
+ const result = canonicalResourcePathClaims(resource, platform);
584
+ errors.push(...result.errors);
585
+ claimsByResource.set(resource.id, result.claims);
586
+ }
587
+ const claims = orderedResources
588
+ .flatMap((resource) => {
589
+ if (resource.kind !== "file"
590
+ && resource.kind !== "directory"
591
+ && resource.kind !== "config"
592
+ && resource.kind !== "skill"
593
+ && resource.kind !== "schedule")
594
+ return [];
595
+ const root = {
596
+ resource,
597
+ path: normalizedTargetPath(resource.target, platform),
598
+ rawPath: resource.target,
599
+ namespace: resource.kind === "schedule" ? "schedule" : "filesystem",
600
+ isRoot: true,
601
+ };
602
+ if (resource.kind === "schedule")
603
+ return [root];
604
+ return [
605
+ root,
606
+ ...(claimsByResource.get(resource.id) ?? []).map((claim) => ({
607
+ resource,
608
+ path: claim.path,
609
+ rawPath: claim.rawPath,
610
+ namespace: "filesystem",
611
+ isRoot: false,
612
+ })),
613
+ ];
614
+ })
615
+ .sort((left, right) => compareText(left.resource.id, right.resource.id)
616
+ || compareText(left.path, right.path)
617
+ || compareText(left.rawPath, right.rawPath));
618
+ const overlaps = (left, right) => left === right
619
+ || left.startsWith(`${right}/`)
620
+ || right.startsWith(`${left}/`);
621
+ for (let index = 0; index < claims.length; index += 1) {
622
+ const claim = claims[index];
623
+ for (let otherIndex = index + 1; otherIndex < claims.length; otherIndex += 1) {
624
+ const other = claims[otherIndex];
625
+ if (claim.namespace !== other.namespace
626
+ || !overlaps(claim.path, other.path)) {
627
+ continue;
628
+ }
629
+ if (claim.resource.id === other.resource.id) {
630
+ if (claim.isRoot || other.isRoot)
631
+ continue;
632
+ if (claim.rawPath === other.rawPath) {
633
+ errors.push(new ConflictingResourceTargetError({
634
+ id: claim.resource.id,
635
+ target: claim.rawPath,
636
+ conflictsWith: other.resource.id,
637
+ reason: `managed path ${claim.path} is declared more than once`,
638
+ }));
639
+ }
640
+ else if (claim.rawPath !== other.rawPath) {
641
+ errors.push(new ConflictingResourceTargetError({
642
+ id: claim.resource.id,
643
+ target: claim.rawPath,
644
+ conflictsWith: other.resource.id,
645
+ reason: `managed path ${claim.path} overlaps managed path ${other.path}`,
646
+ }));
647
+ }
648
+ continue;
649
+ }
650
+ const duplicate = errors.some((error) => error._tag === "ConflictingResourceTargetError"
651
+ && error.id === claim.resource.id
652
+ && error.conflictsWith === other.resource.id);
653
+ if (!duplicate) {
654
+ errors.push(new ConflictingResourceTargetError({
655
+ id: claim.resource.id,
656
+ target: claim.resource.target,
657
+ conflictsWith: other.resource.id,
658
+ reason: `target ${claim.path} overlaps target ${other.path}`,
659
+ }));
660
+ }
661
+ }
662
+ }
663
+ return errors;
664
+ };
665
+ const pathClaimsResource = (resource) => ({
666
+ id: resource.id,
667
+ kind: resource.kind,
668
+ target: resource.target,
669
+ entries: resourceEntries(resource),
670
+ });
671
+ const validateResourceTargetConflicts = (resources, platform) => validateResourcePathConflicts(resources.map(pathClaimsResource), platform);
672
+ const timePattern = /^([01]\d|2[0-3]):[0-5]\d$/u;
673
+ const dayNames = new Set(["mon", "tue", "wed", "thu", "fri", "sat", "sun"]);
674
+ const validateScheduleDefault = (schedule) => {
675
+ if (schedule.type === "daily" && !timePattern.test(schedule.at)) {
676
+ return new InvalidScheduleError({
677
+ id: "$scheduleDefault",
678
+ reason: `invalid daily time ${schedule.at}`,
679
+ });
680
+ }
681
+ if (schedule.type === "weekly") {
682
+ if (!timePattern.test(schedule.at)) {
683
+ return new InvalidScheduleError({
684
+ id: "$scheduleDefault",
685
+ reason: `invalid weekly time ${schedule.at}`,
686
+ });
687
+ }
688
+ if (schedule.days.length === 0) {
689
+ return new InvalidScheduleError({
690
+ id: "$scheduleDefault",
691
+ reason: "weekly schedule needs at least one day",
692
+ });
693
+ }
694
+ for (const day of schedule.days) {
695
+ if (!dayNames.has(day)) {
696
+ return new InvalidScheduleError({
697
+ id: "$scheduleDefault",
698
+ reason: `unknown day ${day}`,
699
+ });
700
+ }
701
+ }
702
+ }
703
+ return null;
704
+ };
705
+ const validateSchedule = (resource) => {
706
+ const spec = resource.spec;
707
+ if (spec.kind !== "schedule")
708
+ return null;
709
+ const calendar = spec.calendar;
710
+ if (calendar.type === "daily" && !timePattern.test(calendar.at)) {
711
+ return new InvalidScheduleError({ id: resource.id, reason: `invalid daily time ${calendar.at}` });
712
+ }
713
+ if (calendar.type === "weekly") {
714
+ if (!timePattern.test(calendar.at)) {
715
+ return new InvalidScheduleError({ id: resource.id, reason: `invalid weekly time ${calendar.at}` });
716
+ }
717
+ if (calendar.days.length === 0) {
718
+ return new InvalidScheduleError({ id: resource.id, reason: "weekly schedule needs at least one day" });
719
+ }
720
+ for (const day of calendar.days) {
721
+ if (!dayNames.has(day)) {
722
+ return new InvalidScheduleError({ id: resource.id, reason: `unknown day ${day}` });
723
+ }
724
+ }
725
+ }
726
+ if (calendar.type === "custom" && calendar.expression.trim().length === 0) {
727
+ return new InvalidScheduleError({ id: resource.id, reason: "empty custom expression" });
728
+ }
729
+ return null;
730
+ };
731
+ /** Detect a dependency cycle; returns one cycle path or null. */
732
+ export const findDependencyCycle = (resources) => {
733
+ const graph = new Map();
734
+ for (const resource of resources) {
735
+ graph.set(resource.id, resource.dependsOn ?? []);
736
+ }
737
+ const visiting = [];
738
+ const visited = new Set();
739
+ const dfs = (node) => {
740
+ if (visited.has(node))
741
+ return null;
742
+ const index = visiting.indexOf(node);
743
+ if (index >= 0)
744
+ return [...visiting.slice(index), node];
745
+ visiting.push(node);
746
+ const deps = graph.get(node) ?? [];
747
+ for (const dep of deps) {
748
+ if (!graph.has(dep))
749
+ continue;
750
+ const found = dfs(dep);
751
+ if (found !== null)
752
+ return found;
753
+ }
754
+ visiting.pop();
755
+ visited.add(node);
756
+ return null;
757
+ };
758
+ for (const resource of resources) {
759
+ const found = dfs(resource.id);
760
+ if (found !== null)
761
+ return found;
762
+ }
763
+ return null;
764
+ };
765
+ /** Topological order of resource ids; deterministic (stable input order, deps first). */
766
+ export const topologicalOrder = (resources) => {
767
+ const byId = new Map(resources.map((r) => [r.id, r]));
768
+ const ordered = [];
769
+ const emitted = new Set();
770
+ const visiting = new Set();
771
+ const visit = (id) => {
772
+ if (emitted.has(id) || visiting.has(id))
773
+ return;
774
+ visiting.add(id);
775
+ const resource = byId.get(id);
776
+ if (resource !== undefined) {
777
+ for (const dep of resource.dependsOn ?? [])
778
+ visit(dep);
779
+ }
780
+ visiting.delete(id);
781
+ emitted.add(id);
782
+ ordered.push(id);
783
+ };
784
+ for (const resource of resources)
785
+ visit(resource.id);
786
+ return ordered;
787
+ };
788
+ const compareText = (left, right) => {
789
+ if (left < right)
790
+ return -1;
791
+ if (left > right)
792
+ return 1;
793
+ return 0;
794
+ };
795
+ const uniqueSorted = (values) => [...new Set(values)].sort(compareText);
796
+ const normalizeResourceSpec = (spec) => {
797
+ switch (spec.kind) {
798
+ case "file": {
799
+ const base = {
800
+ kind: spec.kind,
801
+ content: spec.content,
802
+ executable: spec.executable ?? false,
803
+ };
804
+ if (spec.symlinkTo === undefined)
805
+ return base;
806
+ return { ...base, symlinkTo: spec.symlinkTo };
807
+ }
808
+ case "directory":
809
+ return {
810
+ kind: spec.kind,
811
+ files: spec.files
812
+ .map((file) => ({
813
+ path: file.path,
814
+ content: file.content,
815
+ executable: file.executable ?? false,
816
+ }))
817
+ .sort((left, right) => compareText(left.path, right.path)),
818
+ };
819
+ case "config":
820
+ return {
821
+ kind: spec.kind,
822
+ format: spec.format,
823
+ keys: [...spec.keys].sort((left, right) => compareText(left.path, right.path)),
824
+ };
825
+ case "skill":
826
+ return {
827
+ kind: spec.kind,
828
+ name: spec.name,
829
+ files: spec.files
830
+ .map((file) => ({
831
+ path: file.path,
832
+ content: file.content,
833
+ executable: file.executable ?? false,
834
+ }))
835
+ .sort((left, right) => compareText(left.path, right.path)),
836
+ };
837
+ case "tool":
838
+ return {
839
+ kind: spec.kind,
840
+ toolId: spec.toolId,
841
+ recipes: [...spec.recipes].sort((left, right) => compareText(`${left.platform}\0${left.method}\0${left.package}\0${left.version ?? ""}\0${JSON.stringify(left.indexPolicy)}\0${JSON.stringify(left.source)}`, `${right.platform}\0${right.method}\0${right.package}\0${right.version ?? ""}\0${JSON.stringify(right.indexPolicy)}\0${JSON.stringify(right.source)}`)).map((recipe) => {
842
+ const indexPolicy = recipe.indexPolicy === undefined
843
+ ? undefined
844
+ : {
845
+ ...recipe.indexPolicy,
846
+ url: canonicalRecipeIndexUrl(recipe.indexPolicy.url) ?? recipe.indexPolicy.url,
847
+ };
848
+ const { indexPolicy: _indexPolicy, ...recipeWithoutIndex } = recipe;
849
+ const base = {
850
+ ...recipeWithoutIndex,
851
+ buildPolicy: recipe.buildPolicy ?? { mode: "scripts-disabled" },
852
+ };
853
+ return indexPolicy === undefined
854
+ ? base
855
+ : { ...base, indexPolicy };
856
+ }),
857
+ login: spec.login ?? { required: false },
858
+ };
859
+ case "credential":
860
+ return { kind: spec.kind, reference: spec.reference };
861
+ case "schedule": {
862
+ if (spec.calendar.type !== "weekly") {
863
+ return {
864
+ kind: spec.kind,
865
+ calendar: spec.calendar,
866
+ timezone: spec.timezone,
867
+ };
868
+ }
869
+ const dayOrder = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"];
870
+ const days = [...new Set(spec.calendar.days)].sort((left, right) => dayOrder.indexOf(left) - dayOrder.indexOf(right));
871
+ return {
872
+ kind: spec.kind,
873
+ calendar: { ...spec.calendar, days },
874
+ timezone: spec.timezone,
875
+ };
876
+ }
877
+ }
878
+ };
879
+ const normalizeResource = (resource) => {
880
+ const base = {
881
+ id: resource.id,
882
+ kind: resource.kind,
883
+ policy: resource.policy ?? defaultPolicy(resource.kind),
884
+ target: resource.target,
885
+ dependsOn: uniqueSorted(resource.dependsOn ?? []),
886
+ spec: normalizeResourceSpec(resource.spec),
887
+ verify: resource.verify,
888
+ };
889
+ if (resource.groups === undefined)
890
+ return base;
891
+ return { ...base, groups: uniqueSorted(resource.groups) };
892
+ };
893
+ /** Apply all v2 defaults and order unordered collections deterministically. */
894
+ export const normalizeMachineProfile = (profile) => {
895
+ const groups = (profile.groups ?? [])
896
+ .map((group) => group.description === undefined
897
+ ? { name: group.name }
898
+ : { name: group.name, description: group.description })
899
+ .sort((left, right) => compareText(left.name, right.name));
900
+ const resources = (profile.resources ?? [])
901
+ .map(normalizeResource)
902
+ .sort((left, right) => compareText(left.id, right.id));
903
+ const scheduleDefault = profile.scheduleDefault ?? {
904
+ type: "daily",
905
+ at: "00:00",
906
+ timezone: "local",
907
+ };
908
+ const normalizedSchedule = scheduleDefault.type === "weekly"
909
+ ? { ...scheduleDefault, days: uniqueSorted(scheduleDefault.days) }
910
+ : scheduleDefault;
911
+ return {
912
+ id: profile.id,
913
+ version: 2,
914
+ name: profile.name,
915
+ groups,
916
+ resources,
917
+ scheduleDefault: normalizedSchedule,
918
+ };
919
+ };
920
+ /** Decode strict JSONC authoring input, normalize it, then reject invalid graphs. */
921
+ export const decodeMachineProfileJsonc = (text) => {
922
+ const authored = decodeJsonc(MachineProfileAuthoringSchema)(text);
923
+ const normalized = normalizeMachineProfile(authored);
924
+ Schema.decodeUnknownSync(MachineProfileSchema, { onExcessProperty: "error" })(normalized);
925
+ const errors = validateMachineProfile(normalized);
926
+ if (errors.length > 0)
927
+ throw new ProfileContractError(errors);
928
+ return normalized;
929
+ };
930
+ /** Backwards-friendly concise name for the JSONC authoring boundary. */
931
+ export const decodeMachineProfile = decodeMachineProfileJsonc;
932
+ const profileJsonValue = (profile) => Schema.decodeUnknownSync(Schema.MutableJson)(profile);
933
+ /** Canonical publication encoding of a validated, normalized profile. */
934
+ export const encodeMachineProfile = (profile) => {
935
+ const normalized = normalizeMachineProfile(profile);
936
+ Schema.decodeUnknownSync(MachineProfileSchema, { onExcessProperty: "error" })(normalized);
937
+ const errors = validateMachineProfile(normalized);
938
+ if (errors.length > 0)
939
+ throw new ProfileContractError(errors);
940
+ return canonicalJson(profileJsonValue(normalized));
941
+ };
942
+ /** Stable SHA-256 digest of the canonical publication encoding. */
943
+ export const digestMachineProfile = (profile) => {
944
+ const normalized = normalizeMachineProfile(profile);
945
+ Schema.decodeUnknownSync(MachineProfileSchema, { onExcessProperty: "error" })(normalized);
946
+ const errors = validateMachineProfile(normalized);
947
+ if (errors.length > 0)
948
+ throw new ProfileContractError(errors);
949
+ return digestOf(profileJsonValue(normalized));
950
+ };