@nowcrew/daemon 0.6.18 → 0.6.20

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 (37) hide show
  1. package/dist/atomic-no-replace-rename.js +91 -0
  2. package/dist/control-plane-url.js +4 -2
  3. package/dist/directory-projection-publication.js +105 -0
  4. package/dist/directory-projection.js +20 -4
  5. package/dist/execution-journal.js +40 -4
  6. package/dist/execution-posix-stop-proof.js +82 -0
  7. package/dist/execution-runner.js +68 -8
  8. package/dist/local-executor.js +73 -52
  9. package/dist/machine-info.js +8 -5
  10. package/dist/project-skills/capability.js +109 -0
  11. package/dist/project-skills/controller-convergence.js +57 -0
  12. package/dist/project-skills/controller.js +80 -24
  13. package/dist/project-skills/initialized-reconciler.js +4 -4
  14. package/dist/project-skills/projection-state-domain.js +19 -2
  15. package/dist/project-skills/projection-state-store.js +3 -2
  16. package/dist/project-skills/projection-state-transaction.js +5 -1
  17. package/dist/project-skills/projection-state.js +1 -1
  18. package/dist/project-skills/reconciler.js +275 -102
  19. package/dist/project-skills/runtime-launch.js +102 -0
  20. package/dist/project-skills/runtime-root-bootstrap.js +47 -0
  21. package/dist/project-skills/runtime-root-domain.js +268 -0
  22. package/dist/project-skills/runtime-root-gc.js +293 -0
  23. package/dist/project-skills/runtime-root-lease-artifact.js +46 -0
  24. package/dist/project-skills/runtime-root-leases.js +487 -0
  25. package/dist/project-skills/runtime-root-source-identity.js +60 -0
  26. package/dist/project-skills/runtime-root-startup.js +49 -0
  27. package/dist/project-skills/runtime-root-state-artifact-domain.js +143 -0
  28. package/dist/project-skills/runtime-root-state-index.js +356 -0
  29. package/dist/project-skills/runtime-root-store.js +722 -0
  30. package/dist/project-skills/serve-capability.js +28 -0
  31. package/dist/project-skills/serve-startup.js +22 -0
  32. package/dist/project-skills/types.js +1 -0
  33. package/dist/provider-env.js +3 -0
  34. package/dist/runtimes/codex-home.js +50 -0
  35. package/dist/serve.js +58 -73
  36. package/dist/supervised-runtime.js +1 -5
  37. package/package.json +2 -2
@@ -0,0 +1,293 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { chmod, lstat, mkdir, opendir, readFile, readdir, rename, rm, } from "node:fs/promises";
3
+ import { posix, win32 } from "node:path";
4
+ import { durableDirectorySync } from "../atomic-private-write.js";
5
+ import { isCanonicalProjectSkillRuntimeRootId, parseProjectSkillRuntimeRootManifest, } from "./runtime-root-domain.js";
6
+ import { createProjectSkillRuntimeRootStore, projectSkillRuntimeRootDirectory, } from "./runtime-root-store.js";
7
+ const MANIFEST = "MANIFEST.json";
8
+ const MAX_SCAN_ENTRIES = 2_048;
9
+ const MAX_TREE_ENTRIES = 20_480;
10
+ const MAX_TREE_DEPTH = 64;
11
+ const MAX_TREE_BYTES = 8 * 1024 * 1024;
12
+ const MAX_DELETE_COUNT = 64;
13
+ const MAX_MANIFEST_BYTES = 256 * 1024;
14
+ const QUARANTINE = /^\.gc-([0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$/u;
15
+ const codeOf = (error) => error.code;
16
+ const isMissing = (error) => codeOf(error) === "ENOENT";
17
+ const isBusy = (error) => ["EPERM", "EACCES", "EBUSY"].includes(codeOf(error) ?? "");
18
+ const owned = (uid) => process.getuid === undefined || uid === BigInt(process.getuid());
19
+ const diagnostic = (code) => Object.freeze({ code });
20
+ const sortIds = (ids) => Object.freeze([...new Set(ids)].sort());
21
+ const treeSafe = async (root, path, maxEntries, maxDepth, maxBytes) => {
22
+ const pending = [{ path: root, depth: 0 }];
23
+ let entries = 0;
24
+ let bytes = 0;
25
+ while (pending.length > 0) {
26
+ const current = pending.pop();
27
+ let info;
28
+ try {
29
+ info = await lstat(current.path, { bigint: true });
30
+ }
31
+ catch {
32
+ return false;
33
+ }
34
+ if (!owned(info.uid))
35
+ return false;
36
+ if (info.isSymbolicLink())
37
+ continue;
38
+ if (!info.isDirectory()) {
39
+ if (!info.isFile())
40
+ return false;
41
+ bytes += Number(info.size);
42
+ if (bytes > maxBytes)
43
+ return false;
44
+ continue;
45
+ }
46
+ if (current.depth > maxDepth)
47
+ return false;
48
+ let names;
49
+ try {
50
+ names = await opendir(current.path).then(async (handle) => {
51
+ const result = [];
52
+ try {
53
+ for (;;) {
54
+ const entry = await handle.read();
55
+ if (entry === null)
56
+ break;
57
+ result.push(entry.name);
58
+ entries += 1;
59
+ if (entries > maxEntries)
60
+ return [];
61
+ }
62
+ return result;
63
+ }
64
+ finally {
65
+ await handle.close().catch(() => undefined);
66
+ }
67
+ });
68
+ }
69
+ catch {
70
+ return false;
71
+ }
72
+ if (entries > maxEntries)
73
+ return false;
74
+ for (const name of names)
75
+ pending.push({ path: path.join(current.path, name), depth: current.depth + 1 });
76
+ }
77
+ return true;
78
+ };
79
+ const exactNames = async (directory, expected) => {
80
+ try {
81
+ const actual = await readdir(directory);
82
+ return JSON.stringify([...actual].sort()) === JSON.stringify([...expected].sort());
83
+ }
84
+ catch {
85
+ return false;
86
+ }
87
+ };
88
+ const readCandidate = async (rootPath, rootId, path, maxEntries, maxDepth, maxBytes) => {
89
+ let info;
90
+ try {
91
+ info = await lstat(rootPath, { bigint: true });
92
+ }
93
+ catch {
94
+ return null;
95
+ }
96
+ if (!info.isDirectory() || info.isSymbolicLink() || !owned(info.uid))
97
+ return null;
98
+ const manifestPath = path.join(rootPath, MANIFEST);
99
+ const manifestInfo = await lstat(manifestPath, { bigint: true }).catch(() => null);
100
+ if (manifestInfo === null || !manifestInfo.isFile() || manifestInfo.isSymbolicLink()
101
+ || !owned(manifestInfo.uid) || manifestInfo.size > BigInt(MAX_MANIFEST_BYTES))
102
+ return null;
103
+ let raw;
104
+ try {
105
+ raw = await readFile(manifestPath, "utf8");
106
+ }
107
+ catch {
108
+ return null;
109
+ }
110
+ if (Buffer.byteLength(raw, "utf8") > MAX_MANIFEST_BYTES)
111
+ return null;
112
+ let manifest;
113
+ try {
114
+ manifest = parseProjectSkillRuntimeRootManifest(JSON.parse(raw));
115
+ }
116
+ catch {
117
+ return null;
118
+ }
119
+ if (manifest.rootId !== rootId
120
+ || manifest.directoryIdentity.dev !== String(info.dev)
121
+ || manifest.directoryIdentity.ino !== String(info.ino)
122
+ || manifest.directoryIdentity.birthtimeNs !== String(info.birthtimeNs))
123
+ return null;
124
+ const runtime = path.join(rootPath, "runtime");
125
+ const claude = path.join(runtime, ".claude");
126
+ const skills = path.join(claude, "skills");
127
+ const structural = await Promise.all([runtime, claude, skills].map(async (directory) => {
128
+ const directoryInfo = await lstat(directory, { bigint: true }).catch(() => null);
129
+ return directoryInfo !== null && directoryInfo.isDirectory() && !directoryInfo.isSymbolicLink();
130
+ }));
131
+ if (structural.some((value) => !value)
132
+ || !(await exactNames(rootPath, [MANIFEST, "runtime"]))
133
+ || !(await exactNames(runtime, [".claude"]))
134
+ || !(await exactNames(claude, ["skills"])))
135
+ return null;
136
+ const materialized = manifest.projectionModes.filter(({ mode }) => mode !== "missing");
137
+ if (!(await exactNames(skills, materialized.map(({ skillName }) => skillName))))
138
+ return null;
139
+ for (const projection of manifest.projectionModes) {
140
+ const child = path.join(skills, projection.skillName);
141
+ const childInfo = await lstat(child, { bigint: true }).catch(() => null);
142
+ if (projection.mode === "missing") {
143
+ if (childInfo !== null)
144
+ return null;
145
+ continue;
146
+ }
147
+ if (childInfo === null || !owned(childInfo.uid))
148
+ return null;
149
+ if ((projection.mode === "symlink" || projection.mode === "junction") !== childInfo.isSymbolicLink()) {
150
+ return null;
151
+ }
152
+ }
153
+ if (!(await treeSafe(rootPath, path, maxEntries, maxDepth, maxBytes)))
154
+ return null;
155
+ return Object.freeze({
156
+ id: rootId,
157
+ path: rootPath,
158
+ record: Object.freeze({
159
+ rootId: manifest.rootId,
160
+ publishedSequence: manifest.publishedSequence,
161
+ materializationRevision: manifest.materializationRevision,
162
+ directoryIdentity: manifest.directoryIdentity,
163
+ bindingDigest: manifest.bindingDigest,
164
+ resolutionDigest: manifest.resolutionDigest,
165
+ }),
166
+ identity: Object.freeze({ dev: info.dev, ino: info.ino, birthtimeNs: info.birthtimeNs }),
167
+ raw,
168
+ });
169
+ };
170
+ export function createProjectSkillRuntimeRootGc(options = {}) {
171
+ const platform = options.platform ?? process.platform;
172
+ const path = platform === "win32" ? win32 : posix;
173
+ const syncDirectory = options.directorySync ?? (platform === "win32" ? async () => undefined : durableDirectorySync);
174
+ const claimRename = options.rename ?? rename;
175
+ const removeClaim = options.remove ?? rm;
176
+ const rootStore = options.runtimeRoots ?? options.rootStore ?? createProjectSkillRuntimeRootStore({ platform });
177
+ const randomId = options.randomId ?? randomUUID;
178
+ const maxScanEntries = options.maximumScanEntries ?? MAX_SCAN_ENTRIES;
179
+ const maxTreeEntries = options.maximumTreeEntries ?? MAX_TREE_ENTRIES;
180
+ const maxTreeDepth = options.maximumTreeDepth ?? MAX_TREE_DEPTH;
181
+ const maxTreeBytes = options.maximumTreeBytes ?? MAX_TREE_BYTES;
182
+ const maxDeleteCount = options.maximumDeleteCount ?? MAX_DELETE_COUNT;
183
+ const collect = async (agentRoot, input) => {
184
+ const retained = new Set(input.leasedRootIds);
185
+ if (input.currentRootId !== null)
186
+ retained.add(input.currentRootId);
187
+ const removed = new Set();
188
+ const diagnostics = [];
189
+ const rootsDirectory = projectSkillRuntimeRootDirectory(agentRoot, platform);
190
+ let handle;
191
+ try {
192
+ handle = await opendir(rootsDirectory);
193
+ }
194
+ catch (error) {
195
+ if (isMissing(error))
196
+ return Object.freeze({ retained: sortIds(retained), removed: [], diagnostics: [] });
197
+ return Object.freeze({ retained: sortIds(retained), removed: [], diagnostics: [diagnostic("skill_projection_gc_deferred")] });
198
+ }
199
+ const names = [];
200
+ try {
201
+ for (;;) {
202
+ const entry = await handle.read();
203
+ if (entry === null)
204
+ break;
205
+ names.push(entry.name);
206
+ if (names.length > maxScanEntries) {
207
+ diagnostics.push(diagnostic("skill_projection_gc_deferred"));
208
+ return Object.freeze({ retained: sortIds(retained), removed: [], diagnostics: Object.freeze(diagnostics) });
209
+ }
210
+ }
211
+ }
212
+ finally {
213
+ await handle.close().catch(() => undefined);
214
+ }
215
+ const candidates = [];
216
+ for (const name of names) {
217
+ if (QUARANTINE.test(name))
218
+ continue;
219
+ if (!isCanonicalProjectSkillRuntimeRootId(name)) {
220
+ diagnostics.push(diagnostic("skill_projection_runtime_root_unmanaged"));
221
+ continue;
222
+ }
223
+ const candidate = await readCandidate(path.join(rootsDirectory, name), name, path, maxTreeEntries, maxTreeDepth, maxTreeBytes);
224
+ if (candidate === null) {
225
+ diagnostics.push(diagnostic("skill_projection_runtime_root_unmanaged"));
226
+ continue;
227
+ }
228
+ candidates.push(candidate);
229
+ }
230
+ candidates.sort((a, b) => a.record.publishedSequence - b.record.publishedSequence || a.id.localeCompare(b.id));
231
+ const unleasedHistory = candidates.filter(({ id }) => !retained.has(id));
232
+ for (const candidate of unleasedHistory.slice(-input.keepHistory))
233
+ retained.add(candidate.id);
234
+ const deleteCandidates = unleasedHistory.slice(0, Math.min(maxDeleteCount, Math.max(0, unleasedHistory.length - input.keepHistory)));
235
+ if (unleasedHistory.length - input.keepHistory > maxDeleteCount) {
236
+ diagnostics.push(diagnostic("skill_projection_gc_deferred"));
237
+ for (const candidate of unleasedHistory.slice(maxDeleteCount, unleasedHistory.length - input.keepHistory)) {
238
+ retained.add(candidate.id);
239
+ }
240
+ }
241
+ if (deleteCandidates.length === 0)
242
+ return Object.freeze({ retained: sortIds(retained), removed: [], diagnostics: Object.freeze(diagnostics) });
243
+ if (rootStore !== undefined) {
244
+ try {
245
+ await (rootStore.checkpointNextSequenceForGc ?? rootStore.checkpointNextSequence)(agentRoot);
246
+ }
247
+ catch {
248
+ diagnostics.push(diagnostic("skill_projection_gc_deferred"));
249
+ for (const candidate of deleteCandidates)
250
+ retained.add(candidate.id);
251
+ return Object.freeze({ retained: sortIds(retained), removed: [], diagnostics: Object.freeze(diagnostics) });
252
+ }
253
+ }
254
+ for (const candidate of deleteCandidates) {
255
+ if (retained.has(candidate.id))
256
+ continue;
257
+ const claim = path.join(rootsDirectory, `.gc-${randomId()}`);
258
+ if (!QUARANTINE.test(path.basename(claim))) {
259
+ diagnostics.push(diagnostic("skill_projection_gc_deferred"));
260
+ retained.add(candidate.id);
261
+ continue;
262
+ }
263
+ try {
264
+ await options.beforeClaim?.(candidate.path, claim);
265
+ await claimRename(candidate.path, claim);
266
+ await syncDirectory(rootsDirectory);
267
+ const claimed = await lstat(claim, { bigint: true });
268
+ if (!claimed.isDirectory() || claimed.isSymbolicLink() || !owned(claimed.uid)
269
+ || claimed.dev !== candidate.identity.dev || claimed.ino !== candidate.identity.ino
270
+ || claimed.birthtimeNs !== candidate.identity.birthtimeNs) {
271
+ retained.add(candidate.id);
272
+ continue;
273
+ }
274
+ const raw = await readFile(path.join(claim, MANIFEST), "utf8");
275
+ if (raw !== candidate.raw) {
276
+ retained.add(candidate.id);
277
+ continue;
278
+ }
279
+ await chmod(claim, 0o700).catch(() => undefined);
280
+ await removeClaim(claim, { recursive: true, force: true });
281
+ await syncDirectory(rootsDirectory);
282
+ removed.add(candidate.id);
283
+ }
284
+ catch (error) {
285
+ if (!isMissing(error))
286
+ diagnostics.push(diagnostic("skill_projection_gc_deferred"));
287
+ retained.add(candidate.id);
288
+ }
289
+ }
290
+ return Object.freeze({ retained: sortIds(retained), removed: sortIds(removed), diagnostics: Object.freeze(diagnostics) });
291
+ };
292
+ return Object.freeze({ collect });
293
+ }
@@ -0,0 +1,46 @@
1
+ import { z } from "zod";
2
+ import { ProjectSkillRuntimeStoreError, parseProjectSkillRuntimeLeaseRecord, } from "./runtime-root-domain.js";
3
+ const LEASE_ARTIFACT_MANAGED_BY = "nowcrew-project-skill-runtime-lease-artifact";
4
+ const DECIMAL_BIGINT = /^(?:0|[1-9][0-9]*)$/u;
5
+ const ArtifactIdentitySchema = z.object({
6
+ dev: z.string().regex(DECIMAL_BIGINT),
7
+ ino: z.string().regex(DECIMAL_BIGINT),
8
+ birthtimeNs: z.string().regex(DECIMAL_BIGINT),
9
+ }).strict();
10
+ const LeaseArtifactSchema = z.object({
11
+ managedBy: z.literal(LEASE_ARTIFACT_MANAGED_BY),
12
+ version: z.literal(1),
13
+ record: z.unknown(),
14
+ fileIdentity: ArtifactIdentitySchema,
15
+ }).strict();
16
+ const invalidArtifact = () => {
17
+ throw new ProjectSkillRuntimeStoreError("skill_projection_snapshot_corrupt");
18
+ };
19
+ const immutableIdentity = (identity) => Object.freeze({ ...identity });
20
+ export function createProjectSkillRuntimeLeaseArtifact(input) {
21
+ return parseProjectSkillRuntimeLeaseArtifact({
22
+ managedBy: LEASE_ARTIFACT_MANAGED_BY,
23
+ version: 1,
24
+ ...input,
25
+ });
26
+ }
27
+ export function parseProjectSkillRuntimeLeaseArtifact(candidate) {
28
+ const parsed = LeaseArtifactSchema.safeParse(candidate);
29
+ if (!parsed.success)
30
+ return invalidArtifact();
31
+ const record = parseProjectSkillRuntimeLeaseRecord(parsed.data.record);
32
+ return Object.freeze({
33
+ managedBy: LEASE_ARTIFACT_MANAGED_BY,
34
+ version: 1,
35
+ record,
36
+ fileIdentity: immutableIdentity(parsed.data.fileIdentity),
37
+ });
38
+ }
39
+ export const leaseArtifactIdentityFromStat = (identity) => Object.freeze({
40
+ dev: String(identity.dev),
41
+ ino: String(identity.ino),
42
+ birthtimeNs: String(identity.birthtimeNs),
43
+ });
44
+ export const matchesLeaseArtifactIdentity = (expected, actual) => expected.dev === String(actual.dev)
45
+ && expected.ino === String(actual.ino)
46
+ && expected.birthtimeNs === String(actual.birthtimeNs);