@nowcrew/daemon 0.6.16 → 0.6.18
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/dist/agent-ability/runtime-context.js +7 -1
- package/dist/atomic-private-write.js +54 -1
- package/dist/automatic-install-target.js +40 -11
- package/dist/console.js +9 -0
- package/dist/control-plane-url.js +2 -2
- package/dist/daemon-migration-controller.js +198 -0
- package/dist/daemon-migration-wiring.js +22 -0
- package/dist/daemon-update-eligibility.js +1 -1
- package/dist/directory-projection-identity.js +32 -0
- package/dist/directory-projection.js +922 -0
- package/dist/execution-protocol.js +78 -11
- package/dist/execution-runner.js +50 -2
- package/dist/i18n.js +1 -0
- package/dist/local-execution-prompt.js +57 -0
- package/dist/local-executor.js +99 -40
- package/dist/machine-info.js +45 -9
- package/dist/main.js +0 -0
- package/dist/normalize.js +5 -0
- package/dist/profile-layout.js +41 -0
- package/dist/project-skills/controller.js +74 -14
- package/dist/project-skills/execution-adapter.js +11 -0
- package/dist/project-skills/initialized-reconciler.js +20 -0
- package/dist/project-skills/projection-set-switch.js +419 -0
- package/dist/project-skills/projection-state-domain.js +153 -0
- package/dist/project-skills/projection-state-store.js +841 -0
- package/dist/project-skills/projection-state-transaction.js +318 -0
- package/dist/project-skills/projection-state.js +3 -0
- package/dist/project-skills/reconciler.js +299 -68
- package/dist/project-skills/runtime-warning.js +6 -0
- package/dist/project-skills/scanner.js +30 -1
- package/dist/project-skills/types.js +9 -0
- package/dist/project-workspaces/resolver.js +179 -0
- package/dist/project-workspaces/types.js +1 -0
- package/dist/prompt.js +40 -0
- package/dist/runtimes/claude.js +235 -4
- package/dist/runtimes/codex-app-server-runner.js +100 -25
- package/dist/runtimes/codex-contract.js +123 -0
- package/dist/runtimes/codex.js +2 -0
- package/dist/serve.js +31 -17
- package/dist/session.js +3 -0
- package/dist/supervised-runtime.js +12 -4
- package/dist/workspace.js +14 -5
- package/package.json +10 -9
|
@@ -0,0 +1,841 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { chmod, link, lstat, mkdir, open, opendir, rename, rmdir, unlink } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { durableDirectorySync } from "../atomic-private-write.js";
|
|
6
|
+
import { parseAppliedProjectSkillManifest, ProjectSkillProjectionStateError, } from "./projection-state-domain.js";
|
|
7
|
+
const MANIFEST_FILE_NAME = "project-skills-v2.json";
|
|
8
|
+
const MAX_MANIFEST_BYTES = 256 * 1024;
|
|
9
|
+
const MAX_CONSISTENT_READ_ATTEMPTS = 6;
|
|
10
|
+
const MAX_STATE_DIRECTORY_ENTRIES = 1_024;
|
|
11
|
+
const MAX_OPERATION_ENTRIES = 4;
|
|
12
|
+
const MAX_INTENT_BYTES = (MAX_MANIFEST_BYTES * 2) + (16 * 1024);
|
|
13
|
+
const DIGEST = /^sha256:[a-f0-9]{64}$/u;
|
|
14
|
+
const ARTIFACT_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
|
|
15
|
+
const OPERATION_MANAGED_BY = "nowcrew-project-skills-v2-operation";
|
|
16
|
+
const OPERATION_PREFIX = ".project-skills-v2.";
|
|
17
|
+
const OPERATION_SUFFIX = ".op";
|
|
18
|
+
const PREPARING_PREFIX = ".project-skills-v2.preparing-";
|
|
19
|
+
const DISCARD_PREFIX = ".project-skills-v2.discard-";
|
|
20
|
+
const STAGE_BASENAME = "stage.json";
|
|
21
|
+
const PREVIOUS_BASENAME = "previous.json";
|
|
22
|
+
const PUBLISHED_BASENAME = "published.json";
|
|
23
|
+
const INTENT_BASENAME = "intent.json";
|
|
24
|
+
/** Private WAL tombstone: it is never a valid public applied-manifest shape. */
|
|
25
|
+
const ABSENCE_MARKER_RAW = `${JSON.stringify({
|
|
26
|
+
managedBy: "nowcrew-project-skills-v2-absence", version: 1,
|
|
27
|
+
})}\n`;
|
|
28
|
+
const corrupt = () => { throw new ProjectSkillProjectionStateError("skill_projection_state_corrupt"); };
|
|
29
|
+
const pathUnmanaged = () => {
|
|
30
|
+
throw new ProjectSkillProjectionStateError("skill_projection_state_path_unmanaged");
|
|
31
|
+
};
|
|
32
|
+
const unmanaged = () => { throw new ProjectSkillProjectionStateError("skill_projection_state_unmanaged"); };
|
|
33
|
+
export const appliedProjectSkillManifestPath = (agentRoot) => join(agentRoot, ".crew", MANIFEST_FILE_NAME);
|
|
34
|
+
const isMissing = (error) => error.code === "ENOENT";
|
|
35
|
+
const ownedByCurrentUser = (uid) => process.getuid === undefined || uid === BigInt(process.getuid());
|
|
36
|
+
/**
|
|
37
|
+
* Node/libuv cannot open Windows directories with a handle suitable for FlushFileBuffers. Project
|
|
38
|
+
* Skill publication therefore keeps file fsync and ordering there, but directory durability is
|
|
39
|
+
* explicitly best-effort until the v2 capability passes native Windows smoke validation.
|
|
40
|
+
*/
|
|
41
|
+
const projectionDirectorySync = (options) => options.platform === "win32" || (options.platform === undefined && process.platform === "win32")
|
|
42
|
+
? async () => undefined
|
|
43
|
+
: options.directorySync ?? durableDirectorySync;
|
|
44
|
+
const SerializedFileIdentitySchema = z.object({
|
|
45
|
+
dev: z.string().regex(/^\d+$/u).max(40), ino: z.string().regex(/^\d+$/u).max(40),
|
|
46
|
+
uid: z.string().regex(/^\d+$/u).max(40), size: z.string().regex(/^\d+$/u).max(40),
|
|
47
|
+
mtimeNs: z.string().regex(/^\d+$/u).max(40), ctimeNs: z.string().regex(/^\d+$/u).max(40),
|
|
48
|
+
}).strict();
|
|
49
|
+
const StageBindingSchema = z.object({
|
|
50
|
+
basename: z.literal(STAGE_BASENAME), content: z.string(), digest: z.string().regex(DIGEST),
|
|
51
|
+
identity: SerializedFileIdentitySchema,
|
|
52
|
+
}).strict();
|
|
53
|
+
const PreviousBindingSchema = z.object({
|
|
54
|
+
basename: z.literal(PREVIOUS_BASENAME), content: z.string(), digest: z.string().regex(DIGEST),
|
|
55
|
+
identity: SerializedFileIdentitySchema,
|
|
56
|
+
}).strict();
|
|
57
|
+
const OperationIntentSchema = z.object({
|
|
58
|
+
managedBy: z.literal(OPERATION_MANAGED_BY), version: z.literal(2),
|
|
59
|
+
nonce: z.string().regex(ARTIFACT_ID), finalBasename: z.literal(MANIFEST_FILE_NAME),
|
|
60
|
+
stage: StageBindingSchema, previous: PreviousBindingSchema.nullable(),
|
|
61
|
+
}).strict();
|
|
62
|
+
const fileIdentity = (info) => Object.freeze({
|
|
63
|
+
dev: info.dev, ino: info.ino, uid: info.uid, size: info.size,
|
|
64
|
+
mtimeNs: info.mtimeNs, ctimeNs: info.ctimeNs,
|
|
65
|
+
});
|
|
66
|
+
const serializeIdentity = (identity) => ({
|
|
67
|
+
dev: identity.dev.toString(), ino: identity.ino.toString(), uid: identity.uid.toString(),
|
|
68
|
+
size: identity.size.toString(), mtimeNs: identity.mtimeNs.toString(),
|
|
69
|
+
ctimeNs: identity.ctimeNs.toString(),
|
|
70
|
+
});
|
|
71
|
+
const sameFileVersion = (left, right) => left.dev === right.dev
|
|
72
|
+
&& left.ino === right.ino
|
|
73
|
+
&& left.uid === right.uid
|
|
74
|
+
&& left.size === right.size
|
|
75
|
+
&& left.mtimeNs === right.mtimeNs
|
|
76
|
+
&& left.ctimeNs === right.ctimeNs;
|
|
77
|
+
const sameSnapshot = (left, right) => left.raw === right.raw && sameFileVersion(left.identity, right.identity);
|
|
78
|
+
const digestRaw = (raw) => `sha256:${createHash("sha256").update(raw, "utf8").digest("hex")}`;
|
|
79
|
+
const isExisting = (error) => error.code === "EEXIST";
|
|
80
|
+
const realOwnedDirectory = async (path, absentIsAllowed) => {
|
|
81
|
+
try {
|
|
82
|
+
const info = await lstat(path, { bigint: true });
|
|
83
|
+
if (!info.isDirectory() || info.isSymbolicLink() || !ownedByCurrentUser(info.uid))
|
|
84
|
+
return pathUnmanaged();
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (absentIsAllowed && isMissing(error))
|
|
89
|
+
return false;
|
|
90
|
+
return pathUnmanaged();
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* A real mode-0700 operation directory is the ownership boundary for internal cleanup. We fence
|
|
95
|
+
* exact bytes and bigint versions before unlinking within it. Same-UID code deliberately bypassing
|
|
96
|
+
* that private boundary is outside the supported (otherwise unbounded) TOCTOU threat model.
|
|
97
|
+
*/
|
|
98
|
+
const validatePrivateOperationDirectory = async (path) => {
|
|
99
|
+
try {
|
|
100
|
+
const info = await lstat(path, { bigint: true });
|
|
101
|
+
if (!info.isDirectory() || info.isSymbolicLink() || !ownedByCurrentUser(info.uid)) {
|
|
102
|
+
return pathUnmanaged();
|
|
103
|
+
}
|
|
104
|
+
if (process.getuid !== undefined && (Number(info.mode) & 0o777) !== 0o700)
|
|
105
|
+
return pathUnmanaged();
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return pathUnmanaged();
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
const readAtMostBytes = async (handle, maximumBytes) => {
|
|
112
|
+
const buffer = Buffer.allocUnsafe(maximumBytes + 1);
|
|
113
|
+
let offset = 0;
|
|
114
|
+
while (offset < buffer.length) {
|
|
115
|
+
const result = await handle.read(buffer, offset, buffer.length - offset, offset);
|
|
116
|
+
if (result.bytesRead === 0)
|
|
117
|
+
break;
|
|
118
|
+
offset += result.bytesRead;
|
|
119
|
+
}
|
|
120
|
+
if (offset > maximumBytes)
|
|
121
|
+
return corrupt();
|
|
122
|
+
return buffer.subarray(0, offset);
|
|
123
|
+
};
|
|
124
|
+
const readStableFile = async (path, maximumBytes, options = {}) => {
|
|
125
|
+
for (let attempt = 0; attempt < MAX_CONSISTENT_READ_ATTEMPTS; attempt += 1) {
|
|
126
|
+
let before;
|
|
127
|
+
try {
|
|
128
|
+
before = await lstat(path, { bigint: true });
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
if (isMissing(error))
|
|
132
|
+
return null;
|
|
133
|
+
return pathUnmanaged();
|
|
134
|
+
}
|
|
135
|
+
if (!before.isFile() || before.isSymbolicLink() || !ownedByCurrentUser(before.uid)) {
|
|
136
|
+
return pathUnmanaged();
|
|
137
|
+
}
|
|
138
|
+
if (before.size > BigInt(maximumBytes))
|
|
139
|
+
return corrupt();
|
|
140
|
+
await options.afterInitialStat?.(attempt);
|
|
141
|
+
let handle = null;
|
|
142
|
+
try {
|
|
143
|
+
handle = await open(path, "r");
|
|
144
|
+
const opened = await handle.stat({ bigint: true });
|
|
145
|
+
if (!opened.isFile() || opened.isSymbolicLink() || !ownedByCurrentUser(opened.uid)) {
|
|
146
|
+
return pathUnmanaged();
|
|
147
|
+
}
|
|
148
|
+
if (opened.size > BigInt(maximumBytes))
|
|
149
|
+
return corrupt();
|
|
150
|
+
if (!sameFileVersion(fileIdentity(before), fileIdentity(opened)))
|
|
151
|
+
continue;
|
|
152
|
+
await options.afterOpenStat?.(attempt);
|
|
153
|
+
const content = await readAtMostBytes(handle, maximumBytes);
|
|
154
|
+
const after = await handle.stat({ bigint: true });
|
|
155
|
+
if (!after.isFile() || after.isSymbolicLink() || !ownedByCurrentUser(after.uid)) {
|
|
156
|
+
return pathUnmanaged();
|
|
157
|
+
}
|
|
158
|
+
if (after.size > BigInt(maximumBytes))
|
|
159
|
+
return corrupt();
|
|
160
|
+
let pathAfter;
|
|
161
|
+
try {
|
|
162
|
+
pathAfter = await lstat(path, { bigint: true });
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
if (isMissing(error))
|
|
166
|
+
continue;
|
|
167
|
+
return pathUnmanaged();
|
|
168
|
+
}
|
|
169
|
+
if (!pathAfter.isFile() || pathAfter.isSymbolicLink() || !ownedByCurrentUser(pathAfter.uid)) {
|
|
170
|
+
return pathUnmanaged();
|
|
171
|
+
}
|
|
172
|
+
if (pathAfter.size > BigInt(maximumBytes))
|
|
173
|
+
return corrupt();
|
|
174
|
+
const initialIdentity = fileIdentity(before);
|
|
175
|
+
const afterIdentity = fileIdentity(after);
|
|
176
|
+
if (!sameFileVersion(initialIdentity, afterIdentity)
|
|
177
|
+
|| !sameFileVersion(afterIdentity, fileIdentity(pathAfter))
|
|
178
|
+
|| BigInt(content.byteLength) !== after.size)
|
|
179
|
+
continue;
|
|
180
|
+
return Object.freeze({ identity: afterIdentity, raw: content.toString("utf8") });
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
if (error instanceof ProjectSkillProjectionStateError)
|
|
184
|
+
throw error;
|
|
185
|
+
if (isMissing(error))
|
|
186
|
+
continue;
|
|
187
|
+
return pathUnmanaged();
|
|
188
|
+
}
|
|
189
|
+
finally {
|
|
190
|
+
await handle?.close().catch(() => undefined);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return pathUnmanaged();
|
|
194
|
+
};
|
|
195
|
+
const parseManifestRaw = (raw) => {
|
|
196
|
+
let candidate;
|
|
197
|
+
try {
|
|
198
|
+
candidate = JSON.parse(raw);
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
return corrupt();
|
|
202
|
+
}
|
|
203
|
+
return parseAppliedProjectSkillManifest(candidate);
|
|
204
|
+
};
|
|
205
|
+
const boundedDirectoryNames = async (path, maximumEntries) => {
|
|
206
|
+
let directory;
|
|
207
|
+
try {
|
|
208
|
+
directory = await opendir(path);
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return pathUnmanaged();
|
|
212
|
+
}
|
|
213
|
+
const names = [];
|
|
214
|
+
try {
|
|
215
|
+
for (;;) {
|
|
216
|
+
const entry = await directory.read();
|
|
217
|
+
if (entry === null)
|
|
218
|
+
break;
|
|
219
|
+
names.push(entry.name);
|
|
220
|
+
if (names.length > maximumEntries)
|
|
221
|
+
return pathUnmanaged();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
return pathUnmanaged();
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
await directory.close().catch(() => undefined);
|
|
229
|
+
}
|
|
230
|
+
return Object.freeze(names.sort());
|
|
231
|
+
};
|
|
232
|
+
const operationNonce = (name) => {
|
|
233
|
+
if (!name.startsWith(OPERATION_PREFIX) || !name.endsWith(OPERATION_SUFFIX))
|
|
234
|
+
return null;
|
|
235
|
+
const nonce = name.slice(OPERATION_PREFIX.length, -OPERATION_SUFFIX.length);
|
|
236
|
+
return ARTIFACT_ID.test(nonce) ? nonce : null;
|
|
237
|
+
};
|
|
238
|
+
const lifecycleNonce = (name, prefix) => {
|
|
239
|
+
if (!name.startsWith(prefix))
|
|
240
|
+
return null;
|
|
241
|
+
const nonce = name.slice(prefix.length);
|
|
242
|
+
return ARTIFACT_ID.test(nonce) ? nonce : null;
|
|
243
|
+
};
|
|
244
|
+
const listStateArtifacts = async (stateDirectory) => {
|
|
245
|
+
const names = await boundedDirectoryNames(stateDirectory, MAX_STATE_DIRECTORY_ENTRIES);
|
|
246
|
+
const operations = names.filter((name) => name.startsWith(OPERATION_PREFIX) && name.endsWith(OPERATION_SUFFIX));
|
|
247
|
+
if (operations.some((name) => operationNonce(name) === null))
|
|
248
|
+
return pathUnmanaged();
|
|
249
|
+
const preparing = names.filter((name) => lifecycleNonce(name, PREPARING_PREFIX) !== null);
|
|
250
|
+
const discards = names.filter((name) => lifecycleNonce(name, DISCARD_PREFIX) !== null);
|
|
251
|
+
return Object.freeze({
|
|
252
|
+
operations: Object.freeze(operations.map((name) => join(stateDirectory, name))),
|
|
253
|
+
preparing: Object.freeze(preparing.map((name) => join(stateDirectory, name))),
|
|
254
|
+
discards: Object.freeze(discards.map((name) => join(stateDirectory, name))),
|
|
255
|
+
});
|
|
256
|
+
};
|
|
257
|
+
const identityMatchesBinding = (identity, expected, allowLinkOrRenameCtime) => identity.dev.toString() === expected.dev
|
|
258
|
+
&& identity.ino.toString() === expected.ino
|
|
259
|
+
&& identity.uid.toString() === expected.uid
|
|
260
|
+
&& identity.size.toString() === expected.size
|
|
261
|
+
&& identity.mtimeNs.toString() === expected.mtimeNs
|
|
262
|
+
&& (allowLinkOrRenameCtime || identity.ctimeNs.toString() === expected.ctimeNs);
|
|
263
|
+
const snapshotMatchesBinding = (snapshot, binding, allowLinkOrRenameCtime) => snapshot.raw === binding.content
|
|
264
|
+
&& digestRaw(snapshot.raw) === binding.digest
|
|
265
|
+
&& identityMatchesBinding(snapshot.identity, binding.identity, allowLinkOrRenameCtime);
|
|
266
|
+
const isAbsenceMarkerRaw = (raw) => raw === ABSENCE_MARKER_RAW;
|
|
267
|
+
const validateBindingDefinition = (binding, allowAbsence = false) => {
|
|
268
|
+
if (Buffer.byteLength(binding.content, "utf8") > MAX_MANIFEST_BYTES
|
|
269
|
+
|| digestRaw(binding.content) !== binding.digest
|
|
270
|
+
|| binding.identity.size !== Buffer.byteLength(binding.content, "utf8").toString())
|
|
271
|
+
return corrupt();
|
|
272
|
+
if (!allowAbsence || !isAbsenceMarkerRaw(binding.content))
|
|
273
|
+
parseManifestRaw(binding.content);
|
|
274
|
+
};
|
|
275
|
+
const parseOperationIntent = (snapshot) => {
|
|
276
|
+
let candidate;
|
|
277
|
+
try {
|
|
278
|
+
candidate = JSON.parse(snapshot.raw);
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
return corrupt();
|
|
282
|
+
}
|
|
283
|
+
if (candidate && typeof candidate === "object" && "managedBy" in candidate
|
|
284
|
+
&& candidate.managedBy !== OPERATION_MANAGED_BY) {
|
|
285
|
+
return unmanaged();
|
|
286
|
+
}
|
|
287
|
+
const parsed = OperationIntentSchema.safeParse(candidate);
|
|
288
|
+
if (!parsed.success)
|
|
289
|
+
return corrupt();
|
|
290
|
+
validateBindingDefinition(parsed.data.stage, true);
|
|
291
|
+
if (parsed.data.previous !== null)
|
|
292
|
+
validateBindingDefinition(parsed.data.previous);
|
|
293
|
+
return parsed.data;
|
|
294
|
+
};
|
|
295
|
+
const loadOperation = async (operationPath, expectedNonce = operationNonce(basename(operationPath))) => {
|
|
296
|
+
await validatePrivateOperationDirectory(operationPath);
|
|
297
|
+
if (expectedNonce === null)
|
|
298
|
+
return pathUnmanaged();
|
|
299
|
+
const names = await boundedDirectoryNames(operationPath, MAX_OPERATION_ENTRIES);
|
|
300
|
+
if (names.some((name) => ![
|
|
301
|
+
INTENT_BASENAME, STAGE_BASENAME, PREVIOUS_BASENAME, PUBLISHED_BASENAME,
|
|
302
|
+
].includes(name))) {
|
|
303
|
+
return pathUnmanaged();
|
|
304
|
+
}
|
|
305
|
+
if (!names.includes(INTENT_BASENAME))
|
|
306
|
+
return pathUnmanaged();
|
|
307
|
+
const intentSnapshot = await readStableFile(join(operationPath, INTENT_BASENAME), MAX_INTENT_BYTES);
|
|
308
|
+
if (intentSnapshot === null)
|
|
309
|
+
return pathUnmanaged();
|
|
310
|
+
const intent = parseOperationIntent(intentSnapshot);
|
|
311
|
+
if (intent.nonce !== expectedNonce)
|
|
312
|
+
return pathUnmanaged();
|
|
313
|
+
const stage = names.includes(STAGE_BASENAME)
|
|
314
|
+
? await readStableFile(join(operationPath, STAGE_BASENAME), MAX_MANIFEST_BYTES)
|
|
315
|
+
: null;
|
|
316
|
+
if (stage !== null && !snapshotMatchesBinding(stage, intent.stage, true))
|
|
317
|
+
return corrupt();
|
|
318
|
+
const previous = names.includes(PREVIOUS_BASENAME)
|
|
319
|
+
? await readStableFile(join(operationPath, PREVIOUS_BASENAME), MAX_MANIFEST_BYTES)
|
|
320
|
+
: null;
|
|
321
|
+
if (previous !== null && (intent.previous === null
|
|
322
|
+
|| !snapshotMatchesBinding(previous, intent.previous, true)))
|
|
323
|
+
return corrupt();
|
|
324
|
+
if (intent.previous === null && names.includes(PREVIOUS_BASENAME))
|
|
325
|
+
return corrupt();
|
|
326
|
+
const published = names.includes(PUBLISHED_BASENAME)
|
|
327
|
+
? await readStableFile(join(operationPath, PUBLISHED_BASENAME), MAX_MANIFEST_BYTES)
|
|
328
|
+
: null;
|
|
329
|
+
if (published !== null && !snapshotMatchesBinding(published, intent.stage, true))
|
|
330
|
+
return corrupt();
|
|
331
|
+
return Object.freeze({ path: operationPath, intent, intentSnapshot, stage, previous, published });
|
|
332
|
+
};
|
|
333
|
+
const createExclusiveDurableFile = async (path, content, maximumBytes) => {
|
|
334
|
+
if (Buffer.byteLength(content, "utf8") > maximumBytes)
|
|
335
|
+
return corrupt();
|
|
336
|
+
let handle = null;
|
|
337
|
+
try {
|
|
338
|
+
handle = await open(path, "wx", 0o600);
|
|
339
|
+
await handle.writeFile(content, "utf8");
|
|
340
|
+
await handle.sync();
|
|
341
|
+
await handle.close();
|
|
342
|
+
handle = null;
|
|
343
|
+
const snapshot = await readStableFile(path, maximumBytes);
|
|
344
|
+
if (snapshot === null || snapshot.raw !== content)
|
|
345
|
+
return pathUnmanaged();
|
|
346
|
+
return snapshot;
|
|
347
|
+
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
if (error instanceof ProjectSkillProjectionStateError)
|
|
350
|
+
throw error;
|
|
351
|
+
return pathUnmanaged();
|
|
352
|
+
}
|
|
353
|
+
finally {
|
|
354
|
+
await handle?.close().catch(() => undefined);
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
const operationBinding = (basename, snapshot) => ({
|
|
358
|
+
basename, content: snapshot.raw, digest: digestRaw(snapshot.raw),
|
|
359
|
+
identity: serializeIdentity(snapshot.identity),
|
|
360
|
+
});
|
|
361
|
+
const createOperation = async (stateDirectory, current, content, randomId, syncDirectory, afterIntentDurableBeforeActivate) => {
|
|
362
|
+
const nonce = randomId();
|
|
363
|
+
if (!ARTIFACT_ID.test(nonce))
|
|
364
|
+
return pathUnmanaged();
|
|
365
|
+
const operationPath = join(stateDirectory, `${OPERATION_PREFIX}${nonce}${OPERATION_SUFFIX}`);
|
|
366
|
+
const preparingPath = join(stateDirectory, `${PREPARING_PREFIX}${nonce}`);
|
|
367
|
+
try {
|
|
368
|
+
await mkdir(preparingPath, { mode: 0o700 });
|
|
369
|
+
await chmod(preparingPath, 0o700);
|
|
370
|
+
}
|
|
371
|
+
catch {
|
|
372
|
+
return pathUnmanaged();
|
|
373
|
+
}
|
|
374
|
+
await syncDirectory(stateDirectory);
|
|
375
|
+
await validatePrivateOperationDirectory(preparingPath);
|
|
376
|
+
const stage = await createExclusiveDurableFile(join(preparingPath, STAGE_BASENAME), content, MAX_MANIFEST_BYTES);
|
|
377
|
+
await syncDirectory(preparingPath).catch(() => pathUnmanaged());
|
|
378
|
+
const intent = {
|
|
379
|
+
managedBy: OPERATION_MANAGED_BY,
|
|
380
|
+
version: 2,
|
|
381
|
+
nonce,
|
|
382
|
+
finalBasename: MANIFEST_FILE_NAME,
|
|
383
|
+
stage: operationBinding(STAGE_BASENAME, stage),
|
|
384
|
+
previous: current === null ? null : operationBinding(PREVIOUS_BASENAME, current),
|
|
385
|
+
};
|
|
386
|
+
await createExclusiveDurableFile(join(preparingPath, INTENT_BASENAME), `${JSON.stringify(intent)}\n`, MAX_INTENT_BYTES);
|
|
387
|
+
await syncDirectory(preparingPath).catch(() => pathUnmanaged());
|
|
388
|
+
await syncDirectory(stateDirectory).catch(() => pathUnmanaged());
|
|
389
|
+
await afterIntentDurableBeforeActivate?.(preparingPath);
|
|
390
|
+
try {
|
|
391
|
+
await lstat(operationPath);
|
|
392
|
+
return pathUnmanaged();
|
|
393
|
+
}
|
|
394
|
+
catch (error) {
|
|
395
|
+
if (!isMissing(error))
|
|
396
|
+
return pathUnmanaged();
|
|
397
|
+
}
|
|
398
|
+
try {
|
|
399
|
+
await rename(preparingPath, operationPath);
|
|
400
|
+
await syncDirectory(stateDirectory);
|
|
401
|
+
}
|
|
402
|
+
catch {
|
|
403
|
+
return pathUnmanaged();
|
|
404
|
+
}
|
|
405
|
+
return loadOperation(operationPath);
|
|
406
|
+
};
|
|
407
|
+
const revalidatePrivateFile = async (operationPath, basename, maximumBytes, expected) => {
|
|
408
|
+
await validatePrivateOperationDirectory(operationPath);
|
|
409
|
+
const fenced = await readStableFile(join(operationPath, basename), maximumBytes);
|
|
410
|
+
if (fenced === null || !sameSnapshot(fenced, expected))
|
|
411
|
+
return pathUnmanaged();
|
|
412
|
+
};
|
|
413
|
+
const removeBoundOperationFile = async (operation, basename, binding, syncDirectory) => {
|
|
414
|
+
const path = join(operation.path, basename);
|
|
415
|
+
const snapshot = await readStableFile(path, MAX_MANIFEST_BYTES);
|
|
416
|
+
if (snapshot === null)
|
|
417
|
+
return;
|
|
418
|
+
if (!snapshotMatchesBinding(snapshot, binding, true))
|
|
419
|
+
return corrupt();
|
|
420
|
+
await revalidatePrivateFile(operation.path, basename, MAX_MANIFEST_BYTES, snapshot);
|
|
421
|
+
try {
|
|
422
|
+
await unlink(path);
|
|
423
|
+
await syncDirectory(operation.path);
|
|
424
|
+
}
|
|
425
|
+
catch {
|
|
426
|
+
return pathUnmanaged();
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
const cleanupDiscard = async (discardPath, syncDirectory, hooks = {}) => {
|
|
430
|
+
await validatePrivateOperationDirectory(discardPath);
|
|
431
|
+
const nonce = lifecycleNonce(basename(discardPath), DISCARD_PREFIX);
|
|
432
|
+
if (nonce === null)
|
|
433
|
+
return pathUnmanaged();
|
|
434
|
+
const initialNames = await boundedDirectoryNames(discardPath, MAX_OPERATION_ENTRIES);
|
|
435
|
+
if (initialNames.some((name) => ![
|
|
436
|
+
INTENT_BASENAME, STAGE_BASENAME, PREVIOUS_BASENAME, PUBLISHED_BASENAME,
|
|
437
|
+
].includes(name))) {
|
|
438
|
+
return pathUnmanaged();
|
|
439
|
+
}
|
|
440
|
+
if (!initialNames.includes(INTENT_BASENAME)) {
|
|
441
|
+
if (initialNames.length !== 0)
|
|
442
|
+
return pathUnmanaged();
|
|
443
|
+
try {
|
|
444
|
+
await rmdir(discardPath);
|
|
445
|
+
await syncDirectory(dirname(discardPath));
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
catch {
|
|
449
|
+
return pathUnmanaged();
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
const operation = await loadOperation(discardPath, nonce);
|
|
453
|
+
const names = await boundedDirectoryNames(operation.path, MAX_OPERATION_ENTRIES);
|
|
454
|
+
if (names.includes(PREVIOUS_BASENAME)) {
|
|
455
|
+
if (operation.intent.previous === null)
|
|
456
|
+
return corrupt();
|
|
457
|
+
await removeBoundOperationFile(operation, PREVIOUS_BASENAME, operation.intent.previous, syncDirectory);
|
|
458
|
+
}
|
|
459
|
+
if (names.includes(STAGE_BASENAME)) {
|
|
460
|
+
await removeBoundOperationFile(operation, STAGE_BASENAME, operation.intent.stage, syncDirectory);
|
|
461
|
+
}
|
|
462
|
+
if (names.includes(PUBLISHED_BASENAME)) {
|
|
463
|
+
await removeBoundOperationFile(operation, PUBLISHED_BASENAME, operation.intent.stage, syncDirectory);
|
|
464
|
+
}
|
|
465
|
+
const intent = await readStableFile(join(operation.path, INTENT_BASENAME), MAX_INTENT_BYTES);
|
|
466
|
+
if (intent === null || !sameSnapshot(intent, operation.intentSnapshot))
|
|
467
|
+
return pathUnmanaged();
|
|
468
|
+
parseOperationIntent(intent);
|
|
469
|
+
await revalidatePrivateFile(operation.path, INTENT_BASENAME, MAX_INTENT_BYTES, intent);
|
|
470
|
+
try {
|
|
471
|
+
await unlink(join(operation.path, INTENT_BASENAME));
|
|
472
|
+
await syncDirectory(operation.path);
|
|
473
|
+
}
|
|
474
|
+
catch {
|
|
475
|
+
return pathUnmanaged();
|
|
476
|
+
}
|
|
477
|
+
await hooks.afterDiscardIntentRemoved?.(discardPath);
|
|
478
|
+
try {
|
|
479
|
+
await rmdir(operation.path);
|
|
480
|
+
await syncDirectory(dirname(operation.path));
|
|
481
|
+
}
|
|
482
|
+
catch {
|
|
483
|
+
return pathUnmanaged();
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
const cleanupOperation = async (operationPath, syncDirectory, hooks = {}) => {
|
|
487
|
+
const operation = await loadOperation(operationPath);
|
|
488
|
+
const discardPath = join(dirname(operationPath), `${DISCARD_PREFIX}${operation.intent.nonce}`);
|
|
489
|
+
try {
|
|
490
|
+
await lstat(discardPath);
|
|
491
|
+
return pathUnmanaged();
|
|
492
|
+
}
|
|
493
|
+
catch (error) {
|
|
494
|
+
if (!isMissing(error))
|
|
495
|
+
return pathUnmanaged();
|
|
496
|
+
}
|
|
497
|
+
try {
|
|
498
|
+
await rename(operationPath, discardPath);
|
|
499
|
+
await syncDirectory(dirname(operationPath));
|
|
500
|
+
}
|
|
501
|
+
catch {
|
|
502
|
+
return pathUnmanaged();
|
|
503
|
+
}
|
|
504
|
+
await hooks.afterOperationDiscarded?.(discardPath);
|
|
505
|
+
await cleanupDiscard(discardPath, syncDirectory, hooks);
|
|
506
|
+
};
|
|
507
|
+
const restorePreviousNoClobber = async (operation, manifestPath, stateDirectory, syncDirectory) => {
|
|
508
|
+
const binding = operation.intent.previous;
|
|
509
|
+
if (binding === null)
|
|
510
|
+
return pathUnmanaged();
|
|
511
|
+
const previousPath = join(operation.path, PREVIOUS_BASENAME);
|
|
512
|
+
const previous = await readStableFile(previousPath, MAX_MANIFEST_BYTES);
|
|
513
|
+
if (previous === null || !snapshotMatchesBinding(previous, binding, true))
|
|
514
|
+
return corrupt();
|
|
515
|
+
await revalidatePrivateFile(operation.path, PREVIOUS_BASENAME, MAX_MANIFEST_BYTES, previous);
|
|
516
|
+
try {
|
|
517
|
+
await link(previousPath, manifestPath);
|
|
518
|
+
await syncDirectory(stateDirectory);
|
|
519
|
+
}
|
|
520
|
+
catch (error) {
|
|
521
|
+
if (!isExisting(error))
|
|
522
|
+
return pathUnmanaged();
|
|
523
|
+
}
|
|
524
|
+
const restored = await readStableFile(manifestPath, MAX_MANIFEST_BYTES);
|
|
525
|
+
if (restored === null)
|
|
526
|
+
return pathUnmanaged();
|
|
527
|
+
parseManifestRaw(restored.raw);
|
|
528
|
+
if (!snapshotMatchesBinding(restored, binding, true)
|
|
529
|
+
&& (isAbsenceMarkerRaw(operation.intent.stage.content)
|
|
530
|
+
|| !snapshotMatchesBinding(restored, operation.intent.stage, true)))
|
|
531
|
+
return pathUnmanaged();
|
|
532
|
+
return restored;
|
|
533
|
+
};
|
|
534
|
+
const recoverMissingFinalForReader = async (manifestPath, stateDirectory, syncDirectory) => {
|
|
535
|
+
const artifacts = await listStateArtifacts(stateDirectory);
|
|
536
|
+
if (artifacts.discards.length !== 0)
|
|
537
|
+
return pathUnmanaged();
|
|
538
|
+
const { inert, proven } = await classifyOperations(artifacts.operations);
|
|
539
|
+
if (proven.length === 0)
|
|
540
|
+
return inert === 0 ? null : pathUnmanaged();
|
|
541
|
+
if (proven.length !== 1)
|
|
542
|
+
return pathUnmanaged();
|
|
543
|
+
try {
|
|
544
|
+
const operation = proven[0];
|
|
545
|
+
if (isAbsenceMarkerRaw(operation.intent.stage.content) && operation.published !== null)
|
|
546
|
+
return null;
|
|
547
|
+
if (operation.intent.previous === null)
|
|
548
|
+
return null;
|
|
549
|
+
if (operation.previous === null)
|
|
550
|
+
return pathUnmanaged();
|
|
551
|
+
const restored = await restorePreviousNoClobber(operation, manifestPath, stateDirectory, syncDirectory);
|
|
552
|
+
return parseManifestRaw(restored.raw);
|
|
553
|
+
}
|
|
554
|
+
catch (error) {
|
|
555
|
+
const appeared = await readStableFile(manifestPath, MAX_MANIFEST_BYTES);
|
|
556
|
+
if (appeared !== null)
|
|
557
|
+
return parseManifestRaw(appeared.raw);
|
|
558
|
+
throw error;
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
const classifyOperations = async (paths) => {
|
|
562
|
+
const proven = [];
|
|
563
|
+
let inert = 0;
|
|
564
|
+
for (const path of paths) {
|
|
565
|
+
await validatePrivateOperationDirectory(path);
|
|
566
|
+
const names = await boundedDirectoryNames(path, MAX_OPERATION_ENTRIES);
|
|
567
|
+
if (!names.includes(INTENT_BASENAME)) {
|
|
568
|
+
inert += 1;
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
proven.push(await loadOperation(path));
|
|
572
|
+
}
|
|
573
|
+
return Object.freeze({ inert, proven: Object.freeze(proven) });
|
|
574
|
+
};
|
|
575
|
+
const settleExistingOperation = async (operationPath, manifestPath, stateDirectory, syncDirectory) => {
|
|
576
|
+
const operation = await loadOperation(operationPath);
|
|
577
|
+
let final = await readStableFile(manifestPath, MAX_MANIFEST_BYTES);
|
|
578
|
+
if (final === null) {
|
|
579
|
+
if (isAbsenceMarkerRaw(operation.intent.stage.content) && operation.published !== null) {
|
|
580
|
+
await cleanupOperation(operationPath, syncDirectory);
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
if (operation.intent.previous === null) {
|
|
584
|
+
await cleanupOperation(operationPath, syncDirectory);
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
if (operation.previous === null)
|
|
588
|
+
return pathUnmanaged();
|
|
589
|
+
final = await restorePreviousNoClobber(operation, manifestPath, stateDirectory, syncDirectory);
|
|
590
|
+
}
|
|
591
|
+
parseManifestRaw(final.raw);
|
|
592
|
+
const knownStage = snapshotMatchesBinding(final, operation.intent.stage, true);
|
|
593
|
+
const knownPrevious = operation.intent.previous !== null
|
|
594
|
+
&& snapshotMatchesBinding(final, operation.intent.previous, true);
|
|
595
|
+
if (!knownStage && !knownPrevious)
|
|
596
|
+
return pathUnmanaged();
|
|
597
|
+
await cleanupOperation(operationPath, syncDirectory);
|
|
598
|
+
};
|
|
599
|
+
const settleAnyExistingOperation = async (manifestPath, stateDirectory, syncDirectory) => {
|
|
600
|
+
const { operations } = await listStateArtifacts(stateDirectory);
|
|
601
|
+
const { inert, proven } = await classifyOperations(operations);
|
|
602
|
+
if (proven.length > 1)
|
|
603
|
+
return pathUnmanaged();
|
|
604
|
+
if (proven.length === 1) {
|
|
605
|
+
await settleExistingOperation(proven[0].path, manifestPath, stateDirectory, syncDirectory);
|
|
606
|
+
}
|
|
607
|
+
else if (inert !== 0) {
|
|
608
|
+
const final = await readStableFile(manifestPath, MAX_MANIFEST_BYTES);
|
|
609
|
+
if (final === null)
|
|
610
|
+
return pathUnmanaged();
|
|
611
|
+
parseManifestRaw(final.raw);
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
const settleDiscardResidues = async (stateDirectory, syncDirectory) => {
|
|
615
|
+
const { discards } = await listStateArtifacts(stateDirectory);
|
|
616
|
+
for (const discardPath of discards)
|
|
617
|
+
await cleanupDiscard(discardPath, syncDirectory);
|
|
618
|
+
};
|
|
619
|
+
const settlePreparingResidues = async (stateDirectory, syncDirectory) => {
|
|
620
|
+
const { preparing } = await listStateArtifacts(stateDirectory);
|
|
621
|
+
for (const preparingPath of preparing) {
|
|
622
|
+
const nonce = lifecycleNonce(basename(preparingPath), PREPARING_PREFIX);
|
|
623
|
+
if (nonce === null)
|
|
624
|
+
continue;
|
|
625
|
+
let operation;
|
|
626
|
+
try {
|
|
627
|
+
operation = await loadOperation(preparingPath, nonce);
|
|
628
|
+
}
|
|
629
|
+
catch {
|
|
630
|
+
// Without exact, complete provenance this unrecognized residue is preserved and ignored.
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
if (operation.stage === null || operation.previous !== null)
|
|
634
|
+
continue;
|
|
635
|
+
const discardPath = join(stateDirectory, `${DISCARD_PREFIX}${nonce}`);
|
|
636
|
+
try {
|
|
637
|
+
await lstat(discardPath);
|
|
638
|
+
return pathUnmanaged();
|
|
639
|
+
}
|
|
640
|
+
catch (error) {
|
|
641
|
+
if (!isMissing(error))
|
|
642
|
+
return pathUnmanaged();
|
|
643
|
+
}
|
|
644
|
+
try {
|
|
645
|
+
await rename(preparingPath, discardPath);
|
|
646
|
+
await syncDirectory(stateDirectory);
|
|
647
|
+
}
|
|
648
|
+
catch {
|
|
649
|
+
return pathUnmanaged();
|
|
650
|
+
}
|
|
651
|
+
await cleanupDiscard(discardPath, syncDirectory);
|
|
652
|
+
}
|
|
653
|
+
};
|
|
654
|
+
const currentStillMatches = (current, observed) => current === null ? observed === null : observed !== null && sameSnapshot(current, observed);
|
|
655
|
+
export async function readAppliedProjectSkillManifest(agentRoot, options = {}) {
|
|
656
|
+
const syncDirectory = projectionDirectorySync(options);
|
|
657
|
+
await realOwnedDirectory(agentRoot, false);
|
|
658
|
+
const stateDirectory = join(agentRoot, ".crew");
|
|
659
|
+
if (!await realOwnedDirectory(stateDirectory, true))
|
|
660
|
+
return null;
|
|
661
|
+
const manifestPath = appliedProjectSkillManifestPath(agentRoot);
|
|
662
|
+
const snapshot = await readStableFile(manifestPath, MAX_MANIFEST_BYTES, options);
|
|
663
|
+
return snapshot === null
|
|
664
|
+
? recoverMissingFinalForReader(manifestPath, stateDirectory, syncDirectory)
|
|
665
|
+
: parseManifestRaw(snapshot.raw);
|
|
666
|
+
}
|
|
667
|
+
export async function writeAppliedProjectSkillManifest(agentRoot, manifest, options = {}) {
|
|
668
|
+
const parsed = parseAppliedProjectSkillManifest(manifest);
|
|
669
|
+
const syncDirectory = projectionDirectorySync(options);
|
|
670
|
+
await realOwnedDirectory(agentRoot, false);
|
|
671
|
+
const stateDirectory = join(agentRoot, ".crew");
|
|
672
|
+
if (!await realOwnedDirectory(stateDirectory, true)) {
|
|
673
|
+
try {
|
|
674
|
+
await mkdir(stateDirectory, { mode: 0o700 });
|
|
675
|
+
}
|
|
676
|
+
catch (error) {
|
|
677
|
+
if (!isExisting(error))
|
|
678
|
+
return pathUnmanaged();
|
|
679
|
+
}
|
|
680
|
+
await realOwnedDirectory(stateDirectory, false);
|
|
681
|
+
}
|
|
682
|
+
await chmod(stateDirectory, 0o700).catch(() => pathUnmanaged());
|
|
683
|
+
const content = `${JSON.stringify(parsed)}\n`;
|
|
684
|
+
if (Buffer.byteLength(content, "utf8") > MAX_MANIFEST_BYTES)
|
|
685
|
+
return corrupt();
|
|
686
|
+
const manifestPath = appliedProjectSkillManifestPath(agentRoot);
|
|
687
|
+
await settleDiscardResidues(stateDirectory, syncDirectory);
|
|
688
|
+
await settlePreparingResidues(stateDirectory, syncDirectory);
|
|
689
|
+
await settleAnyExistingOperation(manifestPath, stateDirectory, syncDirectory);
|
|
690
|
+
for (let attempt = 0; attempt < MAX_CONSISTENT_READ_ATTEMPTS; attempt += 1) {
|
|
691
|
+
const current = await readStableFile(manifestPath, MAX_MANIFEST_BYTES);
|
|
692
|
+
if (current !== null)
|
|
693
|
+
parseManifestRaw(current.raw);
|
|
694
|
+
const operation = await createOperation(stateDirectory, current, content, options.randomId ?? randomUUID, syncDirectory, options.afterIntentDurableBeforeActivate);
|
|
695
|
+
const stagePath = join(operation.path, STAGE_BASENAME);
|
|
696
|
+
await options.afterStageDurable?.(stagePath);
|
|
697
|
+
const stagedOperation = await loadOperation(operation.path);
|
|
698
|
+
if (!sameSnapshot(stagedOperation.intentSnapshot, operation.intentSnapshot)
|
|
699
|
+
|| stagedOperation.stage === null
|
|
700
|
+
|| !snapshotMatchesBinding(stagedOperation.stage, operation.intent.stage, false))
|
|
701
|
+
return corrupt();
|
|
702
|
+
let observed;
|
|
703
|
+
try {
|
|
704
|
+
observed = await readStableFile(manifestPath, MAX_MANIFEST_BYTES);
|
|
705
|
+
}
|
|
706
|
+
catch (error) {
|
|
707
|
+
await cleanupOperation(operation.path, syncDirectory);
|
|
708
|
+
throw error;
|
|
709
|
+
}
|
|
710
|
+
if (!currentStillMatches(current, observed)) {
|
|
711
|
+
let observedError;
|
|
712
|
+
try {
|
|
713
|
+
if (observed !== null)
|
|
714
|
+
parseManifestRaw(observed.raw);
|
|
715
|
+
}
|
|
716
|
+
catch (error) {
|
|
717
|
+
observedError = error;
|
|
718
|
+
}
|
|
719
|
+
await cleanupOperation(operation.path, syncDirectory);
|
|
720
|
+
if (observedError !== undefined)
|
|
721
|
+
throw observedError;
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
if (current !== null) {
|
|
725
|
+
const previousPath = join(operation.path, PREVIOUS_BASENAME);
|
|
726
|
+
try {
|
|
727
|
+
await rename(manifestPath, previousPath);
|
|
728
|
+
await syncDirectory(operation.path);
|
|
729
|
+
await syncDirectory(stateDirectory);
|
|
730
|
+
}
|
|
731
|
+
catch (error) {
|
|
732
|
+
if (!isMissing(error))
|
|
733
|
+
return pathUnmanaged();
|
|
734
|
+
await cleanupOperation(operation.path, syncDirectory);
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
await options.afterCurrentClaimed?.(previousPath);
|
|
738
|
+
const previous = await readStableFile(previousPath, MAX_MANIFEST_BYTES);
|
|
739
|
+
const previousBinding = operation.intent.previous;
|
|
740
|
+
if (previous === null || previousBinding === null)
|
|
741
|
+
return pathUnmanaged();
|
|
742
|
+
if (!snapshotMatchesBinding(previous, previousBinding, true)) {
|
|
743
|
+
try {
|
|
744
|
+
await link(previousPath, manifestPath);
|
|
745
|
+
await syncDirectory(stateDirectory);
|
|
746
|
+
}
|
|
747
|
+
catch (error) {
|
|
748
|
+
if (!isExisting(error))
|
|
749
|
+
return pathUnmanaged();
|
|
750
|
+
}
|
|
751
|
+
return corrupt();
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
// This is the final source-of-publication fence, after every hook and previous-state move.
|
|
755
|
+
let publicationOperation;
|
|
756
|
+
try {
|
|
757
|
+
publicationOperation = await loadOperation(operation.path);
|
|
758
|
+
}
|
|
759
|
+
catch (error) {
|
|
760
|
+
if (operation.intent.previous !== null) {
|
|
761
|
+
await restorePreviousNoClobber(operation, manifestPath, stateDirectory, syncDirectory);
|
|
762
|
+
}
|
|
763
|
+
throw error;
|
|
764
|
+
}
|
|
765
|
+
if (!sameSnapshot(publicationOperation.intentSnapshot, operation.intentSnapshot)) {
|
|
766
|
+
if (operation.intent.previous !== null) {
|
|
767
|
+
await restorePreviousNoClobber(operation, manifestPath, stateDirectory, syncDirectory);
|
|
768
|
+
}
|
|
769
|
+
return corrupt();
|
|
770
|
+
}
|
|
771
|
+
const publishableStage = await readStableFile(stagePath, MAX_MANIFEST_BYTES);
|
|
772
|
+
if (publishableStage === null
|
|
773
|
+
|| !snapshotMatchesBinding(publishableStage, operation.intent.stage, false)) {
|
|
774
|
+
if (operation.intent.previous !== null) {
|
|
775
|
+
await restorePreviousNoClobber(operation, manifestPath, stateDirectory, syncDirectory);
|
|
776
|
+
}
|
|
777
|
+
return corrupt();
|
|
778
|
+
}
|
|
779
|
+
parseManifestRaw(publishableStage.raw);
|
|
780
|
+
try {
|
|
781
|
+
await link(stagePath, manifestPath);
|
|
782
|
+
await syncDirectory(stateDirectory);
|
|
783
|
+
}
|
|
784
|
+
catch (error) {
|
|
785
|
+
if (!isExisting(error))
|
|
786
|
+
return pathUnmanaged();
|
|
787
|
+
const occupant = await readStableFile(manifestPath, MAX_MANIFEST_BYTES);
|
|
788
|
+
if (occupant === null)
|
|
789
|
+
return pathUnmanaged();
|
|
790
|
+
parseManifestRaw(occupant.raw);
|
|
791
|
+
const isPrevious = operation.intent.previous !== null
|
|
792
|
+
&& snapshotMatchesBinding(occupant, operation.intent.previous, true);
|
|
793
|
+
const isStage = snapshotMatchesBinding(occupant, operation.intent.stage, true);
|
|
794
|
+
if (!isPrevious && !isStage)
|
|
795
|
+
return pathUnmanaged();
|
|
796
|
+
await cleanupOperation(operation.path, syncDirectory, options);
|
|
797
|
+
if (isStage)
|
|
798
|
+
return;
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
const publishedStage = await readStableFile(stagePath, MAX_MANIFEST_BYTES);
|
|
802
|
+
const publishedFinal = await readStableFile(manifestPath, MAX_MANIFEST_BYTES);
|
|
803
|
+
if (publishedStage === null || publishedFinal === null
|
|
804
|
+
|| !snapshotMatchesBinding(publishedStage, operation.intent.stage, true)
|
|
805
|
+
|| !snapshotMatchesBinding(publishedFinal, operation.intent.stage, true)
|
|
806
|
+
|| publishedStage.identity.dev !== publishedFinal.identity.dev
|
|
807
|
+
|| publishedStage.identity.ino !== publishedFinal.identity.ino)
|
|
808
|
+
return pathUnmanaged();
|
|
809
|
+
await cleanupOperation(operation.path, syncDirectory, options);
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
return pathUnmanaged();
|
|
813
|
+
}
|
|
814
|
+
/** Package-private primitives used by the crash-safe projection/state transaction owner. */
|
|
815
|
+
export const projectSkillManifestStoreInternals = Object.freeze({
|
|
816
|
+
ABSENCE_MARKER_RAW,
|
|
817
|
+
ARTIFACT_ID,
|
|
818
|
+
MAX_MANIFEST_BYTES,
|
|
819
|
+
OPERATION_PREFIX,
|
|
820
|
+
OPERATION_SUFFIX,
|
|
821
|
+
PREVIOUS_BASENAME,
|
|
822
|
+
PUBLISHED_BASENAME,
|
|
823
|
+
STAGE_BASENAME,
|
|
824
|
+
cleanupOperation,
|
|
825
|
+
createOperation,
|
|
826
|
+
isExisting,
|
|
827
|
+
isAbsenceMarkerRaw,
|
|
828
|
+
loadOperation,
|
|
829
|
+
parseAppliedProjectSkillManifest,
|
|
830
|
+
parseManifestRaw,
|
|
831
|
+
pathUnmanaged,
|
|
832
|
+
projectionDirectorySync,
|
|
833
|
+
readStableFile,
|
|
834
|
+
realOwnedDirectory,
|
|
835
|
+
restorePreviousNoClobber,
|
|
836
|
+
sameSnapshot,
|
|
837
|
+
settleAnyExistingOperation,
|
|
838
|
+
settleDiscardResidues,
|
|
839
|
+
settlePreparingResidues,
|
|
840
|
+
snapshotMatchesBinding,
|
|
841
|
+
});
|