@nowcrew/daemon 0.6.19 → 0.6.21
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/atomic-no-replace-rename.js +91 -0
- package/dist/completion-retransmitter-logging.js +16 -0
- package/dist/completion-retransmitter.js +39 -4
- package/dist/control-plane-url.js +4 -2
- package/dist/directory-projection-publication.js +105 -0
- package/dist/directory-projection.js +20 -4
- package/dist/execution-journal.js +40 -4
- package/dist/execution-posix-stop-proof.js +82 -0
- package/dist/execution-runner.js +68 -8
- package/dist/local-executor.js +67 -52
- package/dist/machine-info.js +8 -5
- package/dist/project-skills/capability.js +109 -0
- package/dist/project-skills/controller-convergence.js +57 -0
- package/dist/project-skills/controller.js +80 -24
- package/dist/project-skills/initialized-reconciler.js +4 -4
- package/dist/project-skills/projection-state-domain.js +19 -2
- package/dist/project-skills/projection-state-store.js +3 -2
- package/dist/project-skills/projection-state-transaction.js +5 -1
- package/dist/project-skills/projection-state.js +1 -1
- package/dist/project-skills/reconciler.js +275 -102
- package/dist/project-skills/runtime-launch.js +102 -0
- package/dist/project-skills/runtime-root-bootstrap.js +47 -0
- package/dist/project-skills/runtime-root-domain.js +268 -0
- package/dist/project-skills/runtime-root-gc.js +293 -0
- package/dist/project-skills/runtime-root-lease-artifact.js +46 -0
- package/dist/project-skills/runtime-root-leases.js +487 -0
- package/dist/project-skills/runtime-root-source-identity.js +60 -0
- package/dist/project-skills/runtime-root-startup.js +49 -0
- package/dist/project-skills/runtime-root-state-artifact-domain.js +143 -0
- package/dist/project-skills/runtime-root-state-index.js +356 -0
- package/dist/project-skills/runtime-root-store.js +722 -0
- package/dist/project-skills/serve-capability.js +28 -0
- package/dist/project-skills/serve-startup.js +22 -0
- package/dist/project-skills/types.js +1 -0
- package/dist/runtimes/codex-home-migration-cli.js +26 -0
- package/dist/runtimes/codex-home-migration.js +112 -0
- package/dist/runtimes/codex-home.js +200 -17
- package/dist/serve.js +60 -79
- package/dist/supervised-runtime.js +1 -5
- package/package.json +2 -1
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { compareProjectSkillRefs, isProjectId, isProjectSkillName, MAX_AGENT_PROJECT_SKILL_BINDINGS, MAX_PROJECT_ID_LENGTH, MAX_PROJECT_SKILL_NAME_LENGTH, } from "./types.js";
|
|
4
|
+
const RUNTIME_ROOT_MANAGED_BY = "nowcrew-project-skill-runtime-root";
|
|
5
|
+
const RUNTIME_STORE_MANAGED_BY = "nowcrew-project-skill-runtime-store";
|
|
6
|
+
const RUNTIME_LEASE_MANAGED_BY = "nowcrew-project-skill-runtime-lease";
|
|
7
|
+
const DIGEST = /^sha256:[a-f0-9]{64}$/u;
|
|
8
|
+
const DECIMAL_BIGINT = /^(?:0|[1-9][0-9]*)$/u;
|
|
9
|
+
const PATH_FREE_IDENTITY = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/u;
|
|
10
|
+
const CANONICAL_ROOT_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
|
|
11
|
+
const MAX_LOCAL_IDENTITY_LENGTH = 512;
|
|
12
|
+
const MAX_PUBLISHED_SEQUENCE = Number.MAX_SAFE_INTEGER - 1;
|
|
13
|
+
/** Deployment algorithm/layout revision. This is deliberately not a source-content hash. */
|
|
14
|
+
export const PROJECT_SKILL_RUNTIME_MATERIALIZATION_REVISION = 1;
|
|
15
|
+
export class ProjectSkillRuntimeStoreError extends Error {
|
|
16
|
+
code;
|
|
17
|
+
constructor(code) {
|
|
18
|
+
super(code);
|
|
19
|
+
this.code = code;
|
|
20
|
+
this.name = "ProjectSkillRuntimeStoreError";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const corrupt = () => {
|
|
24
|
+
throw new ProjectSkillRuntimeStoreError("skill_projection_snapshot_corrupt");
|
|
25
|
+
};
|
|
26
|
+
const unmanaged = () => {
|
|
27
|
+
throw new ProjectSkillRuntimeStoreError("skill_projection_runtime_root_unmanaged");
|
|
28
|
+
};
|
|
29
|
+
const SafeSequenceSchema = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
|
|
30
|
+
const PublishedSequenceSchema = SafeSequenceSchema.max(MAX_PUBLISHED_SEQUENCE);
|
|
31
|
+
const PositiveMaterializationRevisionSchema = z.number().int().positive().max(Number.MAX_SAFE_INTEGER);
|
|
32
|
+
export const isCanonicalProjectSkillRuntimeRootId = (value) => CANONICAL_ROOT_ID.test(value);
|
|
33
|
+
const RootIdSchema = z.string().refine(isCanonicalProjectSkillRuntimeRootId);
|
|
34
|
+
const DigestSchema = z.string().regex(DIGEST);
|
|
35
|
+
const DirectoryIdentitySchema = z.object({
|
|
36
|
+
dev: z.string().regex(DECIMAL_BIGINT),
|
|
37
|
+
ino: z.string().regex(DECIMAL_BIGINT),
|
|
38
|
+
birthtimeNs: z.string().regex(DECIMAL_BIGINT),
|
|
39
|
+
}).strict();
|
|
40
|
+
const ProjectionModeSchema = z.object({
|
|
41
|
+
projectId: z.string().min(1).max(MAX_PROJECT_ID_LENGTH),
|
|
42
|
+
skillName: z.string().min(1).max(MAX_PROJECT_SKILL_NAME_LENGTH),
|
|
43
|
+
mode: z.enum(["symlink", "junction", "copy", "missing"]),
|
|
44
|
+
}).strict();
|
|
45
|
+
const RuntimeRootManifestSchema = z.object({
|
|
46
|
+
managedBy: z.literal(RUNTIME_ROOT_MANAGED_BY),
|
|
47
|
+
version: z.literal(1),
|
|
48
|
+
rootId: RootIdSchema,
|
|
49
|
+
publishedSequence: PublishedSequenceSchema,
|
|
50
|
+
materializationRevision: PositiveMaterializationRevisionSchema,
|
|
51
|
+
directoryIdentity: DirectoryIdentitySchema,
|
|
52
|
+
bindingDigest: DigestSchema,
|
|
53
|
+
resolutionDigest: DigestSchema,
|
|
54
|
+
projectionModes: z.array(ProjectionModeSchema).max(MAX_AGENT_PROJECT_SKILL_BINDINGS),
|
|
55
|
+
}).strict();
|
|
56
|
+
const RuntimeRootManifestInputSchema = RuntimeRootManifestSchema
|
|
57
|
+
.omit({ managedBy: true, version: true, materializationRevision: true })
|
|
58
|
+
.partial({ publishedSequence: true, directoryIdentity: true });
|
|
59
|
+
const RuntimeRootRecordSchema = z.object({
|
|
60
|
+
rootId: RootIdSchema,
|
|
61
|
+
publishedSequence: PublishedSequenceSchema,
|
|
62
|
+
materializationRevision: PositiveMaterializationRevisionSchema,
|
|
63
|
+
directoryIdentity: DirectoryIdentitySchema,
|
|
64
|
+
bindingDigest: DigestSchema,
|
|
65
|
+
resolutionDigest: DigestSchema,
|
|
66
|
+
}).strict();
|
|
67
|
+
const RuntimeStoreStateSchema = z.object({
|
|
68
|
+
managedBy: z.literal(RUNTIME_STORE_MANAGED_BY),
|
|
69
|
+
version: z.literal(1),
|
|
70
|
+
nextSequence: SafeSequenceSchema,
|
|
71
|
+
roots: z.array(RuntimeRootRecordSchema),
|
|
72
|
+
}).strict();
|
|
73
|
+
const RuntimeStoreStateInputSchema = RuntimeStoreStateSchema.omit({ managedBy: true, version: true });
|
|
74
|
+
const PathFreeIdentitySchema = z.string()
|
|
75
|
+
.min(1)
|
|
76
|
+
.max(MAX_LOCAL_IDENTITY_LENGTH)
|
|
77
|
+
.regex(PATH_FREE_IDENTITY);
|
|
78
|
+
const RuntimeLeaseRecordSchema = z.object({
|
|
79
|
+
managedBy: z.literal(RUNTIME_LEASE_MANAGED_BY),
|
|
80
|
+
version: z.literal(1),
|
|
81
|
+
executionId: PathFreeIdentitySchema,
|
|
82
|
+
rootId: RootIdSchema,
|
|
83
|
+
nonce: PathFreeIdentitySchema,
|
|
84
|
+
}).strict();
|
|
85
|
+
const RuntimeLeaseRecordInputSchema = RuntimeLeaseRecordSchema.omit({ managedBy: true, version: true });
|
|
86
|
+
const immutableDirectoryIdentity = (identity) => Object.freeze({
|
|
87
|
+
dev: identity.dev,
|
|
88
|
+
ino: identity.ino,
|
|
89
|
+
birthtimeNs: identity.birthtimeNs,
|
|
90
|
+
});
|
|
91
|
+
const sameProjectionRef = (left, right) => left.projectId === right.projectId && left.skillName === right.skillName;
|
|
92
|
+
const immutableProjectionMode = (projection) => Object.freeze({
|
|
93
|
+
projectId: projection.projectId,
|
|
94
|
+
skillName: projection.skillName,
|
|
95
|
+
mode: projection.mode,
|
|
96
|
+
});
|
|
97
|
+
const canonicalProjectionModes = (modes) => {
|
|
98
|
+
const seen = new Set();
|
|
99
|
+
const canonical = modes.map((mode) => {
|
|
100
|
+
if (!isProjectId(mode.projectId) || !isProjectSkillName(mode.skillName))
|
|
101
|
+
return corrupt();
|
|
102
|
+
const key = `${mode.projectId}\0${mode.skillName}`;
|
|
103
|
+
if (seen.has(key))
|
|
104
|
+
return corrupt();
|
|
105
|
+
seen.add(key);
|
|
106
|
+
return immutableProjectionMode(mode);
|
|
107
|
+
}).sort(compareProjectSkillRefs);
|
|
108
|
+
return Object.freeze(canonical);
|
|
109
|
+
};
|
|
110
|
+
const requireCanonicalProjectionModes = (modes) => {
|
|
111
|
+
const canonical = canonicalProjectionModes(modes);
|
|
112
|
+
if (canonical.some((mode, index) => {
|
|
113
|
+
const input = modes[index];
|
|
114
|
+
return input === undefined || !sameProjectionRef(mode, input) || mode.mode !== input.mode;
|
|
115
|
+
}))
|
|
116
|
+
return corrupt();
|
|
117
|
+
return canonical;
|
|
118
|
+
};
|
|
119
|
+
const immutableRootRecord = (record) => Object.freeze({
|
|
120
|
+
rootId: record.rootId,
|
|
121
|
+
publishedSequence: record.publishedSequence,
|
|
122
|
+
materializationRevision: record.materializationRevision,
|
|
123
|
+
directoryIdentity: immutableDirectoryIdentity(record.directoryIdentity),
|
|
124
|
+
bindingDigest: record.bindingDigest,
|
|
125
|
+
resolutionDigest: record.resolutionDigest,
|
|
126
|
+
});
|
|
127
|
+
const compareRootRecords = (left, right) => left.publishedSequence - right.publishedSequence
|
|
128
|
+
|| (left.rootId < right.rootId ? -1 : left.rootId > right.rootId ? 1 : 0);
|
|
129
|
+
const canonicalRootRecords = (records) => {
|
|
130
|
+
const rootIds = new Set();
|
|
131
|
+
const sequences = new Set();
|
|
132
|
+
const canonical = records.map((record) => {
|
|
133
|
+
const parsed = RuntimeRootRecordSchema.safeParse(record);
|
|
134
|
+
if (!parsed.success)
|
|
135
|
+
return corrupt();
|
|
136
|
+
if (rootIds.has(parsed.data.rootId) || sequences.has(parsed.data.publishedSequence))
|
|
137
|
+
return corrupt();
|
|
138
|
+
rootIds.add(parsed.data.rootId);
|
|
139
|
+
sequences.add(parsed.data.publishedSequence);
|
|
140
|
+
return immutableRootRecord(parsed.data);
|
|
141
|
+
}).sort(compareRootRecords);
|
|
142
|
+
return Object.freeze(canonical);
|
|
143
|
+
};
|
|
144
|
+
const requireCanonicalRootRecords = (records) => {
|
|
145
|
+
const canonical = canonicalRootRecords(records);
|
|
146
|
+
if (canonical.some((record, index) => {
|
|
147
|
+
const input = records[index];
|
|
148
|
+
return input === undefined
|
|
149
|
+
|| record.rootId !== input.rootId
|
|
150
|
+
|| record.publishedSequence !== input.publishedSequence;
|
|
151
|
+
}))
|
|
152
|
+
return corrupt();
|
|
153
|
+
return canonical;
|
|
154
|
+
};
|
|
155
|
+
const requireNextSequence = (nextSequence, roots) => {
|
|
156
|
+
const latest = roots.at(-1);
|
|
157
|
+
if (latest !== undefined && latest.publishedSequence >= nextSequence)
|
|
158
|
+
corrupt();
|
|
159
|
+
};
|
|
160
|
+
const rejectUnknownOwner = (candidate, expected) => {
|
|
161
|
+
if (candidate && typeof candidate === "object" && "managedBy" in candidate
|
|
162
|
+
&& candidate.managedBy !== expected)
|
|
163
|
+
unmanaged();
|
|
164
|
+
};
|
|
165
|
+
export function createProjectSkillRuntimeRootManifest(input) {
|
|
166
|
+
const parsedInput = RuntimeRootManifestInputSchema.safeParse(input);
|
|
167
|
+
if (!parsedInput.success)
|
|
168
|
+
return corrupt();
|
|
169
|
+
const candidate = RuntimeRootManifestSchema.safeParse({
|
|
170
|
+
managedBy: RUNTIME_ROOT_MANAGED_BY,
|
|
171
|
+
version: 1,
|
|
172
|
+
rootId: parsedInput.data.rootId,
|
|
173
|
+
publishedSequence: parsedInput.data.publishedSequence ?? 0,
|
|
174
|
+
materializationRevision: PROJECT_SKILL_RUNTIME_MATERIALIZATION_REVISION,
|
|
175
|
+
directoryIdentity: parsedInput.data.directoryIdentity ?? { dev: "0", ino: "0", birthtimeNs: "0" },
|
|
176
|
+
bindingDigest: parsedInput.data.bindingDigest,
|
|
177
|
+
resolutionDigest: parsedInput.data.resolutionDigest,
|
|
178
|
+
projectionModes: parsedInput.data.projectionModes,
|
|
179
|
+
});
|
|
180
|
+
if (!candidate.success)
|
|
181
|
+
return corrupt();
|
|
182
|
+
return Object.freeze({
|
|
183
|
+
...candidate.data,
|
|
184
|
+
directoryIdentity: immutableDirectoryIdentity(candidate.data.directoryIdentity),
|
|
185
|
+
projectionModes: canonicalProjectionModes(candidate.data.projectionModes),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
export function parseProjectSkillRuntimeRootManifest(candidate) {
|
|
189
|
+
rejectUnknownOwner(candidate, RUNTIME_ROOT_MANAGED_BY);
|
|
190
|
+
const parsed = RuntimeRootManifestSchema.safeParse(candidate);
|
|
191
|
+
if (!parsed.success)
|
|
192
|
+
return corrupt();
|
|
193
|
+
return Object.freeze({
|
|
194
|
+
...parsed.data,
|
|
195
|
+
directoryIdentity: immutableDirectoryIdentity(parsed.data.directoryIdentity),
|
|
196
|
+
projectionModes: requireCanonicalProjectionModes(parsed.data.projectionModes),
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
export function createProjectSkillRuntimeRootRecord(input) {
|
|
200
|
+
const parsed = RuntimeRootRecordSchema.safeParse(input);
|
|
201
|
+
if (!parsed.success)
|
|
202
|
+
return corrupt();
|
|
203
|
+
return immutableRootRecord(parsed.data);
|
|
204
|
+
}
|
|
205
|
+
export function parseProjectSkillRuntimeRootRecord(candidate) {
|
|
206
|
+
const parsed = RuntimeRootRecordSchema.safeParse(candidate);
|
|
207
|
+
if (!parsed.success)
|
|
208
|
+
return corrupt();
|
|
209
|
+
return immutableRootRecord(parsed.data);
|
|
210
|
+
}
|
|
211
|
+
export function createProjectSkillRuntimeStoreState(input) {
|
|
212
|
+
const parsedInput = RuntimeStoreStateInputSchema.safeParse(input);
|
|
213
|
+
if (!parsedInput.success)
|
|
214
|
+
return corrupt();
|
|
215
|
+
const parsed = RuntimeStoreStateSchema.safeParse({
|
|
216
|
+
managedBy: RUNTIME_STORE_MANAGED_BY,
|
|
217
|
+
version: 1,
|
|
218
|
+
nextSequence: parsedInput.data.nextSequence,
|
|
219
|
+
roots: parsedInput.data.roots,
|
|
220
|
+
});
|
|
221
|
+
if (!parsed.success)
|
|
222
|
+
return corrupt();
|
|
223
|
+
const roots = canonicalRootRecords(parsed.data.roots);
|
|
224
|
+
requireNextSequence(parsed.data.nextSequence, roots);
|
|
225
|
+
return Object.freeze({
|
|
226
|
+
managedBy: RUNTIME_STORE_MANAGED_BY,
|
|
227
|
+
version: 1,
|
|
228
|
+
nextSequence: parsed.data.nextSequence,
|
|
229
|
+
roots,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
export function parseProjectSkillRuntimeStoreState(candidate) {
|
|
233
|
+
rejectUnknownOwner(candidate, RUNTIME_STORE_MANAGED_BY);
|
|
234
|
+
const parsed = RuntimeStoreStateSchema.safeParse(candidate);
|
|
235
|
+
if (!parsed.success)
|
|
236
|
+
return corrupt();
|
|
237
|
+
const roots = requireCanonicalRootRecords(parsed.data.roots);
|
|
238
|
+
requireNextSequence(parsed.data.nextSequence, roots);
|
|
239
|
+
return Object.freeze({
|
|
240
|
+
managedBy: RUNTIME_STORE_MANAGED_BY,
|
|
241
|
+
version: 1,
|
|
242
|
+
nextSequence: parsed.data.nextSequence,
|
|
243
|
+
roots,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
export function createProjectSkillRuntimeLeaseRecord(input) {
|
|
247
|
+
const parsedInput = RuntimeLeaseRecordInputSchema.safeParse(input);
|
|
248
|
+
if (!parsedInput.success)
|
|
249
|
+
return corrupt();
|
|
250
|
+
const parsed = RuntimeLeaseRecordSchema.safeParse({
|
|
251
|
+
managedBy: RUNTIME_LEASE_MANAGED_BY,
|
|
252
|
+
version: 1,
|
|
253
|
+
...parsedInput.data,
|
|
254
|
+
});
|
|
255
|
+
if (!parsed.success)
|
|
256
|
+
return corrupt();
|
|
257
|
+
return Object.freeze(parsed.data);
|
|
258
|
+
}
|
|
259
|
+
export function parseProjectSkillRuntimeLeaseRecord(candidate) {
|
|
260
|
+
rejectUnknownOwner(candidate, RUNTIME_LEASE_MANAGED_BY);
|
|
261
|
+
const parsed = RuntimeLeaseRecordSchema.safeParse(candidate);
|
|
262
|
+
if (!parsed.success)
|
|
263
|
+
return corrupt();
|
|
264
|
+
return Object.freeze(parsed.data);
|
|
265
|
+
}
|
|
266
|
+
export function projectSkillRuntimeLeaseKey(executionId) {
|
|
267
|
+
return createHash("sha256").update(executionId, "utf8").digest("hex");
|
|
268
|
+
}
|
|
@@ -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);
|