@microck/canonfig 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +263 -0
- package/dist/agent/agent-resolution.errors.js +42 -0
- package/dist/agent/agent-resolution.layer.js +204 -0
- package/dist/agent/agent-resolution.service.js +2259 -0
- package/dist/agent/agent-resolution.types.js +1 -0
- package/dist/agent/controlled-executor.js +704 -0
- package/dist/agent/harness-adapters.js +85 -0
- package/dist/cli/cli.js +618 -0
- package/dist/cli/exit-codes.js +28 -0
- package/dist/cli/follower-commands.js +3 -0
- package/dist/cli/render.js +56 -0
- package/dist/cli/source-commands.js +5 -0
- package/dist/domain/brand.js +29 -0
- package/dist/domain/identity.js +31 -0
- package/dist/domain/npm-package-spec.js +186 -0
- package/dist/domain/profile.js +950 -0
- package/dist/domain/recipe-versions.js +297 -0
- package/dist/domain/resource.js +259 -0
- package/dist/domain/synchronization.js +346 -0
- package/dist/enrollment/enrollment.errors.js +43 -0
- package/dist/enrollment/enrollment.layer.js +724 -0
- package/dist/enrollment/enrollment.service.js +3 -0
- package/dist/enrollment/enrollment.types.js +59 -0
- package/dist/enrollment/follower-client.js +585 -0
- package/dist/enrollment/source-server.js +313 -0
- package/dist/machine/linux.layer.js +1183 -0
- package/dist/machine/machine-state.errors.js +52 -0
- package/dist/machine/machine-state.service.js +3 -0
- package/dist/machine/machine-state.types.js +1 -0
- package/dist/machine/macos.layer.js +470 -0
- package/dist/machine/windows.layer.js +879 -0
- package/dist/profile/discovery.js +740 -0
- package/dist/profile/profile-catalog.errors.js +50 -0
- package/dist/profile/profile-catalog.layer.js +20 -0
- package/dist/profile/profile-catalog.service.js +7 -0
- package/dist/profile/profile-codec.js +153 -0
- package/dist/profile/publication.js +298 -0
- package/dist/profile/tool-catalog.js +384 -0
- package/dist/runtime/doctor.js +306 -0
- package/dist/runtime/layers.js +706 -0
- package/dist/runtime/main.js +38 -0
- package/dist/schedule/linux-schedule.js +24 -0
- package/dist/schedule/macos-schedule.js +25 -0
- package/dist/schedule/schedule-manager.errors.js +17 -0
- package/dist/schedule/schedule-manager.layer.js +205 -0
- package/dist/schedule/schedule-manager.service.js +3 -0
- package/dist/schedule/schedule-manager.types.js +114 -0
- package/dist/schedule/windows-schedule.js +25 -0
- package/dist/state/state-repository.errors.js +55 -0
- package/dist/state/state-repository.layer.js +1507 -0
- package/dist/state/state-repository.service.js +3 -0
- package/dist/state/state-repository.types.js +1 -0
- package/dist/state/state-schema.js +298 -0
- package/dist/synchronization/config-codec.js +97 -0
- package/dist/synchronization/executor.js +700 -0
- package/dist/synchronization/follower-orchestration.js +939 -0
- package/dist/synchronization/follower-sync-config.js +81 -0
- package/dist/synchronization/npm-artifact.js +670 -0
- package/dist/synchronization/planner.js +378 -0
- package/dist/synchronization/recovery.js +397 -0
- package/dist/synchronization/resource-executors.js +1198 -0
- package/dist/synchronization/resource-plans.js +645 -0
- package/dist/synchronization/synchronization.errors.js +102 -0
- package/dist/synchronization/synchronization.layer.js +97 -0
- package/dist/synchronization/synchronization.service.js +11 -0
- package/dist/synchronization/synchronization.types.js +1 -0
- package/package.json +66 -0
|
@@ -0,0 +1,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
|
+
});
|