@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,297 @@
1
+ import { Schema } from "effect";
2
+ import { isNpmRegistryPackageName } from "./npm-package-spec.js";
3
+ export const RecipeMethod = Schema.Literals([
4
+ "npm",
5
+ "pnpm",
6
+ "bun",
7
+ "brew",
8
+ "homebrew",
9
+ "apt",
10
+ "winget",
11
+ "uv",
12
+ "cargo",
13
+ "source",
14
+ ]);
15
+ /** Recipe methods that the current deterministic process executor may run. */
16
+ export const AutomaticRecipeMethod = Schema.Literals([
17
+ "npm",
18
+ "pnpm",
19
+ "bun",
20
+ "brew",
21
+ "homebrew",
22
+ "apt",
23
+ "winget",
24
+ "uv",
25
+ "cargo",
26
+ ]);
27
+ /** Canonical source metadata retained for reviewed package recipes. */
28
+ export const RecipeSourceMetadata = Schema.Struct({
29
+ source: Schema.NonEmptyString,
30
+ integrity: Schema.optional(Schema.NonEmptyString),
31
+ });
32
+ /**
33
+ * A reviewed Python package index. The full URL is retained because private
34
+ * repositories commonly scope their simple API below an origin.
35
+ */
36
+ export const RecipeIndexPolicy = Schema.Struct({
37
+ url: Schema.NonEmptyString,
38
+ reviewedBy: Schema.NonEmptyString,
39
+ reviewedAt: Schema.NonEmptyString,
40
+ });
41
+ export const defaultPythonIndex = "https://pypi.org/simple";
42
+ const hasUnsafeIndexUrlCharacter = (value) => [...value].some((character) => {
43
+ const code = character.codePointAt(0) ?? 0;
44
+ return code <= 0x20
45
+ || (code >= 0x7f && code <= 0x9f)
46
+ || "\"'<>".includes(character);
47
+ });
48
+ /** Canonicalize a reviewed full simple-index URL without widening its scope. */
49
+ export const canonicalRecipeIndexUrl = (value) => {
50
+ if (value.trim() !== value
51
+ || value.includes("#")
52
+ || value.includes("\\")
53
+ || hasUnsafeIndexUrlCharacter(value))
54
+ return undefined;
55
+ try {
56
+ const parsed = new URL(value);
57
+ if (parsed.protocol !== "https:"
58
+ || parsed.username.length > 0
59
+ || parsed.password.length > 0
60
+ || parsed.hash.length > 0
61
+ || parsed.pathname === "/"
62
+ || !/(?:^|\/)simple\/?$/u.test(parsed.pathname))
63
+ return undefined;
64
+ parsed.pathname = parsed.pathname.replace(/\/$/u, "");
65
+ return parsed.href;
66
+ }
67
+ catch {
68
+ return undefined;
69
+ }
70
+ };
71
+ const npmVersion = /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-(?:0|[1-9][0-9]*|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9][0-9]*|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/u;
72
+ const homebrewVersion = /^[0-9A-Za-z][0-9A-Za-z.+_~+-]*$/u;
73
+ const wingetVersion = /^[0-9A-Za-z][0-9A-Za-z.+_~-]*$/u;
74
+ const uvVersion = /^[0-9A-Za-z][0-9A-Za-z!+._-]*$/u;
75
+ const cargoVersion = npmVersion;
76
+ const aptVersion = /^[0-9A-Za-z][0-9A-Za-z.+:~_+-]*$/u;
77
+ const sourceRevision = /^[0-9A-Za-z][0-9A-Za-z._~/-]*$/u;
78
+ const unsafePackageArgument = /[\p{Cc}\s%=&|<>`"'$()[\]{}]/u;
79
+ const unsafeVersionCharacter = /[\p{Cc}\s%/@?#=\\;&|<>`"'$()[\]{}]/u;
80
+ const isSafeScalar = (value, unsafe) => value.length > 0
81
+ && value === value.trim()
82
+ && !value.startsWith("-")
83
+ && !value.startsWith("+")
84
+ && !unsafe.test(value);
85
+ export const isSafePackageArgument = (value) => isSafeScalar(value, unsafePackageArgument);
86
+ const versionPatternFor = (method) => {
87
+ switch (method) {
88
+ case "npm":
89
+ case "pnpm":
90
+ case "bun":
91
+ return npmVersion;
92
+ case "brew":
93
+ case "homebrew":
94
+ return homebrewVersion;
95
+ case "winget":
96
+ return wingetVersion;
97
+ case "uv":
98
+ return uvVersion;
99
+ case "cargo":
100
+ return cargoVersion;
101
+ case "apt":
102
+ return aptVersion;
103
+ case "source":
104
+ return sourceRevision;
105
+ default:
106
+ return undefined;
107
+ }
108
+ };
109
+ /**
110
+ * Return a stable, boundary-friendly explanation when a recipe cannot be
111
+ * represented by the deterministic installer.
112
+ */
113
+ export const recipeValidationError = (input) => {
114
+ const { method, package: packageName, version } = input;
115
+ if (!Schema.is(RecipeMethod)(method)) {
116
+ return `unknown installer method ${method}`;
117
+ }
118
+ if ((method === "npm" || method === "pnpm" || method === "bun")
119
+ && !isNpmRegistryPackageName(packageName)) {
120
+ return `npm-family package must be an exact registry name: ${packageName}`;
121
+ }
122
+ if (method === "source" && version === undefined) {
123
+ return "source recipes require an immutable revision";
124
+ }
125
+ if (method !== "source" && !isSafePackageArgument(packageName)) {
126
+ return `package argument is unsafe: ${packageName}`;
127
+ }
128
+ if (input.indexPolicy !== undefined) {
129
+ if (method !== "uv") {
130
+ return "recipe index policy is only supported for uv recipes";
131
+ }
132
+ const { url, reviewedBy, reviewedAt } = input.indexPolicy;
133
+ if (reviewedBy.trim().length === 0 || !Number.isFinite(Date.parse(reviewedAt))) {
134
+ return "recipe index policy requires a reviewer and valid review timestamp";
135
+ }
136
+ if (canonicalRecipeIndexUrl(url) === undefined) {
137
+ return "recipe index policy must be a credential-free HTTPS simple-index URL";
138
+ }
139
+ }
140
+ const metadata = sourceValue(input.source);
141
+ const integrity = metadata?.integrity ?? input.integrity;
142
+ const sourceVersion = ((method === "npm" || method === "pnpm" || method === "bun")
143
+ && version === undefined)
144
+ ? npmVersionFromTarballSource(packageName, metadata?.source ?? "")
145
+ : version;
146
+ if ((method === "npm" || method === "pnpm" || method === "bun")
147
+ && version === undefined
148
+ && (sourceVersion === undefined
149
+ || integrity === undefined)) {
150
+ return "npm-family registry installs require an exact version or a reviewed tarball with integrity";
151
+ }
152
+ const sourceReason = recipeSourceValidationError({
153
+ ...input,
154
+ version: sourceVersion,
155
+ });
156
+ if (sourceReason !== undefined)
157
+ return sourceReason;
158
+ if (version === undefined && sourceVersion === undefined)
159
+ return undefined;
160
+ const pattern = versionPatternFor(method);
161
+ if (pattern === undefined
162
+ || !isSafeScalar(sourceVersion, unsafeVersionCharacter)
163
+ || !pattern.test(sourceVersion)) {
164
+ return `installer ${method} cannot honor requested version ${sourceVersion}`;
165
+ }
166
+ return undefined;
167
+ };
168
+ /** Whether an automatic recipe lacks a version that its installer can honor. */
169
+ export const isMissingAutomaticRecipeVersion = (input) => {
170
+ if (!Schema.is(AutomaticRecipeMethod)(input.method))
171
+ return false;
172
+ if (input.version !== undefined)
173
+ return false;
174
+ if (!isSafePackageArgument(input.package))
175
+ return false;
176
+ if ((input.method === "npm" || input.method === "pnpm" || input.method === "bun")
177
+ && !isNpmRegistryPackageName(input.package)) {
178
+ return false;
179
+ }
180
+ const metadata = sourceValue(input.source);
181
+ const integrity = metadata?.integrity ?? input.integrity;
182
+ const remoteSource = metadata?.source.startsWith("http:") === true
183
+ || metadata?.source.startsWith("https:") === true;
184
+ const npmFamily = input.method === "npm"
185
+ || input.method === "pnpm"
186
+ || input.method === "bun";
187
+ if (remoteSource && !npmFamily)
188
+ return false;
189
+ if (remoteSource && npmFamily && integrity !== undefined)
190
+ return false;
191
+ return !(npmFamily
192
+ && npmVersionFromTarballSource(input.package, metadata?.source ?? "") !== undefined
193
+ && integrity !== undefined);
194
+ };
195
+ const sourceValue = (source) => source === undefined
196
+ ? undefined
197
+ : Schema.is(Schema.String)(source)
198
+ ? { source }
199
+ : source;
200
+ const isSRI = (value) => /^(?:sha256|sha512)-[A-Za-z0-9+/]+={0,2}$/u.test(value);
201
+ const npmTarballPath = (packageName, version) => {
202
+ const packagePart = packageName.startsWith("@")
203
+ ? packageName.slice(packageName.indexOf("/") + 1)
204
+ : packageName;
205
+ return `/${packageName}/-/${packagePart}-${version}.tgz`;
206
+ };
207
+ export const npmVersionFromTarballSource = (packageName, source) => {
208
+ const packagePart = packageName.startsWith("@")
209
+ ? packageName.slice(packageName.indexOf("/") + 1)
210
+ : packageName;
211
+ const prefix = `https://registry.npmjs.org/${packageName}/-/${packagePart}-`;
212
+ if (!source.startsWith(prefix) || !source.endsWith(".tgz"))
213
+ return undefined;
214
+ const candidate = source.slice(prefix.length, -".tgz".length);
215
+ return npmVersion.test(candidate) ? candidate : undefined;
216
+ };
217
+ /**
218
+ * Validate the reviewed source without turning a lockfile locator into a
219
+ * package-manager argument. Non-URL locators remain useful evidence and are
220
+ * installed from the canonical registry; URL sources are only accepted for
221
+ * exact npm registry tarballs.
222
+ */
223
+ export const recipeSourceValidationError = (input) => {
224
+ const metadata = sourceValue(input.source);
225
+ const integrity = metadata?.integrity ?? input.integrity;
226
+ if (input.integrity !== undefined && metadata?.integrity !== undefined) {
227
+ return "recipe source integrity is duplicated";
228
+ }
229
+ if (integrity !== undefined && !isSRI(integrity)) {
230
+ return "recipe source integrity must be a valid sha256 or sha512 SRI value";
231
+ }
232
+ if (metadata === undefined)
233
+ return undefined;
234
+ let url;
235
+ try {
236
+ url = new URL(metadata.source);
237
+ }
238
+ catch {
239
+ // Lockfile paths and evidence locators are not install arguments.
240
+ if ((input.method === "npm" || input.method === "pnpm" || input.method === "bun")
241
+ && integrity !== undefined) {
242
+ return "npm-family recipe integrity requires a canonical HTTPS registry tarball source";
243
+ }
244
+ return undefined;
245
+ }
246
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
247
+ // Schemes such as lock:, package:, and file paths are evidence locators,
248
+ // not network sources.
249
+ if (/^(?:git\+|git:|ssh:|github:|gitlab:|bitbucket:|file:|link:|workspace:|npm:)/iu.test(metadata.source)) {
250
+ return "mutable Git or source dependency metadata is not an approved package artifact";
251
+ }
252
+ return undefined;
253
+ }
254
+ if (url.protocol !== "https:"
255
+ || url.username.length > 0
256
+ || url.password.length > 0
257
+ || url.search.length > 0
258
+ || url.hash.length > 0
259
+ || url.port.length > 0 && url.port !== "443") {
260
+ return "recipe source must be an exact HTTPS URL without credentials, redirects, query, or fragment";
261
+ }
262
+ if (input.method !== "npm"
263
+ && input.method !== "pnpm"
264
+ && input.method !== "bun"
265
+ && input.method !== "source") {
266
+ return "recipe source URLs are only supported for npm-family registry artifacts or reviewed source recipes";
267
+ }
268
+ if (input.method === "npm" || input.method === "pnpm" || input.method === "bun") {
269
+ const expectedSource = `https://registry.npmjs.org${npmTarballPath(input.package, input.version ?? "")}`;
270
+ if (input.version === undefined || metadata.source !== expectedSource) {
271
+ return "npm-family recipe source must be the canonical registry tarball for package and version";
272
+ }
273
+ if (url.origin !== "https://registry.npmjs.org") {
274
+ return "npm-family recipe source must use the canonical npm registry origin";
275
+ }
276
+ let path;
277
+ try {
278
+ path = decodeURIComponent(url.pathname);
279
+ }
280
+ catch {
281
+ return "npm-family recipe source has an invalid encoded path";
282
+ }
283
+ if (input.version === undefined || path !== npmTarballPath(input.package, input.version)) {
284
+ return "npm-family recipe source must match the exact registry tarball for package and version";
285
+ }
286
+ }
287
+ return undefined;
288
+ };
289
+ export const recipeSourceDetails = (source, integrity) => {
290
+ const metadata = sourceValue(source);
291
+ return {
292
+ source: metadata?.source,
293
+ integrity: metadata?.integrity ?? integrity,
294
+ };
295
+ };
296
+ export const isSafeRecipe = (input) => recipeValidationError(input) === undefined;
297
+ export const isSafeSourceRevision = (value) => isSafeScalar(value, unsafeVersionCharacter) && sourceRevision.test(value);
@@ -0,0 +1,259 @@
1
+ import { Schema } from "effect";
2
+ import { BlobId, ContentDigest, CredentialReference, GroupName, ResourceId, ToolId, } from "./brand.js";
3
+ import { AutomaticRecipeMethod as AutomaticRecipeMethodSchema, RecipeSourceMetadata as RecipeSourceMetadataSchema, RecipeIndexPolicy as RecipeIndexPolicySchema, RecipeMethod as RecipeMethodSchema, recipeValidationError, } from "./recipe-versions.js";
4
+ /**
5
+ * Profile Resource kinds and Apply Policies from the architecture contract.
6
+ * The defaults table is the single source of truth for policy-kind defaults.
7
+ */
8
+ export const ResourceKind = Schema.Literals([
9
+ "file",
10
+ "directory",
11
+ "config",
12
+ "skill",
13
+ "tool",
14
+ "credential",
15
+ "schedule",
16
+ ]);
17
+ export const ApplyPolicy = Schema.Literals([
18
+ "replace",
19
+ "mirror-owned",
20
+ "merge",
21
+ "replace-if-unmodified",
22
+ "ensure",
23
+ "require-local",
24
+ ]);
25
+ /** The default Apply Policy for each resource kind, per the architecture contract. */
26
+ export const defaultPolicyForKind = {
27
+ file: "replace",
28
+ directory: "mirror-owned",
29
+ config: "merge",
30
+ skill: "replace-if-unmodified",
31
+ tool: "ensure",
32
+ credential: "require-local",
33
+ schedule: "replace",
34
+ };
35
+ /** Which policies are compatible with which kinds. `ensure` and `require-local` are kind-specific. */
36
+ const compatiblePolicies = {
37
+ file: ["replace", "replace-if-unmodified"],
38
+ directory: ["mirror-owned", "replace"],
39
+ config: ["merge", "replace"],
40
+ skill: ["replace-if-unmodified", "replace"],
41
+ tool: ["ensure"],
42
+ credential: ["require-local"],
43
+ schedule: ["replace"],
44
+ };
45
+ export const policyCompatibleWithKind = (kind, policy) => compatiblePolicies[kind].some((candidate) => candidate === policy);
46
+ /** Platform selector for installation recipes and path mapping. */
47
+ export const Platform = Schema.Literals(["linux", "macos", "windows"]);
48
+ export const RecipeMethod = RecipeMethodSchema;
49
+ export const AutomaticRecipeMethod = AutomaticRecipeMethodSchema;
50
+ /**
51
+ * A build policy is part of the reviewed recipe contract. The default keeps
52
+ * package lifecycle hooks disabled. A required build policy records the
53
+ * bounds a future sandboxed builder would need; the current process executor
54
+ * deliberately escalates this mode to Human Action Required.
55
+ */
56
+ export const BuildCapability = Schema.Literals([
57
+ "read-files",
58
+ "write-files",
59
+ "network",
60
+ "execute",
61
+ ]);
62
+ export const BuildStep = Schema.Struct({
63
+ executable: Schema.NonEmptyString,
64
+ arguments: Schema.Array(Schema.String),
65
+ });
66
+ export const BuildPolicy = Schema.Union([
67
+ Schema.Struct({
68
+ mode: Schema.Literal("scripts-disabled"),
69
+ }),
70
+ Schema.Struct({
71
+ mode: Schema.Literal("required"),
72
+ reviewedBy: Schema.NonEmptyString,
73
+ reviewedAt: Schema.NonEmptyString,
74
+ executables: Schema.Array(Schema.NonEmptyString),
75
+ paths: Schema.Array(Schema.NonEmptyString),
76
+ origins: Schema.Array(Schema.NonEmptyString),
77
+ capabilities: Schema.Array(BuildCapability),
78
+ steps: Schema.Array(BuildStep),
79
+ }),
80
+ ]);
81
+ export { RecipeSourceMetadataSchema as RecipeSourceMetadata };
82
+ export const RecipeIndexPolicy = RecipeIndexPolicySchema;
83
+ /**
84
+ * A Profile Resource: one named item of desired configuration.
85
+ * `spec` carries kind-specific fields decoded by `resourceSpecSchema`.
86
+ */
87
+ export const GroupFilter = Schema.Struct({
88
+ anyOf: Schema.Array(GroupName),
89
+ });
90
+ export const DirectoryFile = Schema.Struct({
91
+ path: Schema.NonEmptyString,
92
+ blob: BlobId,
93
+ executable: Schema.Boolean,
94
+ });
95
+ export const FileResourceSpec = Schema.Struct({
96
+ kind: Schema.Literal("file"),
97
+ content: Schema.String,
98
+ digest: ContentDigest,
99
+ executable: Schema.Boolean,
100
+ symlinkTo: Schema.optional(Schema.NonEmptyString),
101
+ });
102
+ export const DirectoryResourceSpec = Schema.Struct({
103
+ kind: Schema.Literal("directory"),
104
+ files: Schema.Array(DirectoryFile),
105
+ });
106
+ export const ConfigFormat = Schema.Literals(["toml", "json", "yaml"]);
107
+ export const ConfigValue = Schema.Union([
108
+ Schema.String,
109
+ Schema.Number,
110
+ Schema.Boolean,
111
+ Schema.Array(Schema.String),
112
+ ]);
113
+ export const ConfigKey = Schema.Struct({
114
+ path: Schema.NonEmptyString,
115
+ value: ConfigValue,
116
+ });
117
+ export const ConfigResourceSpec = Schema.Struct({
118
+ kind: Schema.Literal("config"),
119
+ format: ConfigFormat,
120
+ keys: Schema.Array(ConfigKey),
121
+ });
122
+ export const SkillResourceSpec = Schema.Struct({
123
+ kind: Schema.Literal("skill"),
124
+ name: Schema.NonEmptyString,
125
+ files: Schema.Array(DirectoryFile),
126
+ });
127
+ const ToolRecipeRefSchema = Schema.Struct({
128
+ platform: Platform,
129
+ method: RecipeMethod,
130
+ package: Schema.NonEmptyString,
131
+ version: Schema.optional(Schema.NonEmptyString),
132
+ indexPolicy: Schema.optional(RecipeIndexPolicySchema),
133
+ buildPolicy: Schema.optional(BuildPolicy),
134
+ source: Schema.optional(Schema.Union([
135
+ Schema.NonEmptyString,
136
+ RecipeSourceMetadataSchema,
137
+ ])),
138
+ });
139
+ export const ToolRecipeRef = ToolRecipeRefSchema.check(Schema.makeFilter((recipe) => {
140
+ const reason = recipeValidationError(recipe);
141
+ return reason === undefined
142
+ ? undefined
143
+ : { path: ["version"], issue: reason };
144
+ }));
145
+ export const LoginRequirement = Schema.Union([
146
+ Schema.Struct({ required: Schema.Literal(false) }),
147
+ Schema.Struct({
148
+ required: Schema.Literal(true),
149
+ howTo: Schema.NonEmptyString,
150
+ }),
151
+ ]);
152
+ export const ToolResourceSpec = Schema.Struct({
153
+ kind: Schema.Literal("tool"),
154
+ toolId: ToolId,
155
+ recipes: Schema.Array(ToolRecipeRef),
156
+ login: LoginRequirement,
157
+ });
158
+ export const CredentialResourceSpec = Schema.Struct({
159
+ kind: Schema.Literal("credential"),
160
+ reference: CredentialReference,
161
+ });
162
+ export const ScheduleCalendar = Schema.Union([
163
+ Schema.Struct({
164
+ type: Schema.Literal("daily"),
165
+ at: Schema.NonEmptyString,
166
+ }),
167
+ Schema.Struct({
168
+ type: Schema.Literal("weekly"),
169
+ days: Schema.Array(Schema.NonEmptyString),
170
+ at: Schema.NonEmptyString,
171
+ }),
172
+ Schema.Struct({
173
+ type: Schema.Literal("custom"),
174
+ expression: Schema.NonEmptyString,
175
+ }),
176
+ ]);
177
+ export const ScheduleResourceSpec = Schema.Struct({
178
+ kind: Schema.Literal("schedule"),
179
+ calendar: ScheduleCalendar,
180
+ timezone: Schema.NonEmptyString,
181
+ });
182
+ export const ResourceSpec = Schema.Union([
183
+ FileResourceSpec,
184
+ DirectoryResourceSpec,
185
+ ConfigResourceSpec,
186
+ SkillResourceSpec,
187
+ ToolResourceSpec,
188
+ CredentialResourceSpec,
189
+ ScheduleResourceSpec,
190
+ ]);
191
+ /** How to verify a resource reached its desired state. */
192
+ export const VerificationSpec = Schema.Union([
193
+ Schema.Struct({
194
+ method: Schema.Literal("digest"),
195
+ digest: ContentDigest,
196
+ }),
197
+ Schema.Struct({
198
+ method: Schema.Literal("command"),
199
+ command: Schema.Array(Schema.NonEmptyString),
200
+ expectContains: Schema.optional(Schema.String),
201
+ }),
202
+ Schema.Struct({
203
+ method: Schema.Literal("executable-present"),
204
+ executable: Schema.NonEmptyString,
205
+ }),
206
+ Schema.Struct({
207
+ method: Schema.Literal("credential-present"),
208
+ reference: CredentialReference,
209
+ }),
210
+ Schema.Struct({
211
+ method: Schema.Literal("symlink"),
212
+ target: Schema.NonEmptyString,
213
+ }),
214
+ ]);
215
+ export const ProfileResource = Schema.Struct({
216
+ id: ResourceId,
217
+ kind: ResourceKind,
218
+ policy: ApplyPolicy,
219
+ target: Schema.NonEmptyString,
220
+ group: Schema.optional(GroupFilter),
221
+ dependsOn: Schema.Array(ResourceId),
222
+ spec: ResourceSpec,
223
+ verify: VerificationSpec,
224
+ });
225
+ /** A discovered tool entry from the source scan, per the profile contract. */
226
+ export const InvocationEvidence = Schema.Struct({
227
+ source: Schema.NonEmptyString,
228
+ line: Schema.Int.check(Schema.isGreaterThan(0)),
229
+ invocation: Schema.NonEmptyString,
230
+ resolvedExecutable: Schema.optional(Schema.NonEmptyString),
231
+ packageManager: Schema.optional(Schema.NonEmptyString),
232
+ });
233
+ const ToolRecipeSchema = Schema.Struct({
234
+ platform: Platform,
235
+ method: RecipeMethod,
236
+ package: Schema.NonEmptyString,
237
+ version: Schema.optional(Schema.NonEmptyString),
238
+ source: Schema.optional(Schema.Union([
239
+ Schema.NonEmptyString,
240
+ RecipeSourceMetadataSchema,
241
+ ])),
242
+ });
243
+ export const ToolRecipe = ToolRecipeSchema.check(Schema.makeFilter((recipe) => {
244
+ const reason = recipeValidationError(recipe);
245
+ return reason === undefined
246
+ ? undefined
247
+ : { path: ["version"], issue: reason };
248
+ }));
249
+ export const DiscoveredTool = Schema.Struct({
250
+ id: ToolId,
251
+ upstream: Schema.NonEmptyString,
252
+ evidence: Schema.Array(InvocationEvidence),
253
+ recipes: Schema.Array(ToolRecipe),
254
+ verify: Schema.Struct({
255
+ command: Schema.Array(Schema.NonEmptyString),
256
+ expectContains: Schema.optional(Schema.String),
257
+ }),
258
+ login: LoginRequirement,
259
+ });