@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,487 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { chmod, lstat, mkdir, open, opendir, unlink } from "node:fs/promises";
|
|
3
|
+
import { posix, win32 } from "node:path";
|
|
4
|
+
import { durableDirectorySync } from "../atomic-private-write.js";
|
|
5
|
+
import { AtomicNoReplaceRenameError, atomicRenameNoReplace, } from "../atomic-no-replace-rename.js";
|
|
6
|
+
import { createProjectSkillRuntimeLeaseArtifact, leaseArtifactIdentityFromStat, matchesLeaseArtifactIdentity, parseProjectSkillRuntimeLeaseArtifact, } from "./runtime-root-lease-artifact.js";
|
|
7
|
+
import { ProjectSkillRuntimeStoreError, createProjectSkillRuntimeLeaseRecord, projectSkillRuntimeLeaseKey, } from "./runtime-root-domain.js";
|
|
8
|
+
const STORE_DIRECTORY_NAME = "project-skill-runtime";
|
|
9
|
+
const LEASES_DIRECTORY_NAME = "leases";
|
|
10
|
+
const MAX_LEASE_BYTES = 64 * 1024;
|
|
11
|
+
const MAX_LEASE_ENTRIES = 2_048;
|
|
12
|
+
const FINAL_LEASE_NAME = /^([a-f0-9]{64})\.json$/u;
|
|
13
|
+
const STAGING_LEASE_NAME = /^\.lease-private-([a-f0-9]{64})\.json$/u;
|
|
14
|
+
const CLAIM_LEASE_NAME = /^\.lease-claim-([a-f0-9]{64})\.json$/u;
|
|
15
|
+
const DISCARD_STAGE_NAME = /^\.lease-discard-stage-([a-f0-9]{64})\.json$/u;
|
|
16
|
+
const DISCARD_RELEASE_NAME = /^\.lease-discard-release-([a-f0-9]{64})\.json$/u;
|
|
17
|
+
const leaseOperationTails = new Map();
|
|
18
|
+
const codeOf = (error) => error.code;
|
|
19
|
+
const fail = (code = "skill_projection_runtime_root_unmanaged") => {
|
|
20
|
+
throw new ProjectSkillRuntimeStoreError(code);
|
|
21
|
+
};
|
|
22
|
+
const mapLeaseError = (error) => {
|
|
23
|
+
if (error instanceof ProjectSkillRuntimeStoreError)
|
|
24
|
+
throw error;
|
|
25
|
+
if (error instanceof AtomicNoReplaceRenameError)
|
|
26
|
+
return fail("skill_projection_failed");
|
|
27
|
+
return fail("skill_projection_failed");
|
|
28
|
+
};
|
|
29
|
+
const ownedByCurrentUser = (uid) => process.getuid === undefined || uid === BigInt(process.getuid());
|
|
30
|
+
const identityFromStat = (identity) => Object.freeze({
|
|
31
|
+
dev: identity.dev,
|
|
32
|
+
ino: identity.ino,
|
|
33
|
+
birthtimeNs: identity.birthtimeNs,
|
|
34
|
+
size: identity.size,
|
|
35
|
+
mtimeNs: identity.mtimeNs,
|
|
36
|
+
});
|
|
37
|
+
const sameIdentity = (left, right) => left.dev === right.dev
|
|
38
|
+
&& left.ino === right.ino
|
|
39
|
+
&& left.birthtimeNs === right.birthtimeNs
|
|
40
|
+
&& left.size === right.size
|
|
41
|
+
&& left.mtimeNs === right.mtimeNs;
|
|
42
|
+
const sameLease = (left, right) => left.executionId === right.executionId
|
|
43
|
+
&& left.rootId === right.rootId
|
|
44
|
+
&& left.nonce === right.nonce;
|
|
45
|
+
const sameSnapshot = (left, right) => sameIdentity(left.identity, right.identity)
|
|
46
|
+
&& left.raw === right.raw
|
|
47
|
+
&& sameLease(left.record, right.record);
|
|
48
|
+
const withLeaseOperation = async (key, operation) => {
|
|
49
|
+
const previous = leaseOperationTails.get(key) ?? Promise.resolve();
|
|
50
|
+
let release;
|
|
51
|
+
const turn = new Promise((resolve) => { release = resolve; });
|
|
52
|
+
const tail = previous.then(() => turn);
|
|
53
|
+
leaseOperationTails.set(key, tail);
|
|
54
|
+
await previous;
|
|
55
|
+
try {
|
|
56
|
+
return await operation();
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
release();
|
|
60
|
+
if (leaseOperationTails.get(key) === tail)
|
|
61
|
+
leaseOperationTails.delete(key);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
const requireOwnedDirectory = async (directory) => {
|
|
65
|
+
const info = await lstat(directory, { bigint: true }).catch(() => fail());
|
|
66
|
+
if (!info.isDirectory() || info.isSymbolicLink() || !ownedByCurrentUser(info.uid))
|
|
67
|
+
return fail();
|
|
68
|
+
};
|
|
69
|
+
const ensurePrivateDirectory = async (directory, parent, syncDirectory) => {
|
|
70
|
+
let created = false;
|
|
71
|
+
try {
|
|
72
|
+
await mkdir(directory, { mode: 0o700 });
|
|
73
|
+
created = true;
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (codeOf(error) !== "EEXIST")
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
await requireOwnedDirectory(directory);
|
|
80
|
+
await chmod(directory, 0o700).catch(() => fail());
|
|
81
|
+
if (created)
|
|
82
|
+
await syncDirectory(parent);
|
|
83
|
+
};
|
|
84
|
+
const boundedNames = async (directory, maximumEntries) => {
|
|
85
|
+
const handle = await opendir(directory).catch(() => fail());
|
|
86
|
+
const names = [];
|
|
87
|
+
try {
|
|
88
|
+
for (;;) {
|
|
89
|
+
const entry = await handle.read();
|
|
90
|
+
if (entry === null)
|
|
91
|
+
break;
|
|
92
|
+
names.push(entry.name);
|
|
93
|
+
if (names.length > maximumEntries) {
|
|
94
|
+
return fail("skill_projection_runtime_store_limit_exceeded");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
await handle.close().catch(() => undefined);
|
|
100
|
+
}
|
|
101
|
+
return Object.freeze(names.sort());
|
|
102
|
+
};
|
|
103
|
+
const readBoundedLease = async (handle) => {
|
|
104
|
+
const buffer = Buffer.allocUnsafe(MAX_LEASE_BYTES + 1);
|
|
105
|
+
let offset = 0;
|
|
106
|
+
while (offset < buffer.length) {
|
|
107
|
+
const { bytesRead } = await handle.read(buffer, offset, buffer.length - offset, offset);
|
|
108
|
+
if (bytesRead === 0)
|
|
109
|
+
break;
|
|
110
|
+
offset += bytesRead;
|
|
111
|
+
}
|
|
112
|
+
if (offset > MAX_LEASE_BYTES)
|
|
113
|
+
return fail("skill_projection_runtime_store_limit_exceeded");
|
|
114
|
+
return buffer.subarray(0, offset).toString("utf8");
|
|
115
|
+
};
|
|
116
|
+
const artifactName = (location, record) => {
|
|
117
|
+
const executionKey = projectSkillRuntimeLeaseKey(record.executionId);
|
|
118
|
+
const stagingKey = projectSkillRuntimeLeaseKey(record.nonce);
|
|
119
|
+
if (location === "final")
|
|
120
|
+
return `${executionKey}.json`;
|
|
121
|
+
if (location === "staging")
|
|
122
|
+
return `.lease-private-${stagingKey}.json`;
|
|
123
|
+
if (location === "claim")
|
|
124
|
+
return `.lease-claim-${executionKey}.json`;
|
|
125
|
+
if (location === "discard-stage")
|
|
126
|
+
return `.lease-discard-stage-${stagingKey}.json`;
|
|
127
|
+
return `.lease-discard-release-${executionKey}.json`;
|
|
128
|
+
};
|
|
129
|
+
const classifyName = (name) => {
|
|
130
|
+
const candidates = [
|
|
131
|
+
[FINAL_LEASE_NAME, "final"],
|
|
132
|
+
[STAGING_LEASE_NAME, "staging"],
|
|
133
|
+
[CLAIM_LEASE_NAME, "claim"],
|
|
134
|
+
[DISCARD_STAGE_NAME, "discard-stage"],
|
|
135
|
+
[DISCARD_RELEASE_NAME, "discard-release"],
|
|
136
|
+
];
|
|
137
|
+
for (const [pattern, location] of candidates) {
|
|
138
|
+
const match = pattern.exec(name);
|
|
139
|
+
if (match !== null)
|
|
140
|
+
return Object.freeze({ location, token: match[1] });
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
};
|
|
144
|
+
const readLeasePath = async (filePath, name, location, token) => {
|
|
145
|
+
let before;
|
|
146
|
+
try {
|
|
147
|
+
before = await lstat(filePath, { bigint: true });
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
if (codeOf(error) === "ENOENT")
|
|
151
|
+
return null;
|
|
152
|
+
return fail();
|
|
153
|
+
}
|
|
154
|
+
if (!before.isFile() || before.isSymbolicLink() || !ownedByCurrentUser(before.uid)
|
|
155
|
+
|| before.size > BigInt(MAX_LEASE_BYTES))
|
|
156
|
+
return fail();
|
|
157
|
+
const handle = await open(filePath, "r").catch(() => fail());
|
|
158
|
+
try {
|
|
159
|
+
const opened = await handle.stat({ bigint: true });
|
|
160
|
+
if (!opened.isFile() || opened.isSymbolicLink() || !ownedByCurrentUser(opened.uid)
|
|
161
|
+
|| opened.dev !== before.dev || opened.ino !== before.ino
|
|
162
|
+
|| opened.birthtimeNs !== before.birthtimeNs || opened.size !== before.size)
|
|
163
|
+
return fail();
|
|
164
|
+
const raw = await readBoundedLease(handle);
|
|
165
|
+
const after = await handle.stat({ bigint: true });
|
|
166
|
+
const pathAfter = await lstat(filePath, { bigint: true }).catch(() => fail());
|
|
167
|
+
if (Buffer.byteLength(raw, "utf8") !== Number(after.size)
|
|
168
|
+
|| after.dev !== opened.dev || after.ino !== opened.ino
|
|
169
|
+
|| after.birthtimeNs !== opened.birthtimeNs || after.size !== opened.size
|
|
170
|
+
|| after.mtimeNs !== opened.mtimeNs
|
|
171
|
+
|| pathAfter.dev !== after.dev || pathAfter.ino !== after.ino
|
|
172
|
+
|| pathAfter.birthtimeNs !== after.birthtimeNs || pathAfter.size !== after.size
|
|
173
|
+
|| pathAfter.mtimeNs !== after.mtimeNs)
|
|
174
|
+
return fail();
|
|
175
|
+
let candidate;
|
|
176
|
+
try {
|
|
177
|
+
candidate = JSON.parse(raw);
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return fail();
|
|
181
|
+
}
|
|
182
|
+
let artifact;
|
|
183
|
+
try {
|
|
184
|
+
artifact = parseProjectSkillRuntimeLeaseArtifact(candidate);
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return fail();
|
|
188
|
+
}
|
|
189
|
+
if (!matchesLeaseArtifactIdentity(artifact.fileIdentity, after))
|
|
190
|
+
return fail();
|
|
191
|
+
const record = artifact.record;
|
|
192
|
+
const expectedToken = location === "staging" || location === "discard-stage"
|
|
193
|
+
? projectSkillRuntimeLeaseKey(record.nonce)
|
|
194
|
+
: projectSkillRuntimeLeaseKey(record.executionId);
|
|
195
|
+
if (token !== expectedToken || name !== artifactName(location, record))
|
|
196
|
+
return fail();
|
|
197
|
+
return Object.freeze({
|
|
198
|
+
location,
|
|
199
|
+
name,
|
|
200
|
+
path: filePath,
|
|
201
|
+
token,
|
|
202
|
+
identity: identityFromStat(after),
|
|
203
|
+
raw,
|
|
204
|
+
artifact,
|
|
205
|
+
record,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
finally {
|
|
209
|
+
await handle.close().catch(() => undefined);
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
const scanLeases = async (context) => {
|
|
213
|
+
const snapshots = [];
|
|
214
|
+
const executionIds = new Set();
|
|
215
|
+
for (const name of await boundedNames(context.leasesDirectory, context.maximumEntries)) {
|
|
216
|
+
const classified = classifyName(name);
|
|
217
|
+
if (classified === null)
|
|
218
|
+
return fail();
|
|
219
|
+
const snapshot = await readLeasePath(context.path.join(context.leasesDirectory, name), name, classified.location, classified.token);
|
|
220
|
+
if (snapshot === null || executionIds.has(snapshot.record.executionId))
|
|
221
|
+
return fail();
|
|
222
|
+
executionIds.add(snapshot.record.executionId);
|
|
223
|
+
snapshots.push(snapshot);
|
|
224
|
+
}
|
|
225
|
+
return Object.freeze(snapshots.sort((left, right) => left.record.executionId.localeCompare(right.record.executionId)));
|
|
226
|
+
};
|
|
227
|
+
const revalidateSnapshot = async (expected) => {
|
|
228
|
+
const current = await readLeasePath(expected.path, expected.name, expected.location, expected.token);
|
|
229
|
+
if (current === null || !sameSnapshot(current, expected))
|
|
230
|
+
return fail();
|
|
231
|
+
return current;
|
|
232
|
+
};
|
|
233
|
+
export function createProjectSkillRuntimeLeaseStore(options = {}) {
|
|
234
|
+
const platform = options.platform ?? process.platform;
|
|
235
|
+
const path = platform === "win32" ? win32 : posix;
|
|
236
|
+
const syncDirectory = platform === "win32"
|
|
237
|
+
? options.directorySync ?? (async () => undefined)
|
|
238
|
+
: options.directorySync ?? durableDirectorySync;
|
|
239
|
+
const atomicRename = options.atomicRename ?? atomicRenameNoReplace;
|
|
240
|
+
const randomId = options.randomId ?? randomUUID;
|
|
241
|
+
const maximumEntries = options.maximumEntries ?? MAX_LEASE_ENTRIES;
|
|
242
|
+
if (!Number.isInteger(maximumEntries)
|
|
243
|
+
|| maximumEntries <= 0
|
|
244
|
+
|| maximumEntries > MAX_LEASE_ENTRIES) {
|
|
245
|
+
return fail("skill_projection_runtime_store_limit_exceeded");
|
|
246
|
+
}
|
|
247
|
+
const contextFor = async (agentRoot) => {
|
|
248
|
+
await requireOwnedDirectory(agentRoot);
|
|
249
|
+
const crewDirectory = path.join(agentRoot, ".crew");
|
|
250
|
+
const storeDirectory = path.join(crewDirectory, STORE_DIRECTORY_NAME);
|
|
251
|
+
const leasesDirectory = path.join(storeDirectory, LEASES_DIRECTORY_NAME);
|
|
252
|
+
await ensurePrivateDirectory(crewDirectory, agentRoot, syncDirectory);
|
|
253
|
+
await ensurePrivateDirectory(storeDirectory, crewDirectory, syncDirectory);
|
|
254
|
+
await ensurePrivateDirectory(leasesDirectory, storeDirectory, syncDirectory);
|
|
255
|
+
return Object.freeze({ leasesDirectory, maximumEntries, path, syncDirectory });
|
|
256
|
+
};
|
|
257
|
+
const moveSnapshot = async (context, expected, destinationLocation) => {
|
|
258
|
+
await revalidateSnapshot(expected);
|
|
259
|
+
const destinationName = artifactName(destinationLocation, expected.record);
|
|
260
|
+
const destinationPath = context.path.join(context.leasesDirectory, destinationName);
|
|
261
|
+
const moved = await atomicRename(expected.path, destinationPath);
|
|
262
|
+
if (moved !== "published")
|
|
263
|
+
return fail();
|
|
264
|
+
const classified = classifyName(destinationName);
|
|
265
|
+
const after = await readLeasePath(destinationPath, destinationName, destinationLocation, classified.token);
|
|
266
|
+
if (after === null || !sameSnapshot(after, expected))
|
|
267
|
+
return fail();
|
|
268
|
+
return after;
|
|
269
|
+
};
|
|
270
|
+
const deleteStaging = async (context, expected) => {
|
|
271
|
+
const discard = expected.location === "discard-stage"
|
|
272
|
+
? await revalidateSnapshot(expected)
|
|
273
|
+
: await moveSnapshot(context, expected, "discard-stage");
|
|
274
|
+
await revalidateSnapshot(discard);
|
|
275
|
+
await unlink(discard.path);
|
|
276
|
+
await context.syncDirectory(context.leasesDirectory);
|
|
277
|
+
};
|
|
278
|
+
const deleteReleaseArtifact = async (context, expected, hookContext) => {
|
|
279
|
+
const discard = expected.location === "discard-release"
|
|
280
|
+
? await revalidateSnapshot(expected)
|
|
281
|
+
: await moveSnapshot(context, expected, "discard-release");
|
|
282
|
+
if (hookContext !== undefined) {
|
|
283
|
+
await options.afterLeaseDeleteRenamedBeforeParentSync?.(hookContext);
|
|
284
|
+
}
|
|
285
|
+
await revalidateSnapshot(discard);
|
|
286
|
+
await unlink(discard.path);
|
|
287
|
+
if (hookContext !== undefined)
|
|
288
|
+
await options.afterLeaseDeletedBeforeParentSync?.(hookContext);
|
|
289
|
+
await context.syncDirectory(context.leasesDirectory);
|
|
290
|
+
};
|
|
291
|
+
const restoreReleaseArtifact = async (context, expected) => {
|
|
292
|
+
const restored = await moveSnapshot(context, expected, "final");
|
|
293
|
+
await context.syncDirectory(context.leasesDirectory);
|
|
294
|
+
await revalidateSnapshot(restored);
|
|
295
|
+
};
|
|
296
|
+
const releaseSnapshot = async (context, expected) => {
|
|
297
|
+
const snapshots = await scanLeases(context);
|
|
298
|
+
const current = snapshots.find(({ record }) => record.executionId === expected.executionId);
|
|
299
|
+
if (current === undefined || !sameLease(current.record, expected))
|
|
300
|
+
return;
|
|
301
|
+
if (current.location === "claim" || current.location === "discard-release") {
|
|
302
|
+
await deleteReleaseArtifact(context, current);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (current.location !== "final")
|
|
306
|
+
return fail();
|
|
307
|
+
const claimName = artifactName("claim", expected);
|
|
308
|
+
const claimPath = context.path.join(context.leasesDirectory, claimName);
|
|
309
|
+
const hookContext = Object.freeze({
|
|
310
|
+
finalPath: current.path,
|
|
311
|
+
claimPath,
|
|
312
|
+
executionId: expected.executionId,
|
|
313
|
+
rootId: expected.rootId,
|
|
314
|
+
});
|
|
315
|
+
const claim = await moveSnapshot(context, current, "claim");
|
|
316
|
+
await options.afterLeaseClaimRenamedBeforeParentSync?.(hookContext);
|
|
317
|
+
await context.syncDirectory(context.leasesDirectory);
|
|
318
|
+
await revalidateSnapshot(claim);
|
|
319
|
+
await options.afterLeaseClaimDurableBeforeDelete?.(hookContext);
|
|
320
|
+
await deleteReleaseArtifact(context, claim, hookContext);
|
|
321
|
+
};
|
|
322
|
+
const ownedLease = (agentRoot, record) => Object.freeze({
|
|
323
|
+
executionId: record.executionId,
|
|
324
|
+
rootId: record.rootId,
|
|
325
|
+
release: async () => {
|
|
326
|
+
try {
|
|
327
|
+
const context = await contextFor(agentRoot);
|
|
328
|
+
await withLeaseOperation(path.resolve(context.leasesDirectory), () => releaseSnapshot(context, record));
|
|
329
|
+
}
|
|
330
|
+
catch (error) {
|
|
331
|
+
return mapLeaseError(error);
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
const list = async (agentRoot) => {
|
|
336
|
+
try {
|
|
337
|
+
const context = await contextFor(agentRoot);
|
|
338
|
+
return await withLeaseOperation(path.resolve(context.leasesDirectory), async () => {
|
|
339
|
+
const snapshots = await scanLeases(context);
|
|
340
|
+
if (snapshots.some(({ location }) => location !== "final"))
|
|
341
|
+
return fail();
|
|
342
|
+
return Object.freeze(snapshots.map(({ record }) => record));
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
return mapLeaseError(error);
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
return Object.freeze({
|
|
350
|
+
acquire: async (agentRoot, input) => {
|
|
351
|
+
try {
|
|
352
|
+
createProjectSkillRuntimeLeaseRecord({ ...input, nonce: "pending" });
|
|
353
|
+
}
|
|
354
|
+
catch (error) {
|
|
355
|
+
return mapLeaseError(error);
|
|
356
|
+
}
|
|
357
|
+
try {
|
|
358
|
+
const context = await contextFor(agentRoot);
|
|
359
|
+
return await withLeaseOperation(path.resolve(context.leasesDirectory), async () => {
|
|
360
|
+
const snapshots = await scanLeases(context);
|
|
361
|
+
if (snapshots.some(({ location }) => location !== "final"))
|
|
362
|
+
return fail();
|
|
363
|
+
const existing = snapshots
|
|
364
|
+
.find(({ record }) => record.executionId === input.executionId);
|
|
365
|
+
if (existing !== undefined) {
|
|
366
|
+
if (existing.record.rootId !== input.rootId)
|
|
367
|
+
return fail();
|
|
368
|
+
return ownedLease(agentRoot, existing.record);
|
|
369
|
+
}
|
|
370
|
+
if (snapshots.length >= context.maximumEntries) {
|
|
371
|
+
return fail("skill_projection_runtime_store_limit_exceeded");
|
|
372
|
+
}
|
|
373
|
+
const desired = createProjectSkillRuntimeLeaseRecord({ ...input, nonce: randomId() });
|
|
374
|
+
const finalName = artifactName("final", desired);
|
|
375
|
+
const stagingName = artifactName("staging", desired);
|
|
376
|
+
const finalPath = context.path.join(context.leasesDirectory, finalName);
|
|
377
|
+
const stagingPath = context.path.join(context.leasesDirectory, stagingName);
|
|
378
|
+
const classified = classifyName(stagingName);
|
|
379
|
+
const hookContext = Object.freeze({
|
|
380
|
+
stagingPath,
|
|
381
|
+
finalPath,
|
|
382
|
+
executionId: desired.executionId,
|
|
383
|
+
rootId: desired.rootId,
|
|
384
|
+
});
|
|
385
|
+
let handle = null;
|
|
386
|
+
try {
|
|
387
|
+
handle = await open(stagingPath, "wx", 0o600);
|
|
388
|
+
const created = await handle.stat({ bigint: true });
|
|
389
|
+
if (!created.isFile() || created.isSymbolicLink() || !ownedByCurrentUser(created.uid)
|
|
390
|
+
|| created.size !== 0n)
|
|
391
|
+
return fail();
|
|
392
|
+
const artifact = createProjectSkillRuntimeLeaseArtifact({
|
|
393
|
+
record: desired,
|
|
394
|
+
fileIdentity: leaseArtifactIdentityFromStat(created),
|
|
395
|
+
});
|
|
396
|
+
const raw = `${JSON.stringify(artifact)}\n`;
|
|
397
|
+
if (Buffer.byteLength(raw, "utf8") > MAX_LEASE_BYTES) {
|
|
398
|
+
return fail("skill_projection_runtime_store_limit_exceeded");
|
|
399
|
+
}
|
|
400
|
+
await handle.writeFile(raw, "utf8");
|
|
401
|
+
await handle.sync();
|
|
402
|
+
const staged = await handle.stat({ bigint: true });
|
|
403
|
+
if (!staged.isFile() || staged.isSymbolicLink() || !ownedByCurrentUser(staged.uid)
|
|
404
|
+
|| !matchesLeaseArtifactIdentity(artifact.fileIdentity, staged)
|
|
405
|
+
|| staged.size !== BigInt(Buffer.byteLength(raw, "utf8")))
|
|
406
|
+
return fail();
|
|
407
|
+
const expected = Object.freeze({
|
|
408
|
+
location: "staging",
|
|
409
|
+
name: stagingName,
|
|
410
|
+
path: stagingPath,
|
|
411
|
+
token: classified.token,
|
|
412
|
+
identity: identityFromStat(staged),
|
|
413
|
+
raw,
|
|
414
|
+
artifact,
|
|
415
|
+
record: desired,
|
|
416
|
+
});
|
|
417
|
+
await options.afterLeaseFileSynced?.(hookContext);
|
|
418
|
+
await handle.close();
|
|
419
|
+
handle = null;
|
|
420
|
+
await revalidateSnapshot(expected);
|
|
421
|
+
await context.syncDirectory(context.leasesDirectory);
|
|
422
|
+
await options.afterLeaseStagingDurableBeforeRename?.(hookContext);
|
|
423
|
+
const durableStaging = await revalidateSnapshot(expected);
|
|
424
|
+
const published = await moveSnapshot(context, durableStaging, "final");
|
|
425
|
+
await options.afterLeaseRenamedBeforeParentSync?.(hookContext);
|
|
426
|
+
await context.syncDirectory(context.leasesDirectory);
|
|
427
|
+
await revalidateSnapshot(published);
|
|
428
|
+
return ownedLease(agentRoot, desired);
|
|
429
|
+
}
|
|
430
|
+
finally {
|
|
431
|
+
await handle?.close().catch(() => undefined);
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
catch (error) {
|
|
436
|
+
return mapLeaseError(error);
|
|
437
|
+
}
|
|
438
|
+
},
|
|
439
|
+
list,
|
|
440
|
+
protectedExecutionIds: async (agentRoot) => {
|
|
441
|
+
try {
|
|
442
|
+
const context = await contextFor(agentRoot);
|
|
443
|
+
return await withLeaseOperation(path.resolve(context.leasesDirectory), async () => {
|
|
444
|
+
const snapshots = await scanLeases(context);
|
|
445
|
+
return Object.freeze(snapshots
|
|
446
|
+
.filter(({ location }) => location === "final"
|
|
447
|
+
|| location === "claim"
|
|
448
|
+
|| location === "discard-release")
|
|
449
|
+
.map(({ record }) => record.executionId));
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
catch (error) {
|
|
453
|
+
return mapLeaseError(error);
|
|
454
|
+
}
|
|
455
|
+
},
|
|
456
|
+
recover: async (agentRoot, canRelease) => {
|
|
457
|
+
try {
|
|
458
|
+
const context = await contextFor(agentRoot);
|
|
459
|
+
await withLeaseOperation(path.resolve(context.leasesDirectory), async () => {
|
|
460
|
+
const snapshots = await scanLeases(context);
|
|
461
|
+
for (const snapshot of snapshots) {
|
|
462
|
+
if (snapshot.location === "staging" || snapshot.location === "discard-stage") {
|
|
463
|
+
await deleteStaging(context, snapshot);
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
const terminal = await canRelease(snapshot.record.executionId);
|
|
467
|
+
if (snapshot.location === "final") {
|
|
468
|
+
if (terminal)
|
|
469
|
+
await releaseSnapshot(context, snapshot.record);
|
|
470
|
+
}
|
|
471
|
+
else if (terminal) {
|
|
472
|
+
await deleteReleaseArtifact(context, snapshot);
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
await restoreReleaseArtifact(context, snapshot);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
// Also makes an earlier deletion durable when a crash occurred after unlink but before sync.
|
|
479
|
+
await context.syncDirectory(context.leasesDirectory);
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
catch (error) {
|
|
483
|
+
return mapLeaseError(error);
|
|
484
|
+
}
|
|
485
|
+
},
|
|
486
|
+
});
|
|
487
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { lstat, open } from "node:fs/promises";
|
|
2
|
+
import { ProjectSkillRuntimeStoreError } from "./runtime-root-domain.js";
|
|
3
|
+
const fail = () => {
|
|
4
|
+
throw new ProjectSkillRuntimeStoreError("skill_projection_failed");
|
|
5
|
+
};
|
|
6
|
+
const exactEntryIdentity = (info, kind) => Object.freeze({
|
|
7
|
+
kind,
|
|
8
|
+
dev: info.dev,
|
|
9
|
+
ino: info.ino,
|
|
10
|
+
uid: info.uid,
|
|
11
|
+
mode: info.mode,
|
|
12
|
+
birthtimeNs: info.birthtimeNs,
|
|
13
|
+
size: info.size,
|
|
14
|
+
mtimeNs: info.mtimeNs,
|
|
15
|
+
ctimeNs: info.ctimeNs,
|
|
16
|
+
});
|
|
17
|
+
const sameEntryIdentity = (left, right) => left.kind === right.kind
|
|
18
|
+
&& left.dev === right.dev
|
|
19
|
+
&& left.ino === right.ino
|
|
20
|
+
&& left.uid === right.uid
|
|
21
|
+
&& left.mode === right.mode
|
|
22
|
+
&& left.birthtimeNs === right.birthtimeNs
|
|
23
|
+
&& left.size === right.size
|
|
24
|
+
&& left.mtimeNs === right.mtimeNs
|
|
25
|
+
&& left.ctimeNs === right.ctimeNs;
|
|
26
|
+
export const sameResolvedProjectSkillSourceIdentity = (left, right) => sameEntryIdentity(left.directory, right.directory)
|
|
27
|
+
&& sameEntryIdentity(left.skillFile, right.skillFile);
|
|
28
|
+
export const captureResolvedProjectSkillSourceIdentity = async (sourcePath, path) => {
|
|
29
|
+
let skillHandle = null;
|
|
30
|
+
try {
|
|
31
|
+
const skillPath = path.join(sourcePath, "SKILL.md");
|
|
32
|
+
const sourceBefore = await lstat(sourcePath, { bigint: true });
|
|
33
|
+
const skillBefore = await lstat(skillPath, { bigint: true });
|
|
34
|
+
if (!sourceBefore.isDirectory() || sourceBefore.isSymbolicLink()
|
|
35
|
+
|| !skillBefore.isFile() || skillBefore.isSymbolicLink())
|
|
36
|
+
return fail();
|
|
37
|
+
const directoryIdentity = exactEntryIdentity(sourceBefore, "directory");
|
|
38
|
+
const skillIdentity = exactEntryIdentity(skillBefore, "file");
|
|
39
|
+
skillHandle = await open(skillPath, "r");
|
|
40
|
+
const openedSkill = await skillHandle.stat({ bigint: true });
|
|
41
|
+
const sourceAfter = await lstat(sourcePath, { bigint: true });
|
|
42
|
+
const skillAfter = await lstat(skillPath, { bigint: true });
|
|
43
|
+
if (!openedSkill.isFile()
|
|
44
|
+
|| !sourceAfter.isDirectory() || sourceAfter.isSymbolicLink()
|
|
45
|
+
|| !skillAfter.isFile() || skillAfter.isSymbolicLink()
|
|
46
|
+
|| !sameEntryIdentity(directoryIdentity, exactEntryIdentity(sourceAfter, "directory"))
|
|
47
|
+
|| !sameEntryIdentity(skillIdentity, exactEntryIdentity(openedSkill, "file"))
|
|
48
|
+
|| !sameEntryIdentity(skillIdentity, exactEntryIdentity(skillAfter, "file")))
|
|
49
|
+
return fail();
|
|
50
|
+
return Object.freeze({ directory: directoryIdentity, skillFile: skillIdentity });
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (error instanceof ProjectSkillRuntimeStoreError)
|
|
54
|
+
throw error;
|
|
55
|
+
return fail();
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
await skillHandle?.close().catch(() => undefined);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { mkdir, readdir } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { readAppliedProjectSkillManifest } from "./projection-state.js";
|
|
4
|
+
import { initializeAfterProjectSkillLeaseProtection, recoverProjectSkillRuntimeRoots, } from "./runtime-root-bootstrap.js";
|
|
5
|
+
import { createProjectSkillRuntimeRootGc } from "./runtime-root-gc.js";
|
|
6
|
+
import { createProjectSkillRuntimeLeaseStore } from "./runtime-root-leases.js";
|
|
7
|
+
import { createProjectSkillRuntimeRootStore } from "./runtime-root-store.js";
|
|
8
|
+
export function createProjectSkillRuntimeRootStartup() {
|
|
9
|
+
const roots = createProjectSkillRuntimeRootStore();
|
|
10
|
+
const leases = createProjectSkillRuntimeLeaseStore();
|
|
11
|
+
const gc = createProjectSkillRuntimeRootGc({ runtimeRoots: roots });
|
|
12
|
+
const agentRoots = async (agentsRoot) => {
|
|
13
|
+
await mkdir(agentsRoot, { recursive: true, mode: 0o700 });
|
|
14
|
+
return (await readdir(agentsRoot, { withFileTypes: true }))
|
|
15
|
+
.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."))
|
|
16
|
+
.map((entry) => join(agentsRoot, entry.name));
|
|
17
|
+
};
|
|
18
|
+
return Object.freeze({
|
|
19
|
+
probeNativePublicationPrimitiveReadiness: () => roots.probeNativePublicationPrimitiveReadiness(),
|
|
20
|
+
initializeAfterLeaseProtection: async (agentsRoot, initialize) => initializeAfterProjectSkillLeaseProtection({
|
|
21
|
+
agentRoots: await agentRoots(agentsRoot),
|
|
22
|
+
leases,
|
|
23
|
+
initialize,
|
|
24
|
+
}),
|
|
25
|
+
recover: async (agentsRoot, journal, protectedExecutionIds) => {
|
|
26
|
+
await recoverProjectSkillRuntimeRoots({
|
|
27
|
+
agentRoots: await agentRoots(agentsRoot),
|
|
28
|
+
journal,
|
|
29
|
+
roots,
|
|
30
|
+
leases,
|
|
31
|
+
gc,
|
|
32
|
+
currentRootId: async (agentRoot) => (await readAppliedProjectSkillManifest(agentRoot))?.rootId ?? null,
|
|
33
|
+
afterLeaseRecovery: async () => {
|
|
34
|
+
const remaining = await initializeAfterProjectSkillLeaseProtection({
|
|
35
|
+
agentRoots: await agentRoots(agentsRoot),
|
|
36
|
+
leases,
|
|
37
|
+
initialize: async (snapshot) => snapshot,
|
|
38
|
+
});
|
|
39
|
+
protectedExecutionIds.clear();
|
|
40
|
+
for (const executionId of remaining)
|
|
41
|
+
protectedExecutionIds.add(executionId);
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
export async function recoverProjectSkillRuntimeRootsAtStartup(agentsRoot, journal) {
|
|
48
|
+
await createProjectSkillRuntimeRootStartup().recover(agentsRoot, journal, new Set());
|
|
49
|
+
}
|