@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,1198 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import { dirname, isAbsolute, join, relative, win32 } from "node:path";
|
|
3
|
+
import { CredentialReference } from "../domain/brand.js";
|
|
4
|
+
import { ResourceSpecInputSchema, } from "../domain/profile.js";
|
|
5
|
+
import { AutomaticRecipeMethod, } from "../domain/resource.js";
|
|
6
|
+
import { MachineState } from "../machine/machine-state.service.js";
|
|
7
|
+
import { sha256BytesHex, sha256Hex } from "../profile/profile-codec.js";
|
|
8
|
+
import { syncScheduleFromResourceSpec, } from "../schedule/schedule-manager.types.js";
|
|
9
|
+
import { ActionExecutionError, InvalidArtifactError, InvalidExecutionPlanError, MissingArtifactError, } from "./synchronization.errors.js";
|
|
10
|
+
import { getConfigPath, parseConfigDocument, removeConfigPath, serializeConfigDocument, setConfigPath, } from "./config-codec.js";
|
|
11
|
+
import { desiredResourceDigest } from "./resource-plans.js";
|
|
12
|
+
import { parseNpmPackageSpecification } from "../domain/npm-package-spec.js";
|
|
13
|
+
import { isMissingAutomaticRecipeVersion, recipeSourceDetails, recipeValidationError, canonicalRecipeIndexUrl, defaultPythonIndex, npmVersionFromTarballSource, } from "../domain/recipe-versions.js";
|
|
14
|
+
import { defaultNpmArtifactTransport, validateNpmArtifactProvenance, verifyNpmArtifactBytes, } from "./npm-artifact.js";
|
|
15
|
+
const isUnboundedNonNpmPackage = (value) => /^(?:git\+|git:\/\/|github:|gitlab:|bitbucket:|git@|file:|link:|workspace:|https?:\/\/)/iu
|
|
16
|
+
.test(value)
|
|
17
|
+
|| /(?:^|@)(?:npm:|git\+|git:\/\/|github:|gitlab:|bitbucket:|git@|file:|link:|workspace:|https?:\/\/)/iu
|
|
18
|
+
.test(value);
|
|
19
|
+
const storedState = (entry) => "state" in entry
|
|
20
|
+
? entry.state
|
|
21
|
+
: entry.existed
|
|
22
|
+
? "regular"
|
|
23
|
+
: "absent";
|
|
24
|
+
const StoredFileSchema = Schema.Union([
|
|
25
|
+
Schema.Struct({
|
|
26
|
+
path: Schema.NonEmptyString,
|
|
27
|
+
state: Schema.Literal("absent"),
|
|
28
|
+
}),
|
|
29
|
+
Schema.Struct({
|
|
30
|
+
path: Schema.NonEmptyString,
|
|
31
|
+
state: Schema.Literal("directory"),
|
|
32
|
+
}),
|
|
33
|
+
Schema.Struct({
|
|
34
|
+
path: Schema.NonEmptyString,
|
|
35
|
+
state: Schema.Literal("regular"),
|
|
36
|
+
content: Schema.String,
|
|
37
|
+
mode: Schema.Int,
|
|
38
|
+
}),
|
|
39
|
+
Schema.Struct({
|
|
40
|
+
path: Schema.NonEmptyString,
|
|
41
|
+
state: Schema.Literal("symlink"),
|
|
42
|
+
target: Schema.NonEmptyString,
|
|
43
|
+
}),
|
|
44
|
+
Schema.Struct({
|
|
45
|
+
path: Schema.NonEmptyString,
|
|
46
|
+
existed: Schema.Boolean,
|
|
47
|
+
content: Schema.String,
|
|
48
|
+
}),
|
|
49
|
+
]);
|
|
50
|
+
const encoder = new TextEncoder();
|
|
51
|
+
const decoder = new TextDecoder();
|
|
52
|
+
const scheduleInputFromSpec = (spec) => ({
|
|
53
|
+
schedule: syncScheduleFromResourceSpec(spec),
|
|
54
|
+
});
|
|
55
|
+
const scheduleInputFor = (context) => Effect.gen(function* () {
|
|
56
|
+
if (context.desired.kind !== "schedule") {
|
|
57
|
+
return yield* new InvalidExecutionPlanError({
|
|
58
|
+
message: `schedule action targets non-schedule resource ${context.resource.id}`,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
const digest = context.desired.digest;
|
|
62
|
+
const bytes = yield* artifact(context.artifacts, digest);
|
|
63
|
+
return yield* Effect.try({
|
|
64
|
+
try: () => {
|
|
65
|
+
const spec = Schema.decodeUnknownSync(ResourceSpecInputSchema)(JSON.parse(decoder.decode(bytes)));
|
|
66
|
+
if (spec.kind !== "schedule") {
|
|
67
|
+
throw new Error("schedule artifact does not contain a schedule specification");
|
|
68
|
+
}
|
|
69
|
+
return scheduleInputFromSpec(spec);
|
|
70
|
+
},
|
|
71
|
+
catch: (cause) => new InvalidArtifactError({
|
|
72
|
+
digest,
|
|
73
|
+
message: String(cause),
|
|
74
|
+
}),
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
const artifact = (artifacts, digest) => {
|
|
78
|
+
const value = artifacts.get(digest);
|
|
79
|
+
if (value === undefined)
|
|
80
|
+
return Effect.fail(new MissingArtifactError({ digest }));
|
|
81
|
+
const observed = sha256BytesHex(value.content);
|
|
82
|
+
if (observed !== digest) {
|
|
83
|
+
return Effect.fail(new InvalidArtifactError({
|
|
84
|
+
digest,
|
|
85
|
+
message: `artifact content digest was ${observed}`,
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
return Effect.succeed(value.content);
|
|
89
|
+
};
|
|
90
|
+
const readIfPresent = (path, maximumBytes) => Effect.gen(function* () {
|
|
91
|
+
const machine = yield* MachineState;
|
|
92
|
+
return yield* machine.readFile({ path, maximumBytes }).pipe(Effect.catchTag("MachineFilesystemError", (error) => error.message.includes("ENOENT")
|
|
93
|
+
? Effect.succeed(undefined)
|
|
94
|
+
: Effect.fail(error)));
|
|
95
|
+
});
|
|
96
|
+
const captureStoredFile = (path, maximumBytes) => Effect.gen(function* () {
|
|
97
|
+
const machine = yield* MachineState;
|
|
98
|
+
const kind = yield* machine.inspectPath(path).pipe(Effect.catchTag("MachineFilesystemError", (error) => error.message.includes("ENOENT")
|
|
99
|
+
? Effect.succeed(undefined)
|
|
100
|
+
: Effect.fail(error)));
|
|
101
|
+
if (kind === undefined)
|
|
102
|
+
return { path: path.absolute, state: "absent" };
|
|
103
|
+
if (kind.kind === "directory") {
|
|
104
|
+
return { path: path.absolute, state: "directory" };
|
|
105
|
+
}
|
|
106
|
+
const symlink = yield* machine.readSymlink(path).pipe(Effect.map((target) => target.absolute), Effect.catchTag("MachineFilesystemError", () => Effect.succeed(undefined)));
|
|
107
|
+
if (symlink !== undefined) {
|
|
108
|
+
return { path: path.absolute, state: "symlink", target: symlink };
|
|
109
|
+
}
|
|
110
|
+
const content = yield* readIfPresent(path, maximumBytes);
|
|
111
|
+
if (content === undefined)
|
|
112
|
+
return { path: path.absolute, state: "absent" };
|
|
113
|
+
const permissions = yield* machine.permissions(path);
|
|
114
|
+
return {
|
|
115
|
+
path: path.absolute,
|
|
116
|
+
state: "regular",
|
|
117
|
+
content: Buffer.from(content).toString("base64"),
|
|
118
|
+
mode: permissions.mode,
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
const restoreStoredFile = (entry, root) => Effect.gen(function* () {
|
|
122
|
+
const machine = yield* MachineState;
|
|
123
|
+
const path = yield* machine.normalizePath({ path: entry.path });
|
|
124
|
+
if ("existed" in entry) {
|
|
125
|
+
if (entry.existed) {
|
|
126
|
+
const content = Buffer.from(entry.content, "base64");
|
|
127
|
+
if (root === undefined) {
|
|
128
|
+
yield* machine.atomicWrite({ path, content });
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
yield* machine.mutateWithinRoot({
|
|
132
|
+
root,
|
|
133
|
+
path,
|
|
134
|
+
mutation: { kind: "write", content },
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
if (root === undefined) {
|
|
140
|
+
yield* machine.removeFile({ path });
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
yield* machine.mutateWithinRoot({
|
|
144
|
+
root,
|
|
145
|
+
path,
|
|
146
|
+
mutation: { kind: "remove" },
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
switch (entry.state) {
|
|
153
|
+
case "absent":
|
|
154
|
+
if (root === undefined) {
|
|
155
|
+
yield* machine.removeFile({ path });
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
const currentKind = yield* machine.inspectPath(path).pipe(Effect.catchTag("MachineFilesystemError", (error) => error.message.includes("ENOENT")
|
|
159
|
+
? Effect.succeed(undefined)
|
|
160
|
+
: Effect.fail(error)));
|
|
161
|
+
if (currentKind?.kind === "directory") {
|
|
162
|
+
yield* machine.removeEmptyDirectory({ path });
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
yield* machine.mutateWithinRoot({
|
|
166
|
+
root,
|
|
167
|
+
path,
|
|
168
|
+
mutation: { kind: "remove" },
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return;
|
|
173
|
+
case "directory":
|
|
174
|
+
yield* machine.ensureDirectory({ path });
|
|
175
|
+
return;
|
|
176
|
+
case "regular": {
|
|
177
|
+
const content = Buffer.from(entry.content, "base64");
|
|
178
|
+
if (root === undefined) {
|
|
179
|
+
yield* machine.atomicWrite({ path, content, mode: entry.mode });
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
yield* machine.mutateWithinRoot({
|
|
183
|
+
root,
|
|
184
|
+
path,
|
|
185
|
+
mutation: { kind: "write", content, mode: entry.mode },
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
case "symlink": {
|
|
191
|
+
const target = yield* machine.normalizePath({ path: entry.target });
|
|
192
|
+
if (root === undefined) {
|
|
193
|
+
yield* machine.replaceSymlink({ path, target });
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
yield* machine.mutateWithinRoot({
|
|
197
|
+
root,
|
|
198
|
+
path,
|
|
199
|
+
mutation: { kind: "symlink", target },
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
const normalizeRelative = (target, relative) => Effect.gen(function* () {
|
|
206
|
+
if (relative.length === 0
|
|
207
|
+
|| relative.startsWith("/")
|
|
208
|
+
|| relative.startsWith("\\")
|
|
209
|
+
|| /^[A-Za-z]:/u.test(relative)
|
|
210
|
+
|| relative.split(/[\\/]/u).includes("..")) {
|
|
211
|
+
return yield* new InvalidExecutionPlanError({
|
|
212
|
+
message: `mirror path must remain relative to its target: ${relative}`,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
const machine = yield* MachineState;
|
|
216
|
+
return yield* machine.normalizePath({ path: relative, base: target });
|
|
217
|
+
});
|
|
218
|
+
const sameMachinePath = (left, right) => left.platform === right.platform
|
|
219
|
+
&& (left.platform === "windows"
|
|
220
|
+
? left.absolute.toLowerCase() === right.absolute.toLowerCase()
|
|
221
|
+
: left.absolute === right.absolute);
|
|
222
|
+
const machinePathKey = (path) => `${path.platform}:${path.platform === "windows"
|
|
223
|
+
? path.absolute.toLowerCase()
|
|
224
|
+
: path.absolute}`;
|
|
225
|
+
const pathWithinMachineRoot = (root, path) => {
|
|
226
|
+
if (root.platform !== path.platform)
|
|
227
|
+
return false;
|
|
228
|
+
const remainder = root.platform === "windows"
|
|
229
|
+
? win32.relative(root.absolute.toLowerCase(), path.absolute.toLowerCase())
|
|
230
|
+
: relative(root.absolute, path.absolute);
|
|
231
|
+
return remainder === ""
|
|
232
|
+
|| (!remainder.startsWith("..")
|
|
233
|
+
&& !win32.isAbsolute(remainder)
|
|
234
|
+
&& !isAbsolute(remainder));
|
|
235
|
+
};
|
|
236
|
+
/**
|
|
237
|
+
* Return the exact deterministic rollback path set. Directory mutations need
|
|
238
|
+
* snapshots for every intermediate ancestor because a failed restart can
|
|
239
|
+
* otherwise leave newly-created nested directories behind. Keep this
|
|
240
|
+
* expansion shared by capture and recovery validation so the persisted
|
|
241
|
+
* material cannot be rejected or accepted under a different path contract.
|
|
242
|
+
*/
|
|
243
|
+
const rollbackPathSet = (paths, root) => Effect.gen(function* () {
|
|
244
|
+
const machine = yield* MachineState;
|
|
245
|
+
const normalizedRoot = root === undefined
|
|
246
|
+
? undefined
|
|
247
|
+
: yield* machine.normalizePath({ path: root.absolute });
|
|
248
|
+
const normalizedPaths = yield* Effect.forEach(paths, (path) => machine.normalizePath({ path: path.absolute }));
|
|
249
|
+
const pathsWithAncestors = new Map(normalizedPaths.map((path) => [machinePathKey(path), path]));
|
|
250
|
+
if (normalizedRoot !== undefined) {
|
|
251
|
+
for (const path of normalizedPaths) {
|
|
252
|
+
if (!pathWithinMachineRoot(normalizedRoot, path)) {
|
|
253
|
+
return yield* new InvalidExecutionPlanError({
|
|
254
|
+
message: `rollback path is outside managed root ${normalizedRoot.absolute}: ${path.absolute}`,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
if (sameMachinePath(path, normalizedRoot))
|
|
258
|
+
continue;
|
|
259
|
+
let ancestor = path.platform === "windows"
|
|
260
|
+
? win32.dirname(path.absolute)
|
|
261
|
+
: dirname(path.absolute);
|
|
262
|
+
while (!sameMachinePath({ platform: normalizedRoot.platform, absolute: ancestor }, normalizedRoot)) {
|
|
263
|
+
const candidate = {
|
|
264
|
+
platform: normalizedRoot.platform,
|
|
265
|
+
absolute: ancestor,
|
|
266
|
+
};
|
|
267
|
+
if (!pathWithinMachineRoot(normalizedRoot, candidate)) {
|
|
268
|
+
return yield* new InvalidExecutionPlanError({
|
|
269
|
+
message: `rollback ancestor is outside managed root ${normalizedRoot.absolute}: ${ancestor}`,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
const normalized = yield* machine.normalizePath({ path: ancestor });
|
|
273
|
+
if (!pathWithinMachineRoot(normalizedRoot, normalized)) {
|
|
274
|
+
return yield* new InvalidExecutionPlanError({
|
|
275
|
+
message: `rollback ancestor is outside managed root ${normalizedRoot.absolute}: ${normalized.absolute}`,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
pathsWithAncestors.set(machinePathKey(normalized), normalized);
|
|
279
|
+
const parent = normalized.platform === "windows"
|
|
280
|
+
? win32.dirname(normalized.absolute)
|
|
281
|
+
: dirname(normalized.absolute);
|
|
282
|
+
if (parent === normalized.absolute) {
|
|
283
|
+
return yield* new InvalidExecutionPlanError({
|
|
284
|
+
message: `rollback path ancestry did not reach managed root ${normalizedRoot.absolute}`,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
ancestor = parent;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return [...pathsWithAncestors.values()].sort((left, right) => left.platform.localeCompare(right.platform)
|
|
292
|
+
|| left.absolute.localeCompare(right.absolute));
|
|
293
|
+
});
|
|
294
|
+
const restoreOrder = (entries) => [...entries].sort((left, right) => right.path.split(/[\\/]/u).length - left.path.split(/[\\/]/u).length
|
|
295
|
+
|| right.path.length - left.path.length
|
|
296
|
+
|| left.path.localeCompare(right.path));
|
|
297
|
+
const captureRollback = (context, paths, root) => Effect.gen(function* () {
|
|
298
|
+
const machine = yield* MachineState;
|
|
299
|
+
const directories = yield* machine.userDirectories();
|
|
300
|
+
const rollbackDirectory = yield* machine.normalizePath({
|
|
301
|
+
path: `canonfig/rollback/${context.run}`,
|
|
302
|
+
base: directories.cache,
|
|
303
|
+
});
|
|
304
|
+
yield* machine.ensureDirectory({ path: rollbackDirectory });
|
|
305
|
+
const rollbackPath = yield* machine.normalizePath({
|
|
306
|
+
path: `${sha256Hex(context.action.id)}.json`,
|
|
307
|
+
base: rollbackDirectory,
|
|
308
|
+
});
|
|
309
|
+
const pathsWithAncestors = yield* rollbackPathSet(paths, root);
|
|
310
|
+
const stored = yield* Effect.forEach(pathsWithAncestors, (path) => captureStoredFile(path, context.limits.maximumFileBytes));
|
|
311
|
+
yield* machine.atomicWrite({
|
|
312
|
+
path: rollbackPath,
|
|
313
|
+
content: encoder.encode(JSON.stringify(stored)),
|
|
314
|
+
});
|
|
315
|
+
const restore = Effect.gen(function* () {
|
|
316
|
+
const rootEntry = root === undefined
|
|
317
|
+
? undefined
|
|
318
|
+
: stored.find((entry) => entry.path === root.absolute);
|
|
319
|
+
if (root !== undefined && rootEntry !== undefined && storedState(rootEntry) !== "absent") {
|
|
320
|
+
yield* restoreStoredFile(rootEntry);
|
|
321
|
+
}
|
|
322
|
+
if (rootEntry === undefined
|
|
323
|
+
|| storedState(rootEntry) === "absent"
|
|
324
|
+
|| storedState(rootEntry) === "directory") {
|
|
325
|
+
for (const entry of restoreOrder(stored)) {
|
|
326
|
+
if (entry === rootEntry)
|
|
327
|
+
continue;
|
|
328
|
+
yield* restoreStoredFile(entry, root);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (root !== undefined && rootEntry !== undefined && storedState(rootEntry) === "absent") {
|
|
332
|
+
yield* machine.removeEmptyDirectory({ path: root }).pipe(Effect.catchTag("MachineFilesystemError", (error) => error.message.includes("ENOENT") ? Effect.void : Effect.fail(error)));
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
return { reference: rollbackPath.absolute, restore };
|
|
336
|
+
});
|
|
337
|
+
const rollbackPaths = (context) => Effect.gen(function* () {
|
|
338
|
+
const detail = context.action.detail;
|
|
339
|
+
switch (detail.kind) {
|
|
340
|
+
case "write-file":
|
|
341
|
+
case "write-config":
|
|
342
|
+
return [yield* targetPath(detail.target)];
|
|
343
|
+
case "mirror-directory": {
|
|
344
|
+
const root = yield* targetPath(detail.target);
|
|
345
|
+
const descendants = yield* Effect.forEach([...new Set([...detail.adds, ...detail.removes])], (path) => normalizeRelative(root, path));
|
|
346
|
+
return yield* rollbackPathSet([root, ...descendants], root);
|
|
347
|
+
}
|
|
348
|
+
case "remove-resource": {
|
|
349
|
+
if (context.resource.kind === "directory" || context.resource.kind === "skill") {
|
|
350
|
+
const root = yield* targetPath(detail.target);
|
|
351
|
+
const descendants = yield* Effect.forEach(detail.paths, (path) => normalizeRelative(root, path));
|
|
352
|
+
return yield* rollbackPathSet([root, ...descendants], root);
|
|
353
|
+
}
|
|
354
|
+
if (context.resource.kind === "file" || context.resource.kind === "config") {
|
|
355
|
+
return [yield* targetPath(detail.target)];
|
|
356
|
+
}
|
|
357
|
+
return [];
|
|
358
|
+
}
|
|
359
|
+
default:
|
|
360
|
+
return [];
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
/**
|
|
364
|
+
* Restore a persisted, owned-file rollback snapshot. Both the reference and
|
|
365
|
+
* every stored target are re-derived from the immutable action before use.
|
|
366
|
+
*/
|
|
367
|
+
export const restoreRollbackReference = (context, reference) => Effect.gen(function* () {
|
|
368
|
+
const machine = yield* MachineState;
|
|
369
|
+
const directories = yield* machine.userDirectories();
|
|
370
|
+
const rollbackDirectory = yield* machine.normalizePath({
|
|
371
|
+
path: `canonfig/rollback/${context.run}`,
|
|
372
|
+
base: directories.cache,
|
|
373
|
+
});
|
|
374
|
+
const expectedReference = yield* machine.normalizePath({
|
|
375
|
+
path: `${sha256Hex(context.action.id)}.json`,
|
|
376
|
+
base: rollbackDirectory,
|
|
377
|
+
});
|
|
378
|
+
const actualReference = yield* machine.normalizePath({ path: reference });
|
|
379
|
+
if (actualReference.absolute !== expectedReference.absolute) {
|
|
380
|
+
return yield* new InvalidExecutionPlanError({
|
|
381
|
+
message: `rollback reference does not belong to action ${context.action.id}`,
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
const expectedPaths = yield* rollbackPaths(context);
|
|
385
|
+
const maximumBytes = context.limits.maximumFileBytes * Math.max(1, expectedPaths.length);
|
|
386
|
+
if (!Number.isSafeInteger(maximumBytes)) {
|
|
387
|
+
return yield* new InvalidExecutionPlanError({
|
|
388
|
+
message: `rollback material is too large for action ${context.action.id}`,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
const bytes = yield* machine.readFile({
|
|
392
|
+
path: actualReference,
|
|
393
|
+
maximumBytes,
|
|
394
|
+
});
|
|
395
|
+
const stored = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Array(StoredFileSchema)))(decoder.decode(bytes)).pipe(Effect.mapError((error) => new InvalidExecutionPlanError({
|
|
396
|
+
message: `invalid rollback material for action ${context.action.id}: ${String(error)}`,
|
|
397
|
+
})));
|
|
398
|
+
const allowed = new Set(expectedPaths.map((path) => path.absolute));
|
|
399
|
+
if (stored.length !== allowed.size
|
|
400
|
+
|| stored.some((entry) => !allowed.has(entry.path))
|
|
401
|
+
|| new Set(stored.map((entry) => entry.path)).size !== stored.length) {
|
|
402
|
+
return yield* new InvalidExecutionPlanError({
|
|
403
|
+
message: `rollback material targets do not match action ${context.action.id}`,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
const root = context.action.detail.kind === "mirror-directory"
|
|
407
|
+
|| (context.action.detail.kind === "remove-resource"
|
|
408
|
+
&& (context.resource.kind === "directory" || context.resource.kind === "skill"))
|
|
409
|
+
? yield* targetPath(context.action.detail.target)
|
|
410
|
+
: undefined;
|
|
411
|
+
const rootEntry = root === undefined
|
|
412
|
+
? undefined
|
|
413
|
+
: stored.find((entry) => entry.path === root.absolute);
|
|
414
|
+
if (root !== undefined && rootEntry !== undefined && storedState(rootEntry) !== "absent") {
|
|
415
|
+
yield* restoreStoredFile(rootEntry);
|
|
416
|
+
}
|
|
417
|
+
if (rootEntry === undefined
|
|
418
|
+
|| storedState(rootEntry) === "absent"
|
|
419
|
+
|| storedState(rootEntry) === "directory") {
|
|
420
|
+
for (const entry of restoreOrder(stored)) {
|
|
421
|
+
if (entry === rootEntry)
|
|
422
|
+
continue;
|
|
423
|
+
yield* restoreStoredFile(entry, root);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
if (root !== undefined && rootEntry !== undefined && storedState(rootEntry) === "absent") {
|
|
427
|
+
yield* machine.removeEmptyDirectory({ path: root }).pipe(Effect.catchTag("MachineFilesystemError", (error) => error.message.includes("ENOENT") ? Effect.void : Effect.fail(error)));
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
const targetPath = (target) => Effect.gen(function* () {
|
|
431
|
+
const machine = yield* MachineState;
|
|
432
|
+
return yield* machine.normalizePath({ path: target });
|
|
433
|
+
});
|
|
434
|
+
const prepareSchedule = (context, scheduleManager) => Effect.gen(function* () {
|
|
435
|
+
if (scheduleManager === undefined) {
|
|
436
|
+
return yield* new InvalidExecutionPlanError({
|
|
437
|
+
message: `schedule resource ${context.resource.id} requires ScheduleManager`,
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
const input = yield* scheduleInputFor(context);
|
|
441
|
+
const execute = (context.previousSchedule === undefined
|
|
442
|
+
? scheduleManager.install(input)
|
|
443
|
+
: scheduleManager.update(input)).pipe(Effect.asVoid);
|
|
444
|
+
const rollback = context.previousSchedule === undefined
|
|
445
|
+
? scheduleManager.remove(input).pipe(Effect.asVoid)
|
|
446
|
+
: scheduleManager.update({
|
|
447
|
+
...input,
|
|
448
|
+
schedule: context.previousSchedule,
|
|
449
|
+
}).pipe(Effect.asVoid);
|
|
450
|
+
return { execute, rollback };
|
|
451
|
+
});
|
|
452
|
+
const prepareWrite = (context, target, digest) => Effect.gen(function* () {
|
|
453
|
+
const path = yield* targetPath(target);
|
|
454
|
+
const rollback = yield* captureRollback(context, [path]);
|
|
455
|
+
const execute = Effect.gen(function* () {
|
|
456
|
+
const machine = yield* MachineState;
|
|
457
|
+
if (context.desired.kind !== "file") {
|
|
458
|
+
const content = yield* artifact(context.artifacts, digest);
|
|
459
|
+
yield* machine.atomicWrite({ path, content });
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
if (context.desired.symlinkTo !== undefined) {
|
|
463
|
+
const target = yield* machine.normalizePath({
|
|
464
|
+
path: context.desired.symlinkTo,
|
|
465
|
+
});
|
|
466
|
+
yield* machine.replaceSymlink({ path, target });
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
const content = yield* artifact(context.artifacts, digest);
|
|
470
|
+
yield* machine.atomicWrite({
|
|
471
|
+
path,
|
|
472
|
+
content,
|
|
473
|
+
mode: context.desired.executable ? 0o700 : 0o600,
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
return {
|
|
477
|
+
rollbackReference: rollback.reference,
|
|
478
|
+
execute,
|
|
479
|
+
rollback: rollback.restore,
|
|
480
|
+
};
|
|
481
|
+
});
|
|
482
|
+
const prepareConfig = (context, target, keys) => Effect.gen(function* () {
|
|
483
|
+
if (context.desired.kind !== "config") {
|
|
484
|
+
return yield* new InvalidExecutionPlanError({
|
|
485
|
+
message: `write-config action does not target a config resource: ${context.resource.id}`,
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
const config = context.desired;
|
|
489
|
+
const path = yield* targetPath(target);
|
|
490
|
+
const desiredBytes = yield* artifact(context.artifacts, config.digest);
|
|
491
|
+
const desired = yield* Effect.try({
|
|
492
|
+
try: () => parseConfigDocument(config.format, decoder.decode(desiredBytes)),
|
|
493
|
+
catch: (error) => new InvalidArtifactError({
|
|
494
|
+
digest: config.digest,
|
|
495
|
+
message: String(error),
|
|
496
|
+
}),
|
|
497
|
+
});
|
|
498
|
+
const currentBytes = yield* readIfPresent(path, context.limits.maximumFileBytes);
|
|
499
|
+
const current = currentBytes === undefined
|
|
500
|
+
? {}
|
|
501
|
+
: yield* Effect.try({
|
|
502
|
+
try: () => parseConfigDocument(config.format, decoder.decode(currentBytes)),
|
|
503
|
+
catch: (error) => new InvalidExecutionPlanError({
|
|
504
|
+
message: `cannot merge non-object config ${target}: ${String(error)}`,
|
|
505
|
+
}),
|
|
506
|
+
});
|
|
507
|
+
for (const key of keys) {
|
|
508
|
+
const value = getConfigPath(desired, key);
|
|
509
|
+
if (value !== undefined)
|
|
510
|
+
setConfigPath(current, key, value);
|
|
511
|
+
}
|
|
512
|
+
const content = encoder.encode(serializeConfigDocument(config.format, current));
|
|
513
|
+
const rollback = yield* captureRollback(context, [path]);
|
|
514
|
+
const execute = Effect.gen(function* () {
|
|
515
|
+
const machine = yield* MachineState;
|
|
516
|
+
yield* machine.atomicWrite({ path, content });
|
|
517
|
+
});
|
|
518
|
+
return {
|
|
519
|
+
rollbackReference: rollback.reference,
|
|
520
|
+
execute,
|
|
521
|
+
rollback: rollback.restore,
|
|
522
|
+
};
|
|
523
|
+
});
|
|
524
|
+
const prepareMirror = (context, target, adds, removes) => Effect.gen(function* () {
|
|
525
|
+
if (context.desired.kind !== "directory" && context.desired.kind !== "skill") {
|
|
526
|
+
return yield* new InvalidExecutionPlanError({
|
|
527
|
+
message: `mirror action does not target a directory resource: ${context.resource.id}`,
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
const root = yield* targetPath(target);
|
|
531
|
+
const allRelative = [...new Set([...adds, ...removes])];
|
|
532
|
+
const paths = yield* Effect.forEach(allRelative, (path) => normalizeRelative(root, path));
|
|
533
|
+
const byRelative = new Map(allRelative.map((path, index) => [path, paths[index]]));
|
|
534
|
+
const desiredByPath = new Map(context.desired.files.map((file) => [
|
|
535
|
+
file.path,
|
|
536
|
+
file,
|
|
537
|
+
]));
|
|
538
|
+
const contentByPath = new Map();
|
|
539
|
+
for (const relative of adds) {
|
|
540
|
+
const desiredFile = desiredByPath.get(relative);
|
|
541
|
+
if (desiredFile === undefined) {
|
|
542
|
+
return yield* new InvalidExecutionPlanError({
|
|
543
|
+
message: `mirror add is absent from desired content: ${relative}`,
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
contentByPath.set(relative, yield* artifact(context.artifacts, desiredFile.digest));
|
|
547
|
+
}
|
|
548
|
+
const rollback = yield* captureRollback(context, [root, ...paths], root);
|
|
549
|
+
const execute = Effect.gen(function* () {
|
|
550
|
+
const activeMachine = yield* MachineState;
|
|
551
|
+
yield* activeMachine.ensureDirectory({ path: root });
|
|
552
|
+
for (const relative of adds) {
|
|
553
|
+
yield* activeMachine.mutateWithinRoot({
|
|
554
|
+
root,
|
|
555
|
+
path: byRelative.get(relative),
|
|
556
|
+
mutation: {
|
|
557
|
+
kind: "write",
|
|
558
|
+
content: contentByPath.get(relative),
|
|
559
|
+
mode: desiredByPath.get(relative).executable ? 0o700 : 0o600,
|
|
560
|
+
},
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
for (const relative of removes) {
|
|
564
|
+
yield* activeMachine.mutateWithinRoot({
|
|
565
|
+
root,
|
|
566
|
+
path: byRelative.get(relative),
|
|
567
|
+
mutation: { kind: "remove" },
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
return {
|
|
572
|
+
rollbackReference: rollback.reference,
|
|
573
|
+
execute,
|
|
574
|
+
rollback: rollback.restore,
|
|
575
|
+
};
|
|
576
|
+
});
|
|
577
|
+
const prepareRemoval = (context, detail, scheduleManager) => Effect.gen(function* () {
|
|
578
|
+
switch (context.resource.kind) {
|
|
579
|
+
case "file": {
|
|
580
|
+
const path = yield* targetPath(detail.target);
|
|
581
|
+
const rollback = yield* captureRollback(context, [path]);
|
|
582
|
+
const execute = Effect.gen(function* () {
|
|
583
|
+
const machine = yield* MachineState;
|
|
584
|
+
yield* machine.removeFile({ path }).pipe(Effect.catchTag("MachineFilesystemError", (error) => error.message.includes("ENOENT") ? Effect.void : Effect.fail(error)));
|
|
585
|
+
});
|
|
586
|
+
return { rollbackReference: rollback.reference, execute, rollback: rollback.restore };
|
|
587
|
+
}
|
|
588
|
+
case "config": {
|
|
589
|
+
if (context.resource.policy === "replace") {
|
|
590
|
+
const path = yield* targetPath(detail.target);
|
|
591
|
+
const rollback = yield* captureRollback(context, [path]);
|
|
592
|
+
const execute = Effect.gen(function* () {
|
|
593
|
+
const machine = yield* MachineState;
|
|
594
|
+
yield* machine.removeFile({ path }).pipe(Effect.catchTag("MachineFilesystemError", (error) => error.message.includes("ENOENT") ? Effect.void : Effect.fail(error)));
|
|
595
|
+
});
|
|
596
|
+
return {
|
|
597
|
+
rollbackReference: rollback.reference,
|
|
598
|
+
execute,
|
|
599
|
+
rollback: rollback.restore,
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
if (context.desired.kind !== "config") {
|
|
603
|
+
return yield* new InvalidExecutionPlanError({
|
|
604
|
+
message: `removal action does not target a config resource: ${context.resource.id}`,
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
const config = context.desired;
|
|
608
|
+
const path = yield* targetPath(detail.target);
|
|
609
|
+
const rollback = yield* captureRollback(context, [path]);
|
|
610
|
+
const execute = Effect.gen(function* () {
|
|
611
|
+
const currentBytes = yield* readIfPresent(path, context.limits.maximumFileBytes);
|
|
612
|
+
if (currentBytes === undefined)
|
|
613
|
+
return;
|
|
614
|
+
const current = yield* Effect.try({
|
|
615
|
+
try: () => parseConfigDocument(config.format, decoder.decode(currentBytes)),
|
|
616
|
+
catch: (error) => new InvalidExecutionPlanError({
|
|
617
|
+
message: `cannot remove keys from config ${detail.target}: ${String(error)}`,
|
|
618
|
+
}),
|
|
619
|
+
});
|
|
620
|
+
for (const key of detail.keys)
|
|
621
|
+
removeConfigPath(current, key);
|
|
622
|
+
const machine = yield* MachineState;
|
|
623
|
+
yield* machine.atomicWrite({
|
|
624
|
+
path,
|
|
625
|
+
content: encoder.encode(serializeConfigDocument(config.format, current)),
|
|
626
|
+
});
|
|
627
|
+
});
|
|
628
|
+
return { rollbackReference: rollback.reference, execute, rollback: rollback.restore };
|
|
629
|
+
}
|
|
630
|
+
case "directory":
|
|
631
|
+
case "skill": {
|
|
632
|
+
if (context.desired.kind !== "directory" && context.desired.kind !== "skill") {
|
|
633
|
+
return yield* new InvalidExecutionPlanError({
|
|
634
|
+
message: `removal action does not target a directory resource: ${context.resource.id}`,
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
const root = yield* targetPath(detail.target);
|
|
638
|
+
const paths = yield* Effect.forEach(detail.paths, (path) => normalizeRelative(root, path));
|
|
639
|
+
const rollback = yield* captureRollback(context, [root, ...paths], root);
|
|
640
|
+
const execute = Effect.gen(function* () {
|
|
641
|
+
const machine = yield* MachineState;
|
|
642
|
+
for (const path of paths) {
|
|
643
|
+
yield* machine.mutateWithinRoot({
|
|
644
|
+
root,
|
|
645
|
+
path,
|
|
646
|
+
mutation: { kind: "remove" },
|
|
647
|
+
}).pipe(Effect.catchTag("MachineFilesystemError", (error) => error.message.includes("ENOENT") ? Effect.void : Effect.fail(error)));
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
return { rollbackReference: rollback.reference, execute, rollback: rollback.restore };
|
|
651
|
+
}
|
|
652
|
+
case "schedule": {
|
|
653
|
+
if (scheduleManager === undefined
|
|
654
|
+
|| detail.schedule === undefined
|
|
655
|
+
|| context.desired.kind !== "schedule") {
|
|
656
|
+
return yield* new InvalidExecutionPlanError({
|
|
657
|
+
message: `removal action requires a schedule manager for ${context.resource.id}`,
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
const input = { schedule: detail.schedule };
|
|
661
|
+
return {
|
|
662
|
+
execute: scheduleManager.remove(input).pipe(Effect.asVoid),
|
|
663
|
+
rollback: scheduleManager.install(input).pipe(Effect.asVoid),
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
case "tool":
|
|
667
|
+
case "credential":
|
|
668
|
+
return yield* new InvalidExecutionPlanError({
|
|
669
|
+
message: `resource ${context.resource.id} does not support automatic removal`,
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
});
|
|
673
|
+
const installInvocation = (context, method, packageName, version, buildPolicy = { mode: "scripts-disabled" }, source, indexPolicy) => Effect.gen(function* () {
|
|
674
|
+
if (method === "source") {
|
|
675
|
+
return yield* new InvalidExecutionPlanError({
|
|
676
|
+
message: `source recipe ${packageName} requires Human Action Required; no bounded source installer is available`,
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
if (!Schema.is(AutomaticRecipeMethod)(method)) {
|
|
680
|
+
return yield* new InvalidExecutionPlanError({
|
|
681
|
+
message: `unknown installer method ${method}`,
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
if (buildPolicy.mode === "required") {
|
|
685
|
+
return yield* new InvalidExecutionPlanError({
|
|
686
|
+
message: `recipe ${method}/${packageName} requires a bounded build policy; the process executor cannot confine lifecycle descendants`,
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
if (method === "cargo"
|
|
690
|
+
&& buildPolicy.mode === "scripts-disabled") {
|
|
691
|
+
return yield* new InvalidExecutionPlanError({
|
|
692
|
+
message: `cargo recipe ${packageName} requires Human Action Required because Cargo has no disable-scripts mode`,
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
if (version !== undefined
|
|
696
|
+
&& ![
|
|
697
|
+
"npm",
|
|
698
|
+
"pnpm",
|
|
699
|
+
"bun",
|
|
700
|
+
"brew",
|
|
701
|
+
"homebrew",
|
|
702
|
+
"winget",
|
|
703
|
+
"uv",
|
|
704
|
+
"cargo",
|
|
705
|
+
"apt",
|
|
706
|
+
].includes(method)) {
|
|
707
|
+
return yield* new InvalidExecutionPlanError({
|
|
708
|
+
message: `installer ${method} cannot honor requested version ${version}`,
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
if (packageName === "--"
|
|
712
|
+
|| /^\s*-{1,2}\S*/u.test(packageName)
|
|
713
|
+
|| /\s/u.test(packageName)
|
|
714
|
+
|| (method === "npm"
|
|
715
|
+
? parseNpmPackageSpecification(packageName).kind !== "registry"
|
|
716
|
+
: isUnboundedNonNpmPackage(packageName))) {
|
|
717
|
+
return yield* new InvalidExecutionPlanError({
|
|
718
|
+
message: `ambiguous or source dependency ${packageName} requires a separately bounded execution plan`,
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
const recipeError = recipeValidationError({
|
|
722
|
+
method,
|
|
723
|
+
package: packageName,
|
|
724
|
+
version,
|
|
725
|
+
source,
|
|
726
|
+
indexPolicy,
|
|
727
|
+
});
|
|
728
|
+
if (recipeError !== undefined
|
|
729
|
+
|| isMissingAutomaticRecipeVersion({
|
|
730
|
+
method,
|
|
731
|
+
package: packageName,
|
|
732
|
+
version,
|
|
733
|
+
source,
|
|
734
|
+
})) {
|
|
735
|
+
return yield* new InvalidExecutionPlanError({
|
|
736
|
+
message: recipeError ?? `automatic installer ${method} requires an exact version`,
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
const pythonIndex = method === "uv"
|
|
740
|
+
? canonicalRecipeIndexUrl(indexPolicy?.url ?? defaultPythonIndex)
|
|
741
|
+
: undefined;
|
|
742
|
+
if (method === "uv" && pythonIndex === undefined) {
|
|
743
|
+
return yield* new InvalidExecutionPlanError({
|
|
744
|
+
message: `uv recipe ${packageName} has an invalid reviewed Python index policy`,
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
const npmFamily = method === "npm" || method === "pnpm" || method === "bun";
|
|
748
|
+
const sourceDetailsValue = recipeSourceDetails(source);
|
|
749
|
+
const effectiveVersion = version
|
|
750
|
+
?? (npmFamily && sourceDetailsValue.source !== undefined
|
|
751
|
+
? npmVersionFromTarballSource(packageName, sourceDetailsValue.source)
|
|
752
|
+
: undefined);
|
|
753
|
+
const sourceUrl = npmFamily
|
|
754
|
+
&& sourceDetailsValue.source !== undefined
|
|
755
|
+
&& sourceDetailsValue.source.startsWith("https://")
|
|
756
|
+
? sourceDetailsValue.source
|
|
757
|
+
: undefined;
|
|
758
|
+
const machine = yield* MachineState;
|
|
759
|
+
let verifiedArtifactPath;
|
|
760
|
+
if (sourceUrl !== undefined) {
|
|
761
|
+
if (method === "bun") {
|
|
762
|
+
return yield* new InvalidExecutionPlanError({
|
|
763
|
+
message: "bun cannot guarantee an offline local tarball installation; Human Action Required",
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
const integrity = sourceDetailsValue.integrity;
|
|
767
|
+
if (integrity === undefined) {
|
|
768
|
+
return yield* new InvalidExecutionPlanError({
|
|
769
|
+
message: `reviewed ${method} package artifact ${sourceUrl} has no supported integrity; Human Action Required`,
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
const directories = yield* machine.userDirectories();
|
|
773
|
+
const cacheDirectory = join(directories.cache.absolute, "canonfig", "npm-artifacts");
|
|
774
|
+
const artifact = yield* (context.npmArtifactTransport ?? defaultNpmArtifactTransport)
|
|
775
|
+
.download({
|
|
776
|
+
source: sourceUrl,
|
|
777
|
+
packageName,
|
|
778
|
+
version: effectiveVersion,
|
|
779
|
+
integrity,
|
|
780
|
+
cacheDirectory,
|
|
781
|
+
maximumBytes: 32 * 1024 * 1024,
|
|
782
|
+
timeoutMilliseconds: context.limits.processTimeoutMilliseconds,
|
|
783
|
+
}).pipe(Effect.mapError((error) => new InvalidExecutionPlanError({
|
|
784
|
+
message: `reviewed package artifact could not be verified: ${error.message}`,
|
|
785
|
+
})));
|
|
786
|
+
if (artifact.source !== sourceUrl || artifact.integrity !== integrity) {
|
|
787
|
+
return yield* new InvalidExecutionPlanError({
|
|
788
|
+
message: "verified npm artifact metadata changed before installation",
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
const artifactPath = yield* machine.normalizePath({ path: artifact.path });
|
|
792
|
+
yield* machine.validatePathWithinRoot({
|
|
793
|
+
root: directories.cache,
|
|
794
|
+
path: artifactPath,
|
|
795
|
+
});
|
|
796
|
+
const symlinkTarget = yield* machine.readSymlink(artifactPath).pipe(Effect.map((target) => target.absolute), Effect.catch(() => Effect.succeed(undefined)));
|
|
797
|
+
if (symlinkTarget !== undefined) {
|
|
798
|
+
return yield* new InvalidExecutionPlanError({
|
|
799
|
+
message: "verified npm artifact cache entry is a symlink",
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
if (!Number.isSafeInteger(artifact.bytes)
|
|
803
|
+
|| artifact.bytes <= 0
|
|
804
|
+
|| artifact.bytes > 32 * 1024 * 1024) {
|
|
805
|
+
return yield* new InvalidExecutionPlanError({
|
|
806
|
+
message: "verified npm artifact size is outside the execution bound",
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
const bytes = yield* machine.readFile({
|
|
810
|
+
path: artifactPath,
|
|
811
|
+
maximumBytes: artifact.bytes,
|
|
812
|
+
});
|
|
813
|
+
if (bytes.byteLength !== artifact.bytes || !verifyNpmArtifactBytes(bytes, integrity)) {
|
|
814
|
+
return yield* new InvalidExecutionPlanError({
|
|
815
|
+
message: "verified npm artifact changed or is corrupt before installation",
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
const provenanceError = validateNpmArtifactProvenance(bytes, packageName, effectiveVersion);
|
|
819
|
+
if (provenanceError !== undefined) {
|
|
820
|
+
return yield* new InvalidExecutionPlanError({
|
|
821
|
+
message: `verified npm artifact provenance is not safe: ${provenanceError}; Human Action Required`,
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
verifiedArtifactPath = artifactPath.absolute;
|
|
825
|
+
}
|
|
826
|
+
const executableName = method === "apt"
|
|
827
|
+
? "apt-get"
|
|
828
|
+
: method === "homebrew"
|
|
829
|
+
? "brew"
|
|
830
|
+
: method;
|
|
831
|
+
const executable = yield* machine.findExecutable({ name: executableName });
|
|
832
|
+
const packageSpecifier = npmFamily && verifiedArtifactPath !== undefined
|
|
833
|
+
? verifiedArtifactPath
|
|
834
|
+
: effectiveVersion === undefined
|
|
835
|
+
? packageName
|
|
836
|
+
: `${packageName}@${effectiveVersion}`;
|
|
837
|
+
const packageEnvironment = method === "uv"
|
|
838
|
+
? [
|
|
839
|
+
{ name: "UV_CONFIG_FILE", value: process.platform === "win32" ? "NUL" : "/dev/null" },
|
|
840
|
+
{ name: "PIP_CONFIG_FILE", value: process.platform === "win32" ? "NUL" : "/dev/null" },
|
|
841
|
+
{ name: "UV_DEFAULT_INDEX", value: pythonIndex },
|
|
842
|
+
{ name: "UV_INDEX_URL", value: pythonIndex },
|
|
843
|
+
{ name: "PIP_INDEX_URL", value: pythonIndex },
|
|
844
|
+
]
|
|
845
|
+
: method === "npm" || method === "pnpm" || method === "bun"
|
|
846
|
+
? [
|
|
847
|
+
{ name: "NPM_CONFIG_USERCONFIG", value: process.platform === "win32" ? "NUL" : "/dev/null" },
|
|
848
|
+
{ name: "NPM_CONFIG_GLOBALCONFIG", value: process.platform === "win32" ? "NUL" : "/dev/null" },
|
|
849
|
+
{ name: "NPM_CONFIG_LOCATION", value: "global" },
|
|
850
|
+
{ name: "NPM_CONFIG_REGISTRY", value: "https://registry.npmjs.org/" },
|
|
851
|
+
...(verifiedArtifactPath !== undefined
|
|
852
|
+
? [{ name: "NPM_CONFIG_OFFLINE", value: "true" }]
|
|
853
|
+
: []),
|
|
854
|
+
...(method === "pnpm"
|
|
855
|
+
? [
|
|
856
|
+
{ name: "PNPM_CONFIG_REGISTRY", value: "https://registry.npmjs.org/" },
|
|
857
|
+
...(verifiedArtifactPath !== undefined
|
|
858
|
+
? [{ name: "PNPM_CONFIG_OFFLINE", value: "true" }]
|
|
859
|
+
: []),
|
|
860
|
+
]
|
|
861
|
+
: []),
|
|
862
|
+
...(method === "bun"
|
|
863
|
+
? [
|
|
864
|
+
{ name: "BUN_CONFIG_FILE", value: process.platform === "win32" ? "NUL" : "/dev/null" },
|
|
865
|
+
{ name: "BUN_CONFIG_REGISTRY", value: "https://registry.npmjs.org/" },
|
|
866
|
+
]
|
|
867
|
+
: []),
|
|
868
|
+
]
|
|
869
|
+
: undefined;
|
|
870
|
+
const arguments_ = method === "npm"
|
|
871
|
+
? [
|
|
872
|
+
"install",
|
|
873
|
+
"--global",
|
|
874
|
+
packageSpecifier,
|
|
875
|
+
...(buildPolicy.mode === "scripts-disabled" ? ["--ignore-scripts"] : []),
|
|
876
|
+
...(verifiedArtifactPath !== undefined ? ["--offline"] : []),
|
|
877
|
+
]
|
|
878
|
+
: method === "pnpm" || method === "bun"
|
|
879
|
+
? [
|
|
880
|
+
"add",
|
|
881
|
+
"--global",
|
|
882
|
+
packageSpecifier,
|
|
883
|
+
...(buildPolicy.mode === "scripts-disabled" ? ["--ignore-scripts"] : []),
|
|
884
|
+
...(verifiedArtifactPath !== undefined ? ["--offline"] : []),
|
|
885
|
+
]
|
|
886
|
+
: method === "brew" || method === "homebrew"
|
|
887
|
+
? ["install", version === undefined ? packageName : `${packageName}@${version}`]
|
|
888
|
+
: method === "winget"
|
|
889
|
+
? version === undefined
|
|
890
|
+
? ["install", "--id", packageName, "--silent"]
|
|
891
|
+
: ["install", "--id", packageName, "--version", version, "--exact", "--silent"]
|
|
892
|
+
: method === "uv"
|
|
893
|
+
? [
|
|
894
|
+
"tool",
|
|
895
|
+
"install",
|
|
896
|
+
version === undefined ? packageName : `${packageName}==${version}`,
|
|
897
|
+
...(buildPolicy.mode === "scripts-disabled" ? ["--only-binary=:all:"] : []),
|
|
898
|
+
"--no-config",
|
|
899
|
+
`--default-index=${pythonIndex}`,
|
|
900
|
+
]
|
|
901
|
+
: method === "apt"
|
|
902
|
+
? ["install", "-y", version === undefined ? packageName : `${packageName}=${version}`]
|
|
903
|
+
: method === "cargo" && version !== undefined
|
|
904
|
+
? ["install", packageName, "--version", version, "--locked"]
|
|
905
|
+
: ["install", packageName];
|
|
906
|
+
const result = yield* machine.runProcess({
|
|
907
|
+
executable: executable.path,
|
|
908
|
+
arguments: arguments_,
|
|
909
|
+
timeoutMilliseconds: context.limits.processTimeoutMilliseconds,
|
|
910
|
+
maximumOutputBytes: context.limits.maximumProcessOutputBytes,
|
|
911
|
+
environment: packageEnvironment,
|
|
912
|
+
environmentUnset: method === "uv"
|
|
913
|
+
? [
|
|
914
|
+
"HTTP_PROXY",
|
|
915
|
+
"HTTPS_PROXY",
|
|
916
|
+
"FTP_PROXY",
|
|
917
|
+
"ALL_PROXY",
|
|
918
|
+
"NO_PROXY",
|
|
919
|
+
"NETRC",
|
|
920
|
+
"CURL_CA_BUNDLE",
|
|
921
|
+
"REQUESTS_CA_BUNDLE",
|
|
922
|
+
"SSL_CERT_FILE",
|
|
923
|
+
"SSL_CERT_DIR",
|
|
924
|
+
"PYTHONHTTPSVERIFY",
|
|
925
|
+
"PYTHON_KEYRING_BACKEND",
|
|
926
|
+
"KEYRING_BACKEND",
|
|
927
|
+
]
|
|
928
|
+
: undefined,
|
|
929
|
+
environmentUnsetPrefixes: method === "uv"
|
|
930
|
+
? ["UV_", "PIP_"]
|
|
931
|
+
: npmFamily
|
|
932
|
+
? ["NPM_CONFIG_", "PNPM_CONFIG_", "BUN_CONFIG_"]
|
|
933
|
+
: undefined,
|
|
934
|
+
});
|
|
935
|
+
if (result.exitCode !== 0) {
|
|
936
|
+
return yield* new ActionExecutionError({
|
|
937
|
+
action: context.action.id,
|
|
938
|
+
message: `installer ${method} exited with ${String(result.exitCode)}`,
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
});
|
|
942
|
+
/** Prepare deterministic work. Preparation stores rollback material before owned-file mutation. */
|
|
943
|
+
export const prepareResourceAction = (context, scheduleManager) => {
|
|
944
|
+
const detail = context.action.detail;
|
|
945
|
+
switch (detail.kind) {
|
|
946
|
+
case "write-file":
|
|
947
|
+
if (context.resource.kind === "schedule") {
|
|
948
|
+
return prepareSchedule(context, scheduleManager);
|
|
949
|
+
}
|
|
950
|
+
return prepareWrite(context, detail.target, detail.digest);
|
|
951
|
+
case "write-config":
|
|
952
|
+
return prepareConfig(context, detail.target, detail.keys);
|
|
953
|
+
case "mirror-directory":
|
|
954
|
+
return prepareMirror(context, detail.target, detail.adds, detail.removes);
|
|
955
|
+
case "remove-resource":
|
|
956
|
+
return prepareRemoval(context, detail, scheduleManager);
|
|
957
|
+
case "install-tool":
|
|
958
|
+
return Effect.succeed({
|
|
959
|
+
execute: installInvocation(context, detail.method, detail.package, detail.version, detail.buildPolicy, detail.source, detail.indexPolicy),
|
|
960
|
+
});
|
|
961
|
+
case "transfer-blob":
|
|
962
|
+
return artifact(context.artifacts, detail.blob).pipe(Effect.flatMap((content) => content.byteLength === detail.bytes
|
|
963
|
+
? Effect.succeed({ execute: Effect.void })
|
|
964
|
+
: Effect.fail(new InvalidArtifactError({
|
|
965
|
+
digest: detail.blob,
|
|
966
|
+
message: `artifact size was ${content.byteLength}, expected ${detail.bytes}`,
|
|
967
|
+
}))));
|
|
968
|
+
case "no-op":
|
|
969
|
+
case "verify-only":
|
|
970
|
+
return Effect.succeed({ execute: Effect.void });
|
|
971
|
+
case "schedule-default":
|
|
972
|
+
case "human-action":
|
|
973
|
+
case "agent-task":
|
|
974
|
+
case "drift-conflict":
|
|
975
|
+
return Effect.fail(new InvalidExecutionPlanError({
|
|
976
|
+
message: `${detail.kind} is an outcome action, not executable work`,
|
|
977
|
+
}));
|
|
978
|
+
}
|
|
979
|
+
};
|
|
980
|
+
const verifyDigest = (target, desiredDigest) => Effect.gen(function* () {
|
|
981
|
+
const machine = yield* MachineState;
|
|
982
|
+
const path = yield* machine.normalizePath({ path: target });
|
|
983
|
+
const kind = yield* machine.inspectPath(path);
|
|
984
|
+
if (kind.kind !== "regular") {
|
|
985
|
+
return {
|
|
986
|
+
passed: false,
|
|
987
|
+
method: `sha256:non-${kind.kind}`,
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
const observed = yield* machine.digestFile({ path });
|
|
991
|
+
return {
|
|
992
|
+
passed: observed.value === desiredDigest,
|
|
993
|
+
method: "sha256",
|
|
994
|
+
observedDigest: observed.value,
|
|
995
|
+
};
|
|
996
|
+
});
|
|
997
|
+
const verifySchedule = (context, scheduleManager) => Effect.gen(function* () {
|
|
998
|
+
if (scheduleManager === undefined || context.desired.kind !== "schedule") {
|
|
999
|
+
return yield* new InvalidExecutionPlanError({
|
|
1000
|
+
message: `schedule resource ${context.resource.id} requires ScheduleManager verification`,
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
const input = yield* scheduleInputFor(context);
|
|
1004
|
+
const status = yield* scheduleManager.status(input);
|
|
1005
|
+
return {
|
|
1006
|
+
passed: status.state === "current",
|
|
1007
|
+
method: `native-scheduler:${status.platform}`,
|
|
1008
|
+
};
|
|
1009
|
+
});
|
|
1010
|
+
/** Observe required postconditions independently from action execution. */
|
|
1011
|
+
export const verifyResource = (context, scheduleManager) => {
|
|
1012
|
+
const desired = context.desired;
|
|
1013
|
+
const verification = context.verification;
|
|
1014
|
+
if (desired.kind === "schedule") {
|
|
1015
|
+
return verifySchedule(context, scheduleManager);
|
|
1016
|
+
}
|
|
1017
|
+
if (verification.method === "command") {
|
|
1018
|
+
return verifyCommand(context, verification.command, verification.expectContains);
|
|
1019
|
+
}
|
|
1020
|
+
if (verification.method === "symlink") {
|
|
1021
|
+
return verifySymlink(context, verification.target);
|
|
1022
|
+
}
|
|
1023
|
+
if (verification.method === "executable-present") {
|
|
1024
|
+
return verifyExecutable(context, verification.executable);
|
|
1025
|
+
}
|
|
1026
|
+
if (verification.method === "credential-present") {
|
|
1027
|
+
return verifyCredential(context, verification.reference);
|
|
1028
|
+
}
|
|
1029
|
+
const declaredDigest = verification.digest;
|
|
1030
|
+
switch (desired.kind) {
|
|
1031
|
+
case "file":
|
|
1032
|
+
return Effect.gen(function* () {
|
|
1033
|
+
const digest = yield* verifyDigest(context.resource.target, declaredDigest);
|
|
1034
|
+
if (!digest.passed)
|
|
1035
|
+
return digest;
|
|
1036
|
+
const machine = yield* MachineState;
|
|
1037
|
+
const path = yield* machine.normalizePath({
|
|
1038
|
+
path: context.resource.target,
|
|
1039
|
+
});
|
|
1040
|
+
const permissions = yield* machine.permissions(path);
|
|
1041
|
+
const kind = yield* machine.inspectPath(path);
|
|
1042
|
+
if (kind.kind !== "regular") {
|
|
1043
|
+
return {
|
|
1044
|
+
...digest,
|
|
1045
|
+
passed: false,
|
|
1046
|
+
method: `${digest.method}+non-${kind.kind}`,
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
return {
|
|
1050
|
+
...digest,
|
|
1051
|
+
passed: permissions.executableByOwner === desired.executable,
|
|
1052
|
+
method: `${digest.method}+permissions`,
|
|
1053
|
+
};
|
|
1054
|
+
});
|
|
1055
|
+
case "skill":
|
|
1056
|
+
case "directory":
|
|
1057
|
+
return desiredResourceDigest(desired) === declaredDigest
|
|
1058
|
+
? verifyDirectory(context, desired.files)
|
|
1059
|
+
: Effect.succeed({
|
|
1060
|
+
passed: false,
|
|
1061
|
+
method: "declared-directory-digest",
|
|
1062
|
+
});
|
|
1063
|
+
case "config":
|
|
1064
|
+
return desired.digest === declaredDigest
|
|
1065
|
+
? verifyConfig(context, desired.digest, desired.keys)
|
|
1066
|
+
: Effect.succeed({
|
|
1067
|
+
passed: false,
|
|
1068
|
+
method: "declared-config-digest",
|
|
1069
|
+
});
|
|
1070
|
+
case "tool":
|
|
1071
|
+
case "credential":
|
|
1072
|
+
return Effect.fail(new InvalidExecutionPlanError({
|
|
1073
|
+
message: `resource ${context.resource.id} has incompatible digest verification`,
|
|
1074
|
+
}));
|
|
1075
|
+
}
|
|
1076
|
+
};
|
|
1077
|
+
const verifyCommand = (context, command, expectContains) => Effect.gen(function* () {
|
|
1078
|
+
const [name, ...arguments_] = command;
|
|
1079
|
+
if (name === undefined) {
|
|
1080
|
+
return yield* new InvalidExecutionPlanError({
|
|
1081
|
+
message: `resource ${context.resource.id} has an empty verification command`,
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
const machine = yield* MachineState;
|
|
1085
|
+
const executable = isAbsolute(name) || win32.isAbsolute(name)
|
|
1086
|
+
? {
|
|
1087
|
+
name,
|
|
1088
|
+
path: yield* machine.normalizePath({ path: name }),
|
|
1089
|
+
}
|
|
1090
|
+
: yield* machine.findExecutable({ name });
|
|
1091
|
+
const result = yield* machine.runProcess({
|
|
1092
|
+
executable: executable.path,
|
|
1093
|
+
arguments: arguments_,
|
|
1094
|
+
timeoutMilliseconds: context.limits.processTimeoutMilliseconds,
|
|
1095
|
+
maximumOutputBytes: context.limits.maximumProcessOutputBytes,
|
|
1096
|
+
});
|
|
1097
|
+
const output = `${decoder.decode(result.standardOutput)}${decoder.decode(result.standardError)}`;
|
|
1098
|
+
return {
|
|
1099
|
+
passed: result.exitCode === 0
|
|
1100
|
+
&& (expectContains === undefined || output.includes(expectContains)),
|
|
1101
|
+
method: `command:${name}`,
|
|
1102
|
+
exitCode: result.exitCode ?? undefined,
|
|
1103
|
+
};
|
|
1104
|
+
});
|
|
1105
|
+
const verifyExecutable = (context, executable) => Effect.gen(function* () {
|
|
1106
|
+
const machine = yield* MachineState;
|
|
1107
|
+
return yield* machine.findExecutable({ name: executable }).pipe(Effect.as({ passed: true, method: `executable:${executable}` }), Effect.catch(() => Effect.succeed({ passed: false, method: `executable:${executable}` })));
|
|
1108
|
+
});
|
|
1109
|
+
const verifyCredential = (context, referenceValue) => Effect.gen(function* () {
|
|
1110
|
+
const reference = yield* Schema.decodeUnknownEffect(CredentialReference)(referenceValue).pipe(Effect.mapError((error) => new InvalidExecutionPlanError({ message: String(error) })));
|
|
1111
|
+
const machine = yield* MachineState;
|
|
1112
|
+
return yield* machine.loadCredential({ reference }).pipe(Effect.as({ passed: true, method: `credential:${referenceValue}` }), Effect.catch(() => Effect.succeed({ passed: false, method: `credential:${referenceValue}` })));
|
|
1113
|
+
});
|
|
1114
|
+
const verifySymlink = (context, target) => Effect.gen(function* () {
|
|
1115
|
+
const machine = yield* MachineState;
|
|
1116
|
+
const path = yield* machine.normalizePath({ path: context.resource.target });
|
|
1117
|
+
const expected = yield* machine.normalizePath({ path: target });
|
|
1118
|
+
return yield* machine.readSymlink(path).pipe(Effect.map((observed) => ({
|
|
1119
|
+
passed: observed.absolute === expected.absolute,
|
|
1120
|
+
method: "symlink-target",
|
|
1121
|
+
})), Effect.catch(() => Effect.succeed({ passed: false, method: "symlink-target" })));
|
|
1122
|
+
});
|
|
1123
|
+
const verifyDirectory = (context, files) => Effect.gen(function* () {
|
|
1124
|
+
const root = yield* targetPath(context.resource.target);
|
|
1125
|
+
const machine = yield* MachineState;
|
|
1126
|
+
const rootKind = yield* machine.inspectPath(root).pipe(Effect.catchTag("MachineFilesystemError", (error) => error.message.includes("ENOENT")
|
|
1127
|
+
? Effect.succeed(undefined)
|
|
1128
|
+
: Effect.fail(error)));
|
|
1129
|
+
if (rootKind === undefined) {
|
|
1130
|
+
return { passed: false, method: "directory-root-missing" };
|
|
1131
|
+
}
|
|
1132
|
+
if (rootKind.kind !== "directory") {
|
|
1133
|
+
return {
|
|
1134
|
+
passed: false,
|
|
1135
|
+
method: `directory-root-non-${rootKind.kind}`,
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
const observations = yield* Effect.forEach(files, (file) => Effect.gen(function* () {
|
|
1139
|
+
const path = yield* normalizeRelative(root, file.path);
|
|
1140
|
+
const kind = yield* machine.inspectPath(path);
|
|
1141
|
+
if (kind.kind !== "regular") {
|
|
1142
|
+
return {
|
|
1143
|
+
expected: file.digest,
|
|
1144
|
+
observed: undefined,
|
|
1145
|
+
executable: false,
|
|
1146
|
+
expectedExecutable: "executable" in file && file.executable === true,
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
const observed = yield* machine.digestFile({ path });
|
|
1150
|
+
const permissions = yield* machine.permissions(path);
|
|
1151
|
+
const finalKind = yield* machine.inspectPath(path);
|
|
1152
|
+
return {
|
|
1153
|
+
expected: file.digest,
|
|
1154
|
+
observed: finalKind.kind === "regular" ? observed.value : undefined,
|
|
1155
|
+
executable: permissions.executableByOwner,
|
|
1156
|
+
expectedExecutable: "executable" in file && file.executable === true,
|
|
1157
|
+
};
|
|
1158
|
+
}), {
|
|
1159
|
+
concurrency: context.limits.verificationConcurrency,
|
|
1160
|
+
});
|
|
1161
|
+
const mismatch = observations.find((observation) => observation.observed !== observation.expected
|
|
1162
|
+
|| observation.executable !== observation.expectedExecutable);
|
|
1163
|
+
if (mismatch !== undefined) {
|
|
1164
|
+
return {
|
|
1165
|
+
passed: false,
|
|
1166
|
+
method: "directory-sha256",
|
|
1167
|
+
observedDigest: mismatch.observed,
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
return { passed: true, method: "directory-sha256" };
|
|
1171
|
+
});
|
|
1172
|
+
const verifyConfig = (context, digest, keys) => Effect.gen(function* () {
|
|
1173
|
+
const desiredBytes = yield* artifact(context.artifacts, digest);
|
|
1174
|
+
if (context.desired.kind !== "config") {
|
|
1175
|
+
return yield* new InvalidExecutionPlanError({
|
|
1176
|
+
message: `config verification targets non-config ${context.resource.id}`,
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
const desired = yield* Effect.try({
|
|
1180
|
+
try: () => parseConfigDocument(context.desired.kind === "config" ? context.desired.format : "json", decoder.decode(desiredBytes)),
|
|
1181
|
+
catch: (error) => new InvalidArtifactError({ digest, message: String(error) }),
|
|
1182
|
+
});
|
|
1183
|
+
const machine = yield* MachineState;
|
|
1184
|
+
const path = yield* machine.normalizePath({ path: context.resource.target });
|
|
1185
|
+
const observedBytes = yield* machine.readFile({
|
|
1186
|
+
path,
|
|
1187
|
+
maximumBytes: context.limits.maximumFileBytes,
|
|
1188
|
+
});
|
|
1189
|
+
const observed = yield* Effect.try({
|
|
1190
|
+
try: () => parseConfigDocument(context.desired.kind === "config" ? context.desired.format : "json", decoder.decode(observedBytes)),
|
|
1191
|
+
catch: (error) => new InvalidExecutionPlanError({
|
|
1192
|
+
message: `cannot verify non-object config ${context.resource.target}: ${String(error)}`,
|
|
1193
|
+
}),
|
|
1194
|
+
});
|
|
1195
|
+
const passed = keys.every((key) => JSON.stringify(getConfigPath(observed, key))
|
|
1196
|
+
=== JSON.stringify(getConfigPath(desired, key)));
|
|
1197
|
+
return { passed, method: "config-keys" };
|
|
1198
|
+
});
|