@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,939 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { readFile } from "node:fs/promises";
3
+ import { Effect, Option, Redacted, Schema } from "effect";
4
+ import { AgentResolution, executableAllowed, isNestedCommandLauncher, } from "../agent/agent-resolution.service.js";
5
+ import { ActionId, BlobId, ContentDigest, CredentialReference, ProfileId, ProfileRevisionId, ResourceId, RunId, SourceSignature, } from "../domain/brand.js";
6
+ import { ResourceSpecInputSchema, } from "../domain/profile.js";
7
+ import { fetchRevision, listRevisions, } from "../enrollment/follower-client.js";
8
+ import { MachineState } from "../machine/machine-state.service.js";
9
+ import { ScheduleManager } from "../schedule/schedule-manager.service.js";
10
+ import { defaultSyncSchedule, syncScheduleFromResourceSpec, syncScheduleFromDefault, } from "../schedule/schedule-manager.types.js";
11
+ import { canonicalJson, sha256BytesHex, sha256Hex, } from "../profile/profile-codec.js";
12
+ import { StateRepository } from "../state/state-repository.service.js";
13
+ import { Synchronization } from "./synchronization.service.js";
14
+ import { getConfigPath, parseConfigDocument, serializeConfigDocument, setConfigPath, } from "./config-codec.js";
15
+ import { FollowerSynchronizationConfigurationError, } from "./follower-sync-config.js";
16
+ import { planSynchronization } from "./planner.js";
17
+ const encoder = new TextEncoder();
18
+ const decoder = new TextDecoder();
19
+ const isNotFoundFilesystemError = (error) => /\b(?:ENOENT|ENOTDIR)\b/u.test(error.message);
20
+ const configurationError = (reason, message) => new FollowerSynchronizationConfigurationError({ reason, message });
21
+ export const loadFollowerSynchronizationConfiguration = Effect.fn("FollowerOrchestration.loadConfiguration")(function* (stateLocation) {
22
+ const repository = yield* StateRepository;
23
+ const configuration = yield* repository
24
+ .getFollowerSynchronizationConfiguration()
25
+ .pipe(Effect.mapError(() => configurationError("stale", "follower synchronization configuration is unreadable")));
26
+ if (configuration === undefined) {
27
+ return yield* configurationError("missing", "follower synchronization configuration is not enrolled");
28
+ }
29
+ if (configuration.stateLocation !== stateLocation) {
30
+ return yield* configurationError("stale", "follower synchronization configuration belongs to another state repository");
31
+ }
32
+ let endpoint;
33
+ try {
34
+ endpoint = new URL(configuration.source.endpoint);
35
+ }
36
+ catch {
37
+ return yield* configurationError("stale", "configured source endpoint is malformed");
38
+ }
39
+ if (endpoint.protocol !== "https:"
40
+ || (endpoint.hostname !== "127.0.0.1"
41
+ && endpoint.hostname !== "[::1]"
42
+ && endpoint.hostname !== "::1")) {
43
+ return yield* configurationError("stale", "configured source endpoint is not pinned loopback HTTPS");
44
+ }
45
+ const state = yield* repository.loadState(configuration.follower.id).pipe(Effect.mapError(() => configurationError("stale", "configured follower identity is unavailable")));
46
+ if (state.follower.id !== configuration.follower.id
47
+ || state.follower.credentialReference !== configuration.credentialReference
48
+ || state.follower.revoked
49
+ || state.sourceIdentity?.publicKeyFingerprint
50
+ !== configuration.source.signingFingerprint) {
51
+ return yield* configurationError("stale", "configured follower, source, or credential reference is stale");
52
+ }
53
+ return configuration;
54
+ });
55
+ const transportInput = (configuration, signal) => ({
56
+ endpoint: configuration.source.endpoint,
57
+ tlsFingerprint: configuration.source.tlsFingerprint,
58
+ credentialReference: configuration.credentialReference,
59
+ sourceFingerprint: configuration.source.signingFingerprint,
60
+ timeoutMilliseconds: configuration.scheduledInvocation.timeoutMilliseconds,
61
+ signal,
62
+ });
63
+ const selectedRevision = Effect.fn("FollowerOrchestration.selectedRevision")(function* (configuration, requestedRevision, signal) {
64
+ const revisions = yield* listRevisions(transportInput(configuration, signal));
65
+ const matching = revisions.revisions
66
+ .filter((revision) => revision.profileId === configuration.selectedProfile)
67
+ .sort((left, right) => right.sequence - left.sequence);
68
+ const selected = requestedRevision === undefined
69
+ ? matching[0]
70
+ : matching.find((revision) => revision.id === requestedRevision);
71
+ if (selected === undefined) {
72
+ return yield* configurationError("invalid-profile", requestedRevision === undefined
73
+ ? `selected profile ${configuration.selectedProfile} has no authorized revision`
74
+ : `recovery revision ${requestedRevision} is no longer authorized`);
75
+ }
76
+ return selected;
77
+ });
78
+ const decodeSpecs = (fetched) => Effect.forEach(fetched.metadata.resources, (resource) => Effect.gen(function* () {
79
+ if (resource.blobs.length !== 1) {
80
+ return yield* configurationError("stale", `resource ${resource.id} does not have one canonical content blob`);
81
+ }
82
+ const blob = fetched.blobs.find((entry) => entry.id === resource.blobs[0]);
83
+ if (blob === undefined) {
84
+ return yield* configurationError("stale", `resource ${resource.id} is missing its verified content blob`);
85
+ }
86
+ const blobBytes = yield* Effect.tryPromise({
87
+ try: () => readFile(blob.path),
88
+ catch: () => configurationError("stale", `verified cache blob for ${resource.id} is unavailable`),
89
+ });
90
+ const spec = yield* Effect.try({
91
+ try: () => Schema.decodeUnknownSync(ResourceSpecInputSchema)(JSON.parse(decoder.decode(blobBytes))),
92
+ catch: () => configurationError("stale", `verified content blob for ${resource.id} is malformed`),
93
+ });
94
+ if (spec.kind !== resource.kind) {
95
+ return yield* configurationError("stale", `resource ${resource.id} kind does not match its verified content`);
96
+ }
97
+ return { resource, spec, blob, blobBytes };
98
+ }));
99
+ const fileDigest = (content) => Schema.decodeUnknownSync(ContentDigest)(sha256BytesHex(encoder.encode(content)));
100
+ const filesDigest = (files) => Schema.decodeUnknownSync(ContentDigest)(sha256Hex([...files]
101
+ .sort((left, right) => left.path.localeCompare(right.path))
102
+ .map((file) => `${file.path}\0${file.digest}\0${file.executable ? "x" : "-"}`)
103
+ .join("\n")));
104
+ const configDocument = (spec) => {
105
+ const document = {};
106
+ for (const entry of [...spec.keys].sort((left, right) => left.path.localeCompare(right.path))) {
107
+ setConfigPath(document, entry.path, entry.value);
108
+ }
109
+ return encoder.encode(serializeConfigDocument(spec.format, document));
110
+ };
111
+ export const authorizationViewIdentity = (metadata) => {
112
+ const suffix = `view:${metadata.metadataDigest}`;
113
+ return {
114
+ revision: Schema.decodeUnknownSync(ProfileRevisionId)(`${metadata.id}:${suffix}`),
115
+ profile: Schema.decodeUnknownSync(ProfileId)(`${metadata.profileId}:${suffix}`),
116
+ };
117
+ };
118
+ const desiredFor = (spec) => {
119
+ switch (spec.kind) {
120
+ case "file": {
121
+ const content = encoder.encode(spec.content);
122
+ const digest = Schema.decodeUnknownSync(ContentDigest)(spec.symlinkTo === undefined
123
+ ? sha256BytesHex(content)
124
+ : sha256Hex(spec.symlinkTo));
125
+ return {
126
+ desired: {
127
+ kind: "file",
128
+ digest,
129
+ executable: spec.executable ?? false,
130
+ symlinkTo: spec.symlinkTo,
131
+ },
132
+ artifacts: spec.symlinkTo === undefined ? [{ digest, content }] : [],
133
+ };
134
+ }
135
+ case "directory":
136
+ case "skill": {
137
+ const files = spec.files.map((file) => ({
138
+ path: file.path,
139
+ digest: fileDigest(file.content),
140
+ executable: file.executable ?? false,
141
+ }));
142
+ const artifacts = spec.files.map((file, index) => ({
143
+ digest: files[index].digest,
144
+ content: encoder.encode(file.content),
145
+ }));
146
+ const digest = filesDigest(files);
147
+ return {
148
+ desired: spec.kind === "skill"
149
+ ? { kind: "skill", digest, files }
150
+ : { kind: "directory", files },
151
+ artifacts,
152
+ };
153
+ }
154
+ case "config": {
155
+ const content = configDocument(spec);
156
+ const digest = Schema.decodeUnknownSync(ContentDigest)(sha256BytesHex(content));
157
+ return {
158
+ desired: {
159
+ kind: "config",
160
+ digest,
161
+ format: spec.format,
162
+ keys: spec.keys
163
+ .map((entry) => entry.path)
164
+ .sort((left, right) => left.localeCompare(right)),
165
+ },
166
+ artifacts: [{ digest, content }],
167
+ };
168
+ }
169
+ case "tool":
170
+ return {
171
+ desired: {
172
+ kind: "tool",
173
+ toolId: spec.toolId,
174
+ recipes: spec.recipes,
175
+ loginRequired: spec.login?.required ?? false,
176
+ loginInstructions: spec.login?.required === true
177
+ ? spec.login.howTo
178
+ : undefined,
179
+ },
180
+ artifacts: [],
181
+ };
182
+ case "credential":
183
+ return {
184
+ desired: {
185
+ kind: "credential",
186
+ reference: spec.reference,
187
+ instructions: `Store credential ${spec.reference} in MachineState secure storage, then rerun synchronization.`,
188
+ },
189
+ artifacts: [],
190
+ };
191
+ case "schedule": {
192
+ const content = encoder.encode(canonicalJson(Schema.decodeUnknownSync(Schema.MutableJson)(spec)));
193
+ const digest = Schema.decodeUnknownSync(ContentDigest)(sha256BytesHex(content));
194
+ return {
195
+ desired: {
196
+ kind: "schedule",
197
+ digest,
198
+ schedule: syncScheduleFromResourceSpec(spec),
199
+ },
200
+ artifacts: [{ digest, content }],
201
+ };
202
+ }
203
+ }
204
+ };
205
+ const observeFile = (target, desired) => Effect.gen(function* () {
206
+ const machine = yield* MachineState;
207
+ const path = yield* machine.normalizePath({ path: target });
208
+ const observedKind = yield* machine.inspectPath(path);
209
+ if (observedKind.kind === "symlink") {
210
+ const expected = desired.symlinkTo === undefined
211
+ ? undefined
212
+ : yield* machine.normalizePath({ path: desired.symlinkTo });
213
+ return yield* machine.readSymlink(path).pipe(Effect.map((symlinkTo) => ({
214
+ state: "present",
215
+ digest: expected?.absolute === symlinkTo.absolute
216
+ ? desired.digest
217
+ : sha256Hex(symlinkTo.absolute),
218
+ executable: false,
219
+ objectKind: "symlink",
220
+ symlinkTo: symlinkTo.absolute,
221
+ })));
222
+ }
223
+ if (observedKind.kind !== "regular") {
224
+ return {
225
+ state: "present",
226
+ digest: sha256Hex(`canonfig:observed-object:${observedKind.kind}`),
227
+ executable: false,
228
+ objectKind: observedKind.kind,
229
+ };
230
+ }
231
+ if (desired.symlinkTo !== undefined) {
232
+ return {
233
+ state: "present",
234
+ digest: sha256Hex("canonfig:observed-object:regular"),
235
+ executable: false,
236
+ objectKind: "regular",
237
+ };
238
+ }
239
+ return yield* machine.digestFile({ path }).pipe(Effect.flatMap((digest) => machine.permissions(path).pipe(Effect.map((permissions) => ({
240
+ state: "present",
241
+ digest: digest.value,
242
+ executable: permissions.executableByOwner,
243
+ objectKind: "regular",
244
+ })))), Effect.catchTag("MachineFilesystemError", (error) => Effect.succeed(error.message.includes("ENOENT")
245
+ ? { state: "absent" }
246
+ : { state: "unverifiable", reason: error.message })), Effect.catch((error) => Effect.succeed({ state: "unverifiable", reason: String(error) })));
247
+ }).pipe(Effect.catchTag("MachineFilesystemError", (error) => Effect.succeed(error.message.includes("ENOENT")
248
+ ? { state: "absent" }
249
+ : { state: "unverifiable", reason: error.message })), Effect.catch((error) => Effect.succeed({ state: "unverifiable", reason: String(error) })));
250
+ const observeConfig = (decoded, desired) => Effect.gen(function* () {
251
+ const machine = yield* MachineState;
252
+ const path = yield* machine.normalizePath({ path: decoded.resource.target });
253
+ const observedKind = yield* machine.inspectPath(path);
254
+ if (observedKind.kind !== "regular") {
255
+ return {
256
+ state: "present",
257
+ digest: sha256Hex(`canonfig:observed-object:${observedKind.kind}`),
258
+ executable: false,
259
+ objectKind: observedKind.kind,
260
+ };
261
+ }
262
+ const bytes = yield* machine.readFile({
263
+ path,
264
+ maximumBytes: 8 * 1024 * 1024,
265
+ });
266
+ const current = parseConfigDocument(desired.format, decoder.decode(bytes));
267
+ const managed = {};
268
+ for (const key of desired.keys) {
269
+ const value = getConfigPath(current, key);
270
+ if (value !== undefined)
271
+ setConfigPath(managed, key, value);
272
+ }
273
+ return {
274
+ state: "present",
275
+ digest: sha256BytesHex(encoder.encode(serializeConfigDocument(desired.format, managed))),
276
+ executable: false,
277
+ objectKind: "regular",
278
+ };
279
+ }).pipe(Effect.catch((error) => Effect.succeed(String(error).includes("ENOENT")
280
+ ? { state: "absent" }
281
+ : { state: "unverifiable", reason: String(error) })));
282
+ const observe = (decoded, desired, verification, applied, scheduleManager) => {
283
+ switch (desired.kind) {
284
+ case "file":
285
+ return observeFile(decoded.resource.target, desired);
286
+ case "config":
287
+ return observeConfig(decoded, desired);
288
+ case "schedule":
289
+ if (scheduleManager === undefined) {
290
+ return Effect.succeed({
291
+ state: "unverifiable",
292
+ reason: "ScheduleManager is unavailable",
293
+ });
294
+ }
295
+ return scheduleManager.status({ schedule: desired.schedule }).pipe(Effect.map((status) => status.state === "current"
296
+ ? {
297
+ state: "present",
298
+ digest: desired.digest,
299
+ executable: false,
300
+ }
301
+ : { state: "absent" }), Effect.catch(() => Effect.succeed({
302
+ state: "unverifiable",
303
+ reason: "native scheduler status is unavailable",
304
+ })));
305
+ case "directory":
306
+ case "skill":
307
+ return Effect.gen(function* () {
308
+ const machine = yield* MachineState;
309
+ const root = yield* machine.normalizePath({ path: decoded.resource.target });
310
+ const rootKind = yield* machine.inspectPath(root).pipe(Effect.catchTag("MachineFilesystemError", (error) => isNotFoundFilesystemError(error)
311
+ ? Effect.succeed(undefined)
312
+ : Effect.fail(error)));
313
+ if (rootKind === undefined)
314
+ return { state: "absent" };
315
+ if (rootKind.kind !== "directory") {
316
+ return {
317
+ state: "present",
318
+ digest: sha256Hex(`canonfig:observed-object:${rootKind.kind}`),
319
+ executable: false,
320
+ objectKind: rootKind.kind,
321
+ };
322
+ }
323
+ const candidates = [...new Map([
324
+ ...(applied?.ownedFiles ?? []).map((file) => ({
325
+ ...file,
326
+ executable: file.executable ?? false,
327
+ })),
328
+ ...desired.files,
329
+ ].map((file) => [file.path, file])).values()];
330
+ const files = yield* Effect.forEach(candidates, (file) => Effect.gen(function* () {
331
+ const path = yield* machine.normalizePath({ path: file.path, base: root });
332
+ const kind = yield* machine.inspectPath(path).pipe(Effect.catchTag("MachineFilesystemError", (error) => isNotFoundFilesystemError(error)
333
+ ? Effect.succeed(undefined)
334
+ : Effect.fail(error)));
335
+ if (kind === undefined) {
336
+ return { path: file.path, state: "absent" };
337
+ }
338
+ if (kind.kind !== "regular") {
339
+ return {
340
+ path: file.path,
341
+ digest: sha256Hex(`canonfig:observed-object:${kind.kind}`),
342
+ executable: false,
343
+ objectKind: kind.kind,
344
+ };
345
+ }
346
+ return yield* machine.digestFile({ path }).pipe(Effect.flatMap((digest) => machine.permissions(path).pipe(Effect.map((permissions) => ({
347
+ path: file.path,
348
+ digest: digest.value,
349
+ executable: permissions.executableByOwner,
350
+ objectKind: "regular",
351
+ })))), Effect.catchTag("MachineFilesystemError", (error) => isNotFoundFilesystemError(error)
352
+ ? Effect.succeed({ path: file.path, state: "absent" })
353
+ : Effect.fail(error)));
354
+ }));
355
+ return { state: "directory", objectKind: "directory", files };
356
+ }).pipe(Effect.catch((error) => Effect.succeed({ state: "unverifiable", reason: String(error) })));
357
+ case "tool":
358
+ return Effect.gen(function* () {
359
+ const machine = yield* MachineState;
360
+ const executable = verification.method === "executable-present"
361
+ ? verification.executable
362
+ : verification.method === "command"
363
+ ? verification.command[0] ?? desired.toolId
364
+ : desired.toolId;
365
+ if (executable.includes("/") || executable.includes("\\")) {
366
+ return yield* machine.normalizePath({ path: executable }).pipe(Effect.flatMap((path) => machine.permissions(path)), Effect.as({
367
+ state: "present",
368
+ digest: sha256Hex(executable),
369
+ executable: true,
370
+ }), Effect.catch(() => Effect.succeed({ state: "absent" })));
371
+ }
372
+ return yield* machine.findExecutable({ name: executable }).pipe(Effect.as({ state: "present", digest: sha256Hex(executable), executable: true }), Effect.catch(() => Effect.succeed({ state: "absent" })));
373
+ });
374
+ case "credential":
375
+ return Effect.gen(function* () {
376
+ const machine = yield* MachineState;
377
+ const reference = Schema.decodeUnknownSync(CredentialReference)(verification.method === "credential-present"
378
+ ? verification.reference
379
+ : desired.reference);
380
+ return yield* machine.loadCredential({ reference }).pipe(Effect.map((value) => {
381
+ Redacted.value(value);
382
+ return {
383
+ state: "present",
384
+ digest: sha256Hex(desired.reference),
385
+ executable: false,
386
+ };
387
+ }), Effect.catch(() => Effect.succeed({ state: "absent" })));
388
+ });
389
+ }
390
+ };
391
+ const removedResourceState = (applied) => {
392
+ if (applied.kind === undefined
393
+ || applied.policy === undefined
394
+ || applied.target === undefined) {
395
+ return undefined;
396
+ }
397
+ const resource = {
398
+ id: applied.resource,
399
+ kind: applied.kind,
400
+ policy: applied.policy,
401
+ target: applied.target,
402
+ dependsOn: [],
403
+ blobs: [],
404
+ };
405
+ const digest = Schema.decodeUnknownSync(ContentDigest)(applied.digest);
406
+ switch (applied.kind) {
407
+ case "file":
408
+ return {
409
+ resource,
410
+ desired: {
411
+ kind: "file",
412
+ digest,
413
+ executable: applied.executable ?? false,
414
+ symlinkTo: applied.symlinkTo,
415
+ },
416
+ };
417
+ case "directory":
418
+ case "skill":
419
+ if (applied.ownedFiles === undefined)
420
+ return undefined;
421
+ return {
422
+ resource,
423
+ desired: {
424
+ kind: applied.kind,
425
+ digest,
426
+ files: applied.ownedFiles.map((file) => ({
427
+ path: file.path,
428
+ digest: Schema.decodeUnknownSync(ContentDigest)(file.digest),
429
+ executable: file.executable ?? false,
430
+ })),
431
+ },
432
+ };
433
+ case "config":
434
+ if (applied.ownedKeys === undefined
435
+ || applied.configFormat === undefined) {
436
+ return undefined;
437
+ }
438
+ return {
439
+ resource,
440
+ desired: {
441
+ kind: "config",
442
+ digest,
443
+ format: applied.configFormat,
444
+ keys: applied.ownedKeys,
445
+ },
446
+ };
447
+ case "schedule":
448
+ if (applied.schedule === undefined)
449
+ return undefined;
450
+ return {
451
+ resource,
452
+ desired: {
453
+ kind: "schedule",
454
+ digest,
455
+ schedule: applied.schedule,
456
+ },
457
+ };
458
+ case "tool":
459
+ case "credential":
460
+ return undefined;
461
+ }
462
+ };
463
+ const hydrateRevision = Effect.fn("FollowerOrchestration.hydrateRevision")(function* (fetched, appliedResources = [], scheduleManager) {
464
+ const decoded = yield* decodeSpecs(fetched);
465
+ const desired = [];
466
+ const observations = [];
467
+ const artifacts = [];
468
+ const blobs = [];
469
+ const removedResources = [];
470
+ const appliedByResource = new Map(appliedResources.map((record) => [
471
+ record.resource,
472
+ record,
473
+ ]));
474
+ for (const entry of decoded) {
475
+ const hydration = desiredFor(entry.spec);
476
+ desired.push({
477
+ resource: entry.resource.id,
478
+ desired: hydration.desired,
479
+ verification: entry.resource.verify,
480
+ });
481
+ observations.push({
482
+ resource: entry.resource.id,
483
+ observed: yield* observe(entry, hydration.desired, entry.resource.verify, appliedByResource.get(entry.resource.id), scheduleManager),
484
+ });
485
+ artifacts.push({ digest: entry.blob.id, content: entry.blobBytes }, ...hydration.artifacts);
486
+ blobs.push({
487
+ id: Schema.decodeUnknownSync(BlobId)(entry.blob.id),
488
+ bytes: entry.blobBytes.byteLength,
489
+ });
490
+ }
491
+ const currentIds = new Set(fetched.metadata.resources.map((resource) => resource.id));
492
+ for (const applied of [...appliedResources].sort((left, right) => left.resource.localeCompare(right.resource))) {
493
+ if (currentIds.has(applied.resource))
494
+ continue;
495
+ const removed = removedResourceState(applied);
496
+ if (removed === undefined)
497
+ continue;
498
+ removedResources.push(applied.resource);
499
+ desired.push({
500
+ resource: removed.resource.id,
501
+ desired: removed.desired,
502
+ verification: { method: "digest", digest: applied.digest },
503
+ });
504
+ observations.push({
505
+ resource: removed.resource.id,
506
+ observed: yield* observe({ resource: removed.resource }, removed.desired, { method: "digest", digest: applied.digest }, applied, scheduleManager),
507
+ });
508
+ }
509
+ const metadata = fetched.metadata;
510
+ const view = authorizationViewIdentity(metadata);
511
+ const canonicalBytes = canonicalJson(Schema.decodeUnknownSync(Schema.MutableJson)({
512
+ sourceRevision: metadata.id,
513
+ metadataDigest: metadata.metadataDigest,
514
+ resources: metadata.resources,
515
+ }));
516
+ const base = {
517
+ id: view.revision,
518
+ profileId: view.profile,
519
+ sequence: metadata.sequence,
520
+ canonicalBytes,
521
+ digest: sha256Hex(canonicalBytes),
522
+ signature: Schema.decodeUnknownSync(SourceSignature)(metadata.sourceSignature),
523
+ publishedAt: metadata.publishedAt,
524
+ scheduleDefault: metadata.scheduleDefault,
525
+ resources: [
526
+ ...metadata.resources.map(({ verify: _, ...resource }) => resource),
527
+ ...removedResources.map((resource) => removedResourceState(appliedByResource.get(resource)).resource),
528
+ ],
529
+ groups: [],
530
+ };
531
+ return {
532
+ revision: { ...base, desired, blobs, removedResources },
533
+ observations,
534
+ artifacts: [...new Map(artifacts.map((entry) => [entry.digest, entry])).values()],
535
+ };
536
+ });
537
+ const persistableRevision = (revision) => ({
538
+ id: revision.id,
539
+ profileId: revision.profileId,
540
+ sequence: revision.sequence,
541
+ canonicalBytes: revision.canonicalBytes,
542
+ digest: revision.digest,
543
+ signature: revision.signature,
544
+ publishedAt: revision.publishedAt,
545
+ resources: revision.resources
546
+ .filter((resource) => !revision.removedResources?.includes(resource.id))
547
+ .map((resource) => ({
548
+ id: resource.id,
549
+ kind: resource.kind,
550
+ policy: resource.policy,
551
+ target: resource.target,
552
+ groups: resource.groups,
553
+ dependsOn: resource.dependsOn,
554
+ blobs: resource.blobs,
555
+ })),
556
+ groups: revision.groups,
557
+ scheduleDefault: revision.scheduleDefault,
558
+ });
559
+ const pathWithinHarnessBounds = (path, configuration) => configuration.allowedPaths.some((root) => {
560
+ const normalizedRoot = root.replaceAll("\\", "/").replace(/\/+$/u, "");
561
+ const normalizedPath = path.replaceAll("\\", "/");
562
+ return normalizedPath === normalizedRoot
563
+ || normalizedPath.startsWith(`${normalizedRoot}/`);
564
+ });
565
+ const originWithinHarnessBounds = (origin, configuration) => {
566
+ try {
567
+ const normalized = new URL(origin).origin;
568
+ return configuration.allowedOrigins.some((allowed) => new URL(allowed).origin === normalized);
569
+ }
570
+ catch {
571
+ return false;
572
+ }
573
+ };
574
+ const harnessConfigurationIssue = (configuration) => {
575
+ if (configuration.executable.trim() !== configuration.executable) {
576
+ return "agent harness executable reference is invalid";
577
+ }
578
+ if (configuration.environment?.some((entry) => entry.name.trim() !== entry.name || entry.name.includes("=")) === true) {
579
+ return "agent harness environment overrides are invalid";
580
+ }
581
+ if (configuration.allowedPaths.some((path) => path.trim() !== path)) {
582
+ return "agent harness path bounds are invalid";
583
+ }
584
+ if (configuration.allowedExecutables.some((executable) => executable.trim() !== executable)
585
+ || configuration.executableAuthorizations?.some((authorization) => authorization.executable.trim() !== authorization.executable) === true) {
586
+ return "agent harness executable bounds are invalid";
587
+ }
588
+ if (configuration.executableAuthorizations?.some((authorization) => isNestedCommandLauncher(authorization.executable)) === true) {
589
+ return "agent harness executable bounds include an unboundable nested-command launcher";
590
+ }
591
+ if (configuration.executableAuthorizations?.some((authorization) => authorization.behavior === "script-interpreter") === true) {
592
+ return "agent harness script-interpreter execution requires an unavailable cross-platform sandbox";
593
+ }
594
+ for (const origin of configuration.allowedOrigins) {
595
+ try {
596
+ const url = new URL(origin);
597
+ if (url.protocol !== "https:" || url.origin !== origin) {
598
+ return "agent harness origin bounds must be exact HTTPS origins";
599
+ }
600
+ }
601
+ catch {
602
+ return "agent harness origin bounds are invalid";
603
+ }
604
+ }
605
+ return undefined;
606
+ };
607
+ const boundedTask = (task, configuration) => Effect.gen(function* () {
608
+ const allowedExecutables = [];
609
+ for (const executable of task.allowedExecutables) {
610
+ if (yield* executableAllowed(executable, configuration.allowedExecutables, configuration.environment, task.allowedPaths[0] ?? process.cwd())) {
611
+ allowedExecutables.push(executable);
612
+ }
613
+ }
614
+ // Authorizations may only claim executables that survived the harness
615
+ // allowlist; otherwise the bounded task would over-claim its bounds.
616
+ const boundedAuthorizations = task.executableAuthorizations?.filter((authorization) => allowedExecutables.includes(authorization.executable));
617
+ return {
618
+ ...task,
619
+ allowedPaths: task.allowedPaths.filter((path) => pathWithinHarnessBounds(path, configuration)),
620
+ allowedExecutables,
621
+ executableAuthorizations: boundedAuthorizations,
622
+ allowedOrigins: task.allowedOrigins.filter((origin) => originWithinHarnessBounds(origin, configuration)),
623
+ forbidden: [...new Set([
624
+ ...task.forbidden,
625
+ ...["elevation", "login", "restart", "reboot"].filter((capability) => !configuration.allowedCapabilities.includes(capability)),
626
+ ])],
627
+ };
628
+ });
629
+ const recanonicalizePlan = (plan, actions) => {
630
+ const body = {
631
+ revision: plan.revision,
632
+ follower: plan.follower,
633
+ requiredBlobs: plan.requiredBlobs,
634
+ actions,
635
+ agentTasks: plan.agentTasks,
636
+ };
637
+ const encoded = canonicalJson(Schema.decodeUnknownSync(Schema.MutableJson)(JSON.parse(JSON.stringify(body))));
638
+ return { ...body, encoded, digest: sha256Hex(encoded) };
639
+ };
640
+ const profileScheduleDefaultResource = Schema.decodeUnknownSync(ResourceId)("canonfig.schedule-default");
641
+ const appendProfileScheduleDefaultAction = (plan, configuration, scheduleDefault) => {
642
+ const previousSchedule = configuration.scheduleDefault === undefined
643
+ ? undefined
644
+ : syncScheduleFromDefault(configuration.scheduleDefault);
645
+ const operation = scheduleDefault === undefined ? "remove" : "upsert";
646
+ const schedule = scheduleDefault === undefined
647
+ ? previousSchedule ?? defaultSyncSchedule
648
+ : syncScheduleFromDefault(scheduleDefault);
649
+ const action = {
650
+ id: Schema.decodeUnknownSync(ActionId)("action:canonfig.schedule-default:0:schedule-default"),
651
+ resource: profileScheduleDefaultResource,
652
+ kind: "schedule-default",
653
+ detail: {
654
+ kind: "schedule-default",
655
+ operation,
656
+ schedule,
657
+ previousSchedule,
658
+ },
659
+ before: plan.actions.map((candidate) => candidate.id),
660
+ };
661
+ return recanonicalizePlan(plan, [...plan.actions, action]);
662
+ };
663
+ const planProfileScheduleDefault = Effect.fn("FollowerOrchestration.planProfileScheduleDefault")(function* (plan, configuration, scheduleDefault, scheduleManager) {
664
+ const previousSchedule = configuration.scheduleDefault === undefined
665
+ ? undefined
666
+ : syncScheduleFromDefault(configuration.scheduleDefault);
667
+ const operation = scheduleDefault === undefined ? "remove" : "upsert";
668
+ const schedule = scheduleDefault === undefined
669
+ ? previousSchedule ?? defaultSyncSchedule
670
+ : syncScheduleFromDefault(scheduleDefault);
671
+ if (scheduleManager !== undefined) {
672
+ const status = yield* scheduleManager.status({ schedule }).pipe(Effect.match({
673
+ onFailure: () => undefined,
674
+ onSuccess: (value) => value,
675
+ }));
676
+ const current = operation === "remove"
677
+ ? status?.state === "not-installed"
678
+ : status?.state === "current";
679
+ if (current)
680
+ return plan;
681
+ }
682
+ return appendProfileScheduleDefaultAction(plan, configuration, scheduleDefault);
683
+ });
684
+ const agentConfigurationFor = (configuration, scheduled, signal) => ({
685
+ policy: configuration.agentPolicy,
686
+ harness: configuration.agentHarness === undefined
687
+ ? undefined
688
+ : {
689
+ harness: configuration.agentHarness.kind,
690
+ executable: configuration.agentHarness.executable,
691
+ environment: configuration.agentHarness.environment,
692
+ maximumInputBytes: configuration.agentHarness.maximumInputBytes,
693
+ allowedPaths: configuration.agentHarness.allowedPaths,
694
+ allowedExecutables: configuration.agentHarness.allowedExecutables,
695
+ executableAuthorizations: configuration.agentHarness.executableAuthorizations,
696
+ allowedOrigins: configuration.agentHarness.allowedOrigins,
697
+ allowedCapabilities: configuration.agentHarness.allowedCapabilities,
698
+ },
699
+ scheduled,
700
+ signal,
701
+ });
702
+ const persistProfileScheduleDefault = Effect.fn("FollowerOrchestration.persistProfileScheduleDefault")(function* (configuration, scheduleDefault) {
703
+ const repository = yield* StateRepository;
704
+ const state = yield* repository.loadState(configuration.follower.id);
705
+ if (state.sourceIdentity === undefined)
706
+ return;
707
+ yield* repository.saveFollowerSynchronizationConfiguration({
708
+ sourceIdentity: state.sourceIdentity,
709
+ configuration: {
710
+ ...configuration,
711
+ scheduleDefault,
712
+ updatedAt: new Date().toISOString(),
713
+ },
714
+ });
715
+ });
716
+ export const resolveAgentTasks = Effect.fn("FollowerOrchestration.resolveAgentTasks")(function* (configuration, plan, scheduled, signal, planning = false) {
717
+ const noResolutions = [];
718
+ if (plan.agentTasks.length === 0
719
+ || configuration.agentPolicy === "deterministic-only") {
720
+ return {
721
+ plan,
722
+ agentResolutions: noResolutions,
723
+ };
724
+ }
725
+ const harness = configuration.agentHarness;
726
+ const harnessIssue = harness === undefined
727
+ ? "Agent harness is not configured"
728
+ : harnessConfigurationIssue(harness);
729
+ if (harness === undefined || harnessIssue !== undefined) {
730
+ const actions = plan.actions.map((action) => action.detail.kind === "agent-task"
731
+ ? {
732
+ ...action,
733
+ kind: "human-action",
734
+ detail: {
735
+ kind: "human-action",
736
+ reason: `${harnessIssue} for ${action.detail.summary}`,
737
+ instructions: "Configure a supported bounded agent harness, or switch to deterministic-only policy, then rerun synchronization.",
738
+ },
739
+ }
740
+ : action);
741
+ return {
742
+ plan: recanonicalizePlan(plan, actions),
743
+ agentResolutions: noResolutions,
744
+ };
745
+ }
746
+ const agent = yield* AgentResolution;
747
+ const resolutions = [];
748
+ const replacements = new Map();
749
+ const reasons = new Map();
750
+ for (const task of plan.agentTasks) {
751
+ const bounded = yield* boundedTask(task, harness);
752
+ const resolution = yield* agent.resolve({
753
+ policy: planning ? "agent-propose" : configuration.agentPolicy,
754
+ task: bounded,
755
+ harness: {
756
+ harness: harness.kind,
757
+ executable: harness.executable,
758
+ environment: harness.environment,
759
+ maximumInputBytes: harness.maximumInputBytes,
760
+ allowedPaths: harness.allowedPaths,
761
+ allowedExecutables: harness.allowedExecutables,
762
+ executableAuthorizations: harness.executableAuthorizations,
763
+ allowedOrigins: harness.allowedOrigins,
764
+ allowedCapabilities: harness.allowedCapabilities,
765
+ },
766
+ scheduled,
767
+ signal,
768
+ }).pipe(Effect.match({
769
+ onFailure: (error) => ({ error }),
770
+ onSuccess: (outcome) => ({ outcome }),
771
+ }));
772
+ if ("error" in resolution) {
773
+ replacements.set(task.id, "human");
774
+ reasons.set(task.id, `Configured agent harness could not safely resolve the task: ${resolution.error.message.slice(0, 1024)}`);
775
+ continue;
776
+ }
777
+ resolutions.push(resolution.outcome);
778
+ replacements.set(task.id, resolution.outcome.outcome === "applied" ? "resolved" : "human");
779
+ if (resolution.outcome.outcome === "proposed") {
780
+ reasons.set(task.id, `Agent proposal requires human review: ${resolution.outcome.proposal.summary}`);
781
+ }
782
+ }
783
+ const actions = plan.actions.map((action) => {
784
+ if (action.detail.kind !== "agent-task")
785
+ return action;
786
+ if (planning)
787
+ return action;
788
+ const replacement = replacements.get(action.detail.taskId);
789
+ if (replacement === "resolved") {
790
+ return {
791
+ ...action,
792
+ kind: "no-op",
793
+ detail: { kind: "no-op" },
794
+ };
795
+ }
796
+ return {
797
+ ...action,
798
+ kind: "human-action",
799
+ detail: {
800
+ kind: "human-action",
801
+ reason: reasons.get(action.detail.taskId)
802
+ ?? `Bounded agent task requires resolution: ${action.detail.summary}`,
803
+ instructions: `Resolve task ${action.detail.taskId} under the configured bounds, then rerun synchronization.`,
804
+ },
805
+ };
806
+ });
807
+ return {
808
+ plan: recanonicalizePlan(plan, actions),
809
+ agentResolutions: resolutions,
810
+ };
811
+ });
812
+ export const synchronizeFollower = Effect.fn("FollowerOrchestration.synchronize")(function* (stateLocation, mode, signal, scheduled = false) {
813
+ const repository = yield* StateRepository;
814
+ const machine = yield* MachineState;
815
+ const synchronization = yield* Synchronization;
816
+ const agentResolution = Option.getOrUndefined(yield* Effect.serviceOption(AgentResolution));
817
+ const scheduleManager = Option.getOrUndefined(yield* Effect.serviceOption(ScheduleManager));
818
+ const configuration = yield* loadFollowerSynchronizationConfiguration(stateLocation);
819
+ const selected = yield* selectedRevision(configuration, undefined, signal);
820
+ const fetched = yield* fetchRevision({
821
+ ...transportInput(configuration, signal),
822
+ revisionId: selected.id,
823
+ cacheDirectory: configuration.cacheDirectory,
824
+ maximumMetadataBytes: configuration.scheduledInvocation.maximumMetadataBytes,
825
+ maximumBlobBytes: configuration.scheduledInvocation.maximumBlobBytes,
826
+ }).pipe(Effect.provideService(MachineState, machine));
827
+ const appliedResources = yield* repository.loadAppliedResources(configuration.follower.id);
828
+ const hydrated = yield* hydrateRevision(fetched, appliedResources, scheduleManager).pipe(Effect.provideService(MachineState, machine));
829
+ if (mode === "apply") {
830
+ yield* repository.publishRevision({
831
+ revision: persistableRevision(hydrated.revision),
832
+ });
833
+ }
834
+ const plan = yield* planSynchronization({
835
+ revision: hydrated.revision,
836
+ follower: configuration.follower.id,
837
+ observedState: {
838
+ platform: (yield* machine.userDirectories()).home.platform,
839
+ resources: hydrated.observations,
840
+ availableBlobs: fetched.blobs.map((blob) => blob.id),
841
+ },
842
+ localOverlay: configuration.localOverlay ?? [],
843
+ appliedResources,
844
+ });
845
+ const planWithSchedule = yield* planProfileScheduleDefault(plan, configuration, fetched.metadata.scheduleDefault, scheduleManager);
846
+ const noAgentResolutions = [];
847
+ const planned = mode === "plan"
848
+ ? yield* resolveAgentTasks(configuration, planWithSchedule, scheduled, signal, true)
849
+ : { plan: planWithSchedule, agentResolutions: noAgentResolutions };
850
+ const appliedAgentResolutions = [];
851
+ const journaledAgentResolution = mode === "apply" && agentResolution !== undefined
852
+ ? AgentResolution.of({
853
+ resolve: (input) => agentResolution.resolve(input).pipe(Effect.tap((outcome) => Effect.sync(() => {
854
+ appliedAgentResolutions.push(outcome);
855
+ }))),
856
+ proposeProfileChange: agentResolution.proposeProfileChange,
857
+ })
858
+ : undefined;
859
+ if (mode === "plan") {
860
+ return {
861
+ mode,
862
+ revision: selected.id,
863
+ downloadedBlobs: fetched.downloadedBlobs,
864
+ reusedBlobs: fetched.reusedBlobs,
865
+ plan: planned.plan,
866
+ agentResolutions: planned.agentResolutions,
867
+ };
868
+ }
869
+ const outcome = yield* synchronization.run({
870
+ id: Schema.decodeUnknownSync(RunId)(`run-${randomUUID()}`),
871
+ plan: planned.plan,
872
+ revision: hydrated.revision,
873
+ appliedResources,
874
+ artifacts: hydrated.artifacts,
875
+ agent: agentConfigurationFor(configuration, scheduled, signal),
876
+ agentResolution: journaledAgentResolution,
877
+ limits: {
878
+ processTimeoutMilliseconds: configuration.scheduledInvocation.timeoutMilliseconds,
879
+ },
880
+ });
881
+ if (outcome.outcome === "Converged") {
882
+ yield* persistProfileScheduleDefault(configuration, fetched.metadata.scheduleDefault);
883
+ }
884
+ return {
885
+ mode,
886
+ revision: selected.id,
887
+ downloadedBlobs: fetched.downloadedBlobs,
888
+ reusedBlobs: fetched.reusedBlobs,
889
+ agentResolutions: appliedAgentResolutions,
890
+ outcome,
891
+ };
892
+ });
893
+ export const recoverFollower = Effect.fn("FollowerOrchestration.recover")(function* (stateLocation, signal) {
894
+ const repository = yield* StateRepository;
895
+ const machine = yield* MachineState;
896
+ const synchronization = yield* Synchronization;
897
+ const agentResolution = Option.getOrUndefined(yield* Effect.serviceOption(AgentResolution));
898
+ const scheduleManager = Option.getOrUndefined(yield* Effect.serviceOption(ScheduleManager));
899
+ const configuration = yield* loadFollowerSynchronizationConfiguration(stateLocation);
900
+ const recovery = yield* repository.loadRecovery(configuration.follower.id);
901
+ if (recovery === undefined) {
902
+ return yield* configurationError("stale", "no durable interrupted synchronization run is available");
903
+ }
904
+ const sourceRevision = recovery.run.revision.replace(/:view:[a-f0-9]{64}$/u, "");
905
+ const selected = yield* selectedRevision(configuration, sourceRevision, signal);
906
+ const fetched = yield* fetchRevision({
907
+ ...transportInput(configuration, signal),
908
+ revisionId: selected.id,
909
+ cacheDirectory: configuration.cacheDirectory,
910
+ maximumMetadataBytes: configuration.scheduledInvocation.maximumMetadataBytes,
911
+ maximumBlobBytes: configuration.scheduledInvocation.maximumBlobBytes,
912
+ }).pipe(Effect.provideService(MachineState, machine));
913
+ const appliedResources = [
914
+ ...new Map([
915
+ ...(yield* repository.loadAppliedResources(configuration.follower.id)),
916
+ ...recovery.removedResources,
917
+ ].map((record) => [record.resource, record])).values(),
918
+ ];
919
+ const hydrated = yield* hydrateRevision(fetched, appliedResources, scheduleManager).pipe(Effect.provideService(MachineState, machine));
920
+ const outcome = yield* synchronization.recover({
921
+ follower: configuration.follower.id,
922
+ revision: hydrated.revision,
923
+ artifacts: hydrated.artifacts,
924
+ agent: agentConfigurationFor(configuration, false, signal),
925
+ agentResolution,
926
+ limits: {
927
+ processTimeoutMilliseconds: configuration.scheduledInvocation.timeoutMilliseconds,
928
+ },
929
+ });
930
+ if (outcome.outcome === "Converged") {
931
+ yield* persistProfileScheduleDefault(configuration, fetched.metadata.scheduleDefault);
932
+ }
933
+ return {
934
+ revision: selected.id,
935
+ downloadedBlobs: fetched.downloadedBlobs,
936
+ reusedBlobs: fetched.reusedBlobs,
937
+ outcome,
938
+ };
939
+ });