@nowcrew/daemon 0.6.16 → 0.6.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/agent-ability/runtime-context.js +7 -1
  2. package/dist/atomic-private-write.js +54 -1
  3. package/dist/automatic-install-target.js +40 -11
  4. package/dist/console.js +9 -0
  5. package/dist/control-plane-url.js +2 -2
  6. package/dist/daemon-migration-controller.js +198 -0
  7. package/dist/daemon-migration-wiring.js +22 -0
  8. package/dist/daemon-update-eligibility.js +1 -1
  9. package/dist/directory-projection-identity.js +32 -0
  10. package/dist/directory-projection.js +922 -0
  11. package/dist/execution-protocol.js +78 -11
  12. package/dist/execution-runner.js +50 -2
  13. package/dist/i18n.js +1 -0
  14. package/dist/local-execution-prompt.js +57 -0
  15. package/dist/local-executor.js +99 -40
  16. package/dist/machine-info.js +45 -9
  17. package/dist/normalize.js +5 -0
  18. package/dist/profile-layout.js +41 -0
  19. package/dist/project-skills/controller.js +74 -14
  20. package/dist/project-skills/execution-adapter.js +11 -0
  21. package/dist/project-skills/initialized-reconciler.js +20 -0
  22. package/dist/project-skills/projection-set-switch.js +419 -0
  23. package/dist/project-skills/projection-state-domain.js +153 -0
  24. package/dist/project-skills/projection-state-store.js +841 -0
  25. package/dist/project-skills/projection-state-transaction.js +318 -0
  26. package/dist/project-skills/projection-state.js +3 -0
  27. package/dist/project-skills/reconciler.js +299 -68
  28. package/dist/project-skills/runtime-warning.js +6 -0
  29. package/dist/project-skills/scanner.js +30 -1
  30. package/dist/project-skills/types.js +9 -0
  31. package/dist/project-workspaces/resolver.js +179 -0
  32. package/dist/project-workspaces/types.js +1 -0
  33. package/dist/prompt.js +40 -0
  34. package/dist/runtimes/claude.js +235 -4
  35. package/dist/runtimes/codex-app-server-runner.js +92 -23
  36. package/dist/runtimes/codex-contract.js +123 -0
  37. package/dist/runtimes/codex.js +2 -0
  38. package/dist/serve.js +31 -17
  39. package/dist/session.js +3 -0
  40. package/dist/supervised-runtime.js +12 -4
  41. package/dist/workspace.js +14 -5
  42. package/package.json +1 -1
@@ -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
+ });