@nowcrew/daemon 0.6.89 → 0.6.91-beta.1
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/README.md +64 -3
- package/dist/agent-ability/materializer.js +21 -10
- package/dist/agent-environment-contract.js +55 -0
- package/dist/agent-environment-control.js +24 -0
- package/dist/agent-environment-execution.js +98 -0
- package/dist/agent-environments.js +275 -0
- package/dist/automatic-daemon-installer.js +49 -1
- package/dist/computer-main.js +18 -0
- package/dist/computer-profile.js +17 -4
- package/dist/computer-setup-client.js +318 -0
- package/dist/computer-setup-orchestration.js +203 -0
- package/dist/computer-setup.js +244 -0
- package/dist/config.js +30 -12
- package/dist/console-formatter.js +3 -3
- package/dist/console.js +15 -2
- package/dist/daemon-update-eligibility.js +1 -1
- package/dist/daemon-updater.js +2 -12
- package/dist/execution-journal.js +9 -6
- package/dist/execution-protocol.js +1 -0
- package/dist/execution-runner.js +13 -8
- package/dist/i18n.js +19 -0
- package/dist/list-models.js +12 -12
- package/dist/local-execution-lease.js +22 -0
- package/dist/local-executor.js +57 -84
- package/dist/local-memory/candidate.js +43 -0
- package/dist/local-memory/index-budget.js +8 -0
- package/dist/local-memory/inventory-cli.js +61 -0
- package/dist/local-memory/inventory-preview.js +159 -0
- package/dist/local-memory/inventory-source.js +159 -0
- package/dist/local-memory/pending-match.js +29 -0
- package/dist/local-memory/pending-query.js +100 -0
- package/dist/local-memory/pending-schema.js +29 -0
- package/dist/local-memory/pending-source.js +126 -0
- package/dist/local-memory/pending.js +34 -84
- package/dist/local-memory/prompt.js +19 -12
- package/dist/local-memory/read-cli.js +97 -12
- package/dist/local-memory/rollout.js +82 -0
- package/dist/local-memory/runtime.js +23 -17
- package/dist/local-memory/work-log-execution.js +31 -0
- package/dist/local-memory/work-log.js +58 -0
- package/dist/local-memory/write.js +27 -58
- package/dist/local-runtime-environment.js +50 -0
- package/dist/machine-environment-cache.js +147 -0
- package/dist/machine-environment-contract.js +10 -0
- package/dist/machine-environment-control.js +27 -0
- package/dist/machine-environment-policy.js +75 -0
- package/dist/machine-environment-process.js +110 -0
- package/dist/machine-info.js +70 -25
- package/dist/machine-runtime-environment.js +189 -0
- package/dist/machine-shell-environment-capture.js +135 -0
- package/dist/machine-windows-environment-capture.js +215 -0
- package/dist/machine-windows-environment-script.js +158 -0
- package/dist/main.js +4 -69
- package/dist/marketplace-command.js +21 -0
- package/dist/marketplace-controller.js +80 -0
- package/dist/marketplace-protocol.js +38 -0
- package/dist/marketplace-remote.js +48 -0
- package/dist/marketplace-runner.js +235 -0
- package/dist/marketplace-serve.js +41 -0
- package/dist/marketplace-source.js +103 -0
- package/dist/message-file-preview.js +246 -0
- package/dist/pre-admission-rejection.js +64 -0
- package/dist/project-skills/execution-adapter.js +3 -2
- package/dist/project-skills/runtime-launch.js +14 -0
- package/dist/provider-env.js +26 -15
- package/dist/runner.js +17 -4
- package/dist/runtime-environment-merge.js +16 -0
- package/dist/runtime-health-handler.js +10 -0
- package/dist/runtime-health.js +3 -1
- package/dist/runtime-path.js +22 -12
- package/dist/runtime-probe.js +35 -6
- package/dist/runtimes/acp-probe-transport.js +69 -0
- package/dist/runtimes/claude-models.js +20 -9
- package/dist/runtimes/deepseek-harness-launch.js +10 -0
- package/dist/runtimes/deepseek-harness-profile.js +92 -0
- package/dist/runtimes/deepseek-harness-runner.js +226 -0
- package/dist/runtimes/deepseek-harness.js +76 -15
- package/dist/runtimes/harness-probe-retry.js +42 -0
- package/dist/runtimes/hermes-models.js +2 -1
- package/dist/runtimes/kimi-acp-runner.js +78 -13
- package/dist/runtimes/opencode.js +2 -2
- package/dist/serve.js +88 -178
- package/dist/storage-identity.js +0 -3
- package/dist/storage-serve-adapter.js +6 -72
- package/dist/supervised-runtime.js +17 -2
- package/dist/token.js +0 -1
- package/dist/workspace-control-handler.js +27 -0
- package/dist/workspace-fs.js +66 -15
- package/dist/workspace.js +1 -1
- package/package.json +13 -12
- package/dist/codex-home-launch.js +0 -7
- package/dist/codex-home-mode-controller.js +0 -68
- package/dist/daemon-migration-controller.js +0 -211
- package/dist/daemon-migration-wiring.js +0 -22
- package/dist/runtimes/codex-home-migration-cli.js +0 -26
- package/dist/runtimes/codex-home-migration.js +0 -112
- package/dist/runtimes/codex-home.js +0 -236
- package/dist/storage-control.js +0 -212
- package/dist/storage-copy.js +0 -628
- package/dist/storage-credential-guard.js +0 -21
- package/dist/storage-doctor.js +0 -228
- package/dist/storage-manifest.js +0 -1027
- package/dist/storage-migration-controller.js +0 -440
- package/dist/storage-migration-manifest.js +0 -105
- package/dist/storage-migration-policy.js +0 -100
- package/dist/storage-runtime-controller.js +0 -381
- package/dist/storage-runtime-readiness.js +0 -241
- package/dist/storage-safe-fs.js +0 -302
- package/dist/storage-status-delivery.js +0 -224
- package/dist/storage-status-replay.js +0 -140
- package/dist/storage-target-confirmation.js +0 -129
- package/dist/storage-worker-command.js +0 -37
- package/dist/storage-worker-executor.js +0 -447
- package/dist/storage-worker-handoff.js +0 -151
- package/dist/storage-worker-operation.js +0 -294
- package/dist/storage-worker-status.js +0 -259
package/dist/storage-safe-fs.js
DELETED
|
@@ -1,302 +0,0 @@
|
|
|
1
|
-
import { createRequire } from "node:module";
|
|
2
|
-
import { closeSync, constants, fchmodSync, fstatSync, openSync, readdirSync, realpathSync, } from "node:fs";
|
|
3
|
-
import { open } from "node:fs/promises";
|
|
4
|
-
import { basename, dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
5
|
-
const AT_REMOVEDIR = process.platform === "darwin" ? 0x80 : 0x200;
|
|
6
|
-
const DIRECTORY_FLAGS = constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW;
|
|
7
|
-
const ENTRY_FLAGS = constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK;
|
|
8
|
-
const DARWIN_TRANSIENT_MKDIR_ERRNO = 316;
|
|
9
|
-
const MAX_TRANSIENT_MKDIR_RETRIES = 3;
|
|
10
|
-
let cachedApi = null;
|
|
11
|
-
function nativeApi() {
|
|
12
|
-
if (cachedApi !== null)
|
|
13
|
-
return cachedApi;
|
|
14
|
-
if (process.platform !== "linux" && process.platform !== "darwin") {
|
|
15
|
-
throw new Error("anchored storage filesystem operations are unsupported on this platform");
|
|
16
|
-
}
|
|
17
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- koffi has no bundled TypeScript declarations.
|
|
18
|
-
const koffi = createRequire(import.meta.url)("koffi");
|
|
19
|
-
const library = koffi.load(process.platform === "darwin" ? "libSystem.B.dylib" : "libc.so.6");
|
|
20
|
-
cachedApi = Object.freeze({
|
|
21
|
-
openat: library.func("int openat(int dirfd, const char *pathname, int flags, ...)"),
|
|
22
|
-
mkdirat: library.func("int mkdirat(int dirfd, const char *pathname, unsigned int mode)"),
|
|
23
|
-
unlinkat: library.func("int unlinkat(int dirfd, const char *pathname, int flags)"),
|
|
24
|
-
symlinkat: library.func("int symlinkat(const char *target, int dirfd, const char *pathname)"),
|
|
25
|
-
readlinkat: library.func("long readlinkat(int dirfd, const char *pathname, void *buffer, unsigned long size)"),
|
|
26
|
-
dup: library.func("int dup(int fd)"),
|
|
27
|
-
...(process.platform === "darwin" ? {
|
|
28
|
-
getdirentries: library.func("long getdirentries(int fd, void *buffer, unsigned long size, void *basep)"),
|
|
29
|
-
} : {}),
|
|
30
|
-
errno: () => koffi.errno(),
|
|
31
|
-
errnoNames: new Map(Object.entries(koffi.os.errno).map(([name, code]) => [code, name])),
|
|
32
|
-
});
|
|
33
|
-
return cachedApi;
|
|
34
|
-
}
|
|
35
|
-
function nativeError(operation, api) {
|
|
36
|
-
const errno = api.errno();
|
|
37
|
-
return Object.assign(new Error(`${operation} failed: ${api.errnoNames.get(errno) ?? errno}`), {
|
|
38
|
-
code: api.errnoNames.get(errno) ?? "EIO",
|
|
39
|
-
errno,
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
function safeSegment(segment) {
|
|
43
|
-
if (segment.length === 0 || segment === "." || segment === ".." || /[\/\0\r\n]/u.test(segment)) {
|
|
44
|
-
throw new Error(`unsafe anchored path segment: ${segment}`);
|
|
45
|
-
}
|
|
46
|
-
return segment;
|
|
47
|
-
}
|
|
48
|
-
function absoluteSegments(path) {
|
|
49
|
-
if (!isAbsolute(path) || /[\0\r\n]/u.test(path))
|
|
50
|
-
throw new Error(`anchored path must be absolute: ${path}`);
|
|
51
|
-
const segments = resolve(path).split(sep).filter(Boolean).map(safeSegment);
|
|
52
|
-
if (segments.length === 0)
|
|
53
|
-
return segments;
|
|
54
|
-
const topLevel = `${sep}${segments[0]}`;
|
|
55
|
-
const canonicalTopLevel = realpathSync.native(topLevel);
|
|
56
|
-
return [...canonicalTopLevel.split(sep).filter(Boolean).map(safeSegment), ...segments.slice(1)];
|
|
57
|
-
}
|
|
58
|
-
function assertContained(root, target) {
|
|
59
|
-
const child = relative(resolve(root), resolve(target));
|
|
60
|
-
if (child === "" || child.startsWith(`..${sep}`) || child === ".." || isAbsolute(child)) {
|
|
61
|
-
throw new Error(`anchored storage target escapes root: ${target}`);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
function openAt(api, parentFd, name, flags, mode = 0) {
|
|
65
|
-
const fd = api.openat(parentFd, safeSegment(name), flags, "uint32_t", mode);
|
|
66
|
-
if (fd < 0)
|
|
67
|
-
throw nativeError(`openat(${name})`, api);
|
|
68
|
-
return fd;
|
|
69
|
-
}
|
|
70
|
-
export function isTransientDirectoryCreateError(error) {
|
|
71
|
-
return error?.errno === DARWIN_TRANSIENT_MKDIR_ERRNO;
|
|
72
|
-
}
|
|
73
|
-
function mkdirAt(api, parentFd, name, mode) {
|
|
74
|
-
for (let attempt = 0; attempt <= MAX_TRANSIENT_MKDIR_RETRIES; attempt += 1) {
|
|
75
|
-
if (api.mkdirat(parentFd, safeSegment(name), mode) === 0)
|
|
76
|
-
return;
|
|
77
|
-
const error = nativeError(`mkdirat(${name})`, api);
|
|
78
|
-
if (error.code === "EEXIST")
|
|
79
|
-
return;
|
|
80
|
-
if (!isTransientDirectoryCreateError(error) || attempt === MAX_TRANSIENT_MKDIR_RETRIES)
|
|
81
|
-
throw error;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
function unlinkAt(api, parentFd, name, flags) {
|
|
85
|
-
if (api.unlinkat(parentFd, safeSegment(name), flags) !== 0)
|
|
86
|
-
throw nativeError(`unlinkat(${name})`, api);
|
|
87
|
-
}
|
|
88
|
-
function openAbsoluteDirectory(path, create) {
|
|
89
|
-
const api = nativeApi();
|
|
90
|
-
let current = openSync("/", DIRECTORY_FLAGS);
|
|
91
|
-
try {
|
|
92
|
-
for (const segment of absoluteSegments(path)) {
|
|
93
|
-
if (create)
|
|
94
|
-
mkdirAt(api, current, segment, 0o700);
|
|
95
|
-
const next = openAt(api, current, segment, DIRECTORY_FLAGS);
|
|
96
|
-
closeSync(current);
|
|
97
|
-
current = next;
|
|
98
|
-
}
|
|
99
|
-
return current;
|
|
100
|
-
}
|
|
101
|
-
catch (error) {
|
|
102
|
-
closeSync(current);
|
|
103
|
-
throw error;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
function descriptorPath(fd) {
|
|
107
|
-
return process.platform === "linux" ? `/proc/self/fd/${fd}` : `/dev/fd/${fd}`;
|
|
108
|
-
}
|
|
109
|
-
async function duplicateFileHandle(fd, flags) {
|
|
110
|
-
try {
|
|
111
|
-
return await open(descriptorPath(fd), flags);
|
|
112
|
-
}
|
|
113
|
-
finally {
|
|
114
|
-
closeSync(fd);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
export async function openAnchoredFile(path, flags = constants.O_RDONLY) {
|
|
118
|
-
const api = nativeApi();
|
|
119
|
-
const parent = openAbsoluteDirectory(dirname(path), false);
|
|
120
|
-
try {
|
|
121
|
-
const fd = openAt(api, parent, basename(path), flags | constants.O_NOFOLLOW);
|
|
122
|
-
return await duplicateFileHandle(fd, flags & constants.O_WRONLY ? constants.O_WRONLY : constants.O_RDONLY);
|
|
123
|
-
}
|
|
124
|
-
finally {
|
|
125
|
-
closeSync(parent);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
export async function createAnchoredFile(root, path, mode, afterParentOpen) {
|
|
129
|
-
assertContained(root, path);
|
|
130
|
-
const api = nativeApi();
|
|
131
|
-
const parent = openAbsoluteDirectory(dirname(path), true);
|
|
132
|
-
try {
|
|
133
|
-
await afterParentOpen?.(path);
|
|
134
|
-
const fd = openAt(api, parent, basename(path), constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, mode);
|
|
135
|
-
return await duplicateFileHandle(fd, constants.O_WRONLY);
|
|
136
|
-
}
|
|
137
|
-
finally {
|
|
138
|
-
closeSync(parent);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
export function ensureAnchoredDirectory(root, path, mode = 0o700) {
|
|
142
|
-
if (resolve(root) !== resolve(path))
|
|
143
|
-
assertContained(root, path);
|
|
144
|
-
const fd = openAbsoluteDirectory(path, true);
|
|
145
|
-
try {
|
|
146
|
-
fchmodSync(fd, mode);
|
|
147
|
-
}
|
|
148
|
-
finally {
|
|
149
|
-
closeSync(fd);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
export function chmodAnchoredDirectory(root, path, mode) {
|
|
153
|
-
if (resolve(root) !== resolve(path))
|
|
154
|
-
assertContained(root, path);
|
|
155
|
-
const fd = openAbsoluteDirectory(path, false);
|
|
156
|
-
try {
|
|
157
|
-
fchmodSync(fd, mode);
|
|
158
|
-
}
|
|
159
|
-
finally {
|
|
160
|
-
closeSync(fd);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
function readlinkAt(api, parentFd, name) {
|
|
164
|
-
const buffer = Buffer.alloc(32768);
|
|
165
|
-
const size = api.readlinkat(parentFd, safeSegment(name), buffer, buffer.length);
|
|
166
|
-
if (size < 0)
|
|
167
|
-
throw nativeError(`readlinkat(${name})`, api);
|
|
168
|
-
return buffer.subarray(0, size).toString("utf8");
|
|
169
|
-
}
|
|
170
|
-
export function inspectAnchoredEntry(root, path) {
|
|
171
|
-
assertContained(root, path);
|
|
172
|
-
const api = nativeApi();
|
|
173
|
-
const parent = openAbsoluteDirectory(dirname(path), false);
|
|
174
|
-
const name = basename(path);
|
|
175
|
-
try {
|
|
176
|
-
let fd;
|
|
177
|
-
try {
|
|
178
|
-
fd = openAt(api, parent, name, ENTRY_FLAGS);
|
|
179
|
-
}
|
|
180
|
-
catch (error) {
|
|
181
|
-
if (error.code === "ELOOP") {
|
|
182
|
-
return Object.freeze({ kind: "symlink", linkTarget: readlinkAt(api, parent, name) });
|
|
183
|
-
}
|
|
184
|
-
throw error;
|
|
185
|
-
}
|
|
186
|
-
try {
|
|
187
|
-
const stats = fstatSync(fd);
|
|
188
|
-
return Object.freeze({
|
|
189
|
-
kind: stats.isDirectory() ? "directory" : stats.isFile() ? "file" : "other",
|
|
190
|
-
stats,
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
finally {
|
|
194
|
-
closeSync(fd);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
finally {
|
|
198
|
-
closeSync(parent);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
function removeChild(api, parentFd, name) {
|
|
202
|
-
let fd;
|
|
203
|
-
try {
|
|
204
|
-
fd = openAt(api, parentFd, name, ENTRY_FLAGS);
|
|
205
|
-
}
|
|
206
|
-
catch (error) {
|
|
207
|
-
const code = error.code;
|
|
208
|
-
if (code === "ENOENT")
|
|
209
|
-
return;
|
|
210
|
-
if (code === "ELOOP") {
|
|
211
|
-
unlinkAt(api, parentFd, name, 0);
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
throw error;
|
|
215
|
-
}
|
|
216
|
-
const stats = fstatSync(fd);
|
|
217
|
-
if (!stats.isDirectory()) {
|
|
218
|
-
closeSync(fd);
|
|
219
|
-
unlinkAt(api, parentFd, name, 0);
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
try {
|
|
223
|
-
for (const name of readDirectoryNames(fd))
|
|
224
|
-
removeChild(api, fd, name);
|
|
225
|
-
}
|
|
226
|
-
finally {
|
|
227
|
-
closeSync(fd);
|
|
228
|
-
}
|
|
229
|
-
unlinkAt(api, parentFd, name, AT_REMOVEDIR);
|
|
230
|
-
}
|
|
231
|
-
function readDirectoryNames(fd) {
|
|
232
|
-
if (process.platform === "linux")
|
|
233
|
-
return readdirSync(descriptorPath(fd));
|
|
234
|
-
const api = nativeApi();
|
|
235
|
-
const duplicate = api.dup(fd);
|
|
236
|
-
if (duplicate < 0)
|
|
237
|
-
throw nativeError("dup(directory)", api);
|
|
238
|
-
const names = [];
|
|
239
|
-
try {
|
|
240
|
-
const buffer = Buffer.alloc(64 * 1024);
|
|
241
|
-
const base = Buffer.alloc(8);
|
|
242
|
-
for (;;) {
|
|
243
|
-
const size = api.getdirentries?.(duplicate, buffer, buffer.length, base) ?? -1;
|
|
244
|
-
if (size < 0)
|
|
245
|
-
throw nativeError("getdirentries", api);
|
|
246
|
-
if (size === 0)
|
|
247
|
-
return names;
|
|
248
|
-
let offset = 0;
|
|
249
|
-
while (offset < size) {
|
|
250
|
-
const recordLength = buffer.readUInt16LE(offset + 4);
|
|
251
|
-
const nameLength = buffer[offset + 7] ?? 0;
|
|
252
|
-
if (recordLength < 8 || offset + recordLength > size || nameLength > recordLength - 8) {
|
|
253
|
-
throw new Error("getdirentries returned an invalid record");
|
|
254
|
-
}
|
|
255
|
-
const name = buffer.subarray(offset + 8, offset + 8 + nameLength).toString("utf8");
|
|
256
|
-
if (name !== "." && name !== "..")
|
|
257
|
-
names.push(safeSegment(name));
|
|
258
|
-
offset += recordLength;
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
finally {
|
|
263
|
-
closeSync(duplicate);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
export function removeAnchoredEntry(root, path) {
|
|
267
|
-
assertContained(root, path);
|
|
268
|
-
const api = nativeApi();
|
|
269
|
-
const parent = openAbsoluteDirectory(dirname(path), false);
|
|
270
|
-
try {
|
|
271
|
-
removeChild(api, parent, basename(path));
|
|
272
|
-
}
|
|
273
|
-
finally {
|
|
274
|
-
closeSync(parent);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
export function createAnchoredSymlink(root, path, target) {
|
|
278
|
-
assertContained(root, path);
|
|
279
|
-
if (target.includes("\0"))
|
|
280
|
-
throw new Error("anchored symlink target contains NUL");
|
|
281
|
-
const api = nativeApi();
|
|
282
|
-
const parent = openAbsoluteDirectory(dirname(path), true);
|
|
283
|
-
try {
|
|
284
|
-
if (api.symlinkat(target, parent, safeSegment(basename(path))) !== 0) {
|
|
285
|
-
throw nativeError("symlinkat", api);
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
finally {
|
|
289
|
-
closeSync(parent);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
export async function readAnchoredDirectory(root, path) {
|
|
293
|
-
if (resolve(root) !== resolve(path))
|
|
294
|
-
assertContained(root, path);
|
|
295
|
-
const fd = openAbsoluteDirectory(path, false);
|
|
296
|
-
try {
|
|
297
|
-
return readDirectoryNames(fd);
|
|
298
|
-
}
|
|
299
|
-
finally {
|
|
300
|
-
closeSync(fd);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { chmodSync, closeSync, constants, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
3
|
-
import { dirname, isAbsolute } from "node:path";
|
|
4
|
-
import { z } from "zod";
|
|
5
|
-
const EventIdSchema = z.string().regex(/^[0-9a-f]{64}$/u);
|
|
6
|
-
const AckSchema = z.object({
|
|
7
|
-
type: z.literal("server:storage-migration-status-ack"),
|
|
8
|
-
eventId: EventIdSchema,
|
|
9
|
-
}).strict();
|
|
10
|
-
const DeliveredFrameSchema = z.object({
|
|
11
|
-
type: z.literal("daemon:storage-migration-status"),
|
|
12
|
-
migrationId: z.string().uuid(),
|
|
13
|
-
status: z.enum([
|
|
14
|
-
"preflight_running", "preflight_passed", "backing_up", "initial_sync", "quiescing", "final_sync", "verified",
|
|
15
|
-
"service_switched", "hello_confirmed", "completed", "failed", "rollback_pending", "rolled_back",
|
|
16
|
-
"rollback_failed",
|
|
17
|
-
]),
|
|
18
|
-
environment: z.string().regex(/^[a-z0-9][a-z0-9_-]{0,31}$/u),
|
|
19
|
-
workspaceId: z.string().uuid(),
|
|
20
|
-
profile: z.string().regex(/^[a-z0-9][a-z0-9_-]{0,47}$/u),
|
|
21
|
-
ownerEpoch: z.number().int().positive().safe().optional(),
|
|
22
|
-
ownershipClaimFingerprint: z.string().regex(/^sha256:[0-9a-f]{64}$/u).optional(),
|
|
23
|
-
sourceStorageFingerprint: z.string().optional(),
|
|
24
|
-
targetStorageFingerprint: z.string().optional(),
|
|
25
|
-
manifestDigest: z.string().optional(),
|
|
26
|
-
finalManifestDigest: z.string().optional(),
|
|
27
|
-
progress: z.object({
|
|
28
|
-
state: z.enum(["running", "stale"]),
|
|
29
|
-
scannedEntries: z.number().int().min(0).max(10_000_000),
|
|
30
|
-
totalEntries: z.number().int().min(0).max(10_000_000).optional(),
|
|
31
|
-
}).strict().optional(),
|
|
32
|
-
retryable: z.literal(true).optional(),
|
|
33
|
-
reopenAdmission: z.literal(true).optional(),
|
|
34
|
-
errorCode: z.string().optional(),
|
|
35
|
-
errorMessage: z.string().optional(),
|
|
36
|
-
eventId: EventIdSchema,
|
|
37
|
-
}).strict();
|
|
38
|
-
const OutboxSchema = z.object({
|
|
39
|
-
version: z.literal(1),
|
|
40
|
-
events: z.array(DeliveredFrameSchema).max(256),
|
|
41
|
-
}).strict();
|
|
42
|
-
const deliveredFrame = (raw) => Object.freeze(DeliveredFrameSchema.parse(raw));
|
|
43
|
-
export function storageStatusEventId(frame) {
|
|
44
|
-
return createHash("sha256").update(JSON.stringify([
|
|
45
|
-
frame.migrationId,
|
|
46
|
-
frame.status,
|
|
47
|
-
frame.environment,
|
|
48
|
-
frame.workspaceId,
|
|
49
|
-
frame.profile,
|
|
50
|
-
frame.ownerEpoch ?? null,
|
|
51
|
-
frame.ownershipClaimFingerprint ?? null,
|
|
52
|
-
frame.sourceStorageFingerprint ?? null,
|
|
53
|
-
frame.targetStorageFingerprint ?? null,
|
|
54
|
-
frame.manifestDigest ?? null,
|
|
55
|
-
frame.finalManifestDigest ?? null,
|
|
56
|
-
frame.progress ?? null,
|
|
57
|
-
frame.retryable ?? null,
|
|
58
|
-
frame.errorCode ?? null,
|
|
59
|
-
frame.reopenAdmission ?? null,
|
|
60
|
-
])).digest("hex");
|
|
61
|
-
}
|
|
62
|
-
function syncDirectory(path) {
|
|
63
|
-
const fd = openSync(path, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
64
|
-
try {
|
|
65
|
-
fsyncSync(fd);
|
|
66
|
-
}
|
|
67
|
-
finally {
|
|
68
|
-
closeSync(fd);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
function durableReplace(path, content) {
|
|
72
|
-
const directory = dirname(path);
|
|
73
|
-
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
74
|
-
chmodSync(directory, 0o700);
|
|
75
|
-
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
76
|
-
let fd = null;
|
|
77
|
-
try {
|
|
78
|
-
fd = openSync(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
|
79
|
-
writeFileSync(fd, content, "utf8");
|
|
80
|
-
fsyncSync(fd);
|
|
81
|
-
closeSync(fd);
|
|
82
|
-
fd = null;
|
|
83
|
-
renameSync(temporary, path);
|
|
84
|
-
chmodSync(path, 0o600);
|
|
85
|
-
syncDirectory(directory);
|
|
86
|
-
}
|
|
87
|
-
finally {
|
|
88
|
-
if (fd !== null)
|
|
89
|
-
closeSync(fd);
|
|
90
|
-
rmSync(temporary, { force: true });
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
function durableRemove(path) {
|
|
94
|
-
try {
|
|
95
|
-
unlinkSync(path);
|
|
96
|
-
syncDirectory(dirname(path));
|
|
97
|
-
}
|
|
98
|
-
catch (error) {
|
|
99
|
-
if (error.code !== "ENOENT")
|
|
100
|
-
throw error;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
function loadOutbox(path) {
|
|
104
|
-
if (path === undefined)
|
|
105
|
-
return new Map();
|
|
106
|
-
if (!isAbsolute(path) || /[\0\r\n]/u.test(path))
|
|
107
|
-
throw new Error("storage status outbox path must be absolute");
|
|
108
|
-
let raw;
|
|
109
|
-
try {
|
|
110
|
-
const metadata = lstatSync(path);
|
|
111
|
-
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size > 1024 * 1024) {
|
|
112
|
-
throw new Error("storage status outbox is not a bounded regular file");
|
|
113
|
-
}
|
|
114
|
-
raw = readFileSync(path, "utf8");
|
|
115
|
-
}
|
|
116
|
-
catch (error) {
|
|
117
|
-
if (error.code === "ENOENT")
|
|
118
|
-
return new Map();
|
|
119
|
-
throw error;
|
|
120
|
-
}
|
|
121
|
-
const parsed = OutboxSchema.parse(JSON.parse(raw));
|
|
122
|
-
const pending = new Map();
|
|
123
|
-
for (const event of parsed.events) {
|
|
124
|
-
const delivered = deliveredFrame(event);
|
|
125
|
-
if (storageStatusEventId(delivered) !== delivered.eventId) {
|
|
126
|
-
throw new Error("storage status outbox event identity mismatch");
|
|
127
|
-
}
|
|
128
|
-
// Progress is telemetry, not a durable obligation. Older versions could
|
|
129
|
-
// leave hundreds of progress frames behind and starve terminal statuses.
|
|
130
|
-
if (delivered.status === "preflight_running")
|
|
131
|
-
continue;
|
|
132
|
-
pending.set(delivered.eventId, delivered);
|
|
133
|
-
}
|
|
134
|
-
return pending;
|
|
135
|
-
}
|
|
136
|
-
export function createStorageStatusDelivery(input) {
|
|
137
|
-
const retryMs = z.number().int().min(5).max(60_000).parse(input.retryMs ?? 1_000);
|
|
138
|
-
const pending = loadOutbox(input.outboxPath);
|
|
139
|
-
const timers = new Map();
|
|
140
|
-
let stopped = false;
|
|
141
|
-
const persist = (events) => {
|
|
142
|
-
if (input.outboxPath === undefined)
|
|
143
|
-
return;
|
|
144
|
-
if (events.length === 0) {
|
|
145
|
-
durableRemove(input.outboxPath);
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
durableReplace(input.outboxPath, `${JSON.stringify(OutboxSchema.parse({ version: 1, events }), null, 2)}\n`);
|
|
149
|
-
};
|
|
150
|
-
const schedule = (eventId) => {
|
|
151
|
-
if (stopped || timers.has(eventId))
|
|
152
|
-
return;
|
|
153
|
-
const timer = setTimeout(() => {
|
|
154
|
-
timers.delete(eventId);
|
|
155
|
-
const frame = pending.get(eventId);
|
|
156
|
-
if (frame === undefined || stopped)
|
|
157
|
-
return;
|
|
158
|
-
input.transmit(frame);
|
|
159
|
-
schedule(eventId);
|
|
160
|
-
}, retryMs);
|
|
161
|
-
timer.unref?.();
|
|
162
|
-
timers.set(eventId, timer);
|
|
163
|
-
};
|
|
164
|
-
const send = (frame) => {
|
|
165
|
-
const eventId = frame.eventId ?? storageStatusEventId(frame);
|
|
166
|
-
EventIdSchema.parse(eventId);
|
|
167
|
-
const delivered = deliveredFrame({ ...frame, eventId });
|
|
168
|
-
// A large manifest can emit thousands of progress updates. Send the
|
|
169
|
-
// latest telemetry opportunistically without putting it in the bounded
|
|
170
|
-
// durable outbox; terminal states remain durable and replayable.
|
|
171
|
-
if (delivered.status === "preflight_running") {
|
|
172
|
-
input.transmit(delivered);
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
const existing = pending.get(eventId);
|
|
176
|
-
if (existing === undefined && pending.size >= 256) {
|
|
177
|
-
throw new Error("storage migration status outbox limit exceeded");
|
|
178
|
-
}
|
|
179
|
-
if (existing !== undefined && JSON.stringify(existing) === JSON.stringify(delivered)) {
|
|
180
|
-
schedule(eventId);
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
const next = [...pending.values()].filter((candidate) => candidate.eventId !== eventId);
|
|
184
|
-
next.push(delivered);
|
|
185
|
-
persist(next);
|
|
186
|
-
pending.set(eventId, delivered);
|
|
187
|
-
input.transmit(delivered);
|
|
188
|
-
schedule(eventId);
|
|
189
|
-
};
|
|
190
|
-
const replayPending = () => {
|
|
191
|
-
for (const [eventId, frame] of pending) {
|
|
192
|
-
input.transmit(frame);
|
|
193
|
-
schedule(eventId);
|
|
194
|
-
}
|
|
195
|
-
};
|
|
196
|
-
const acknowledge = (raw) => {
|
|
197
|
-
const parsed = AckSchema.safeParse(raw);
|
|
198
|
-
if (!parsed.success)
|
|
199
|
-
return false;
|
|
200
|
-
if (!pending.has(parsed.data.eventId))
|
|
201
|
-
return true;
|
|
202
|
-
try {
|
|
203
|
-
persist([...pending.values()].filter((frame) => frame.eventId !== parsed.data.eventId));
|
|
204
|
-
}
|
|
205
|
-
catch (error) {
|
|
206
|
-
input.onPersistenceError?.(error);
|
|
207
|
-
return true;
|
|
208
|
-
}
|
|
209
|
-
pending.delete(parsed.data.eventId);
|
|
210
|
-
const timer = timers.get(parsed.data.eventId);
|
|
211
|
-
if (timer !== undefined)
|
|
212
|
-
clearTimeout(timer);
|
|
213
|
-
timers.delete(parsed.data.eventId);
|
|
214
|
-
return true;
|
|
215
|
-
};
|
|
216
|
-
const stop = () => {
|
|
217
|
-
stopped = true;
|
|
218
|
-
for (const timer of timers.values())
|
|
219
|
-
clearTimeout(timer);
|
|
220
|
-
timers.clear();
|
|
221
|
-
pending.clear();
|
|
222
|
-
};
|
|
223
|
-
return Object.freeze({ send, replayPending, acknowledge, stop, pendingCount: () => pending.size });
|
|
224
|
-
}
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
import { readdir } from "node:fs/promises";
|
|
2
|
-
import { readStorageMigrationReceipt, storageMigrationReplayStatuses, } from "./storage-migration-controller.js";
|
|
3
|
-
import { readStorageWorkerFailureStatus, readStorageWorkerHeartbeat, } from "./storage-worker-status.js";
|
|
4
|
-
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
|
|
5
|
-
export async function latestSourceStorageMigrationReceipt(backupsRoot, expected) {
|
|
6
|
-
let entries;
|
|
7
|
-
try {
|
|
8
|
-
entries = await readdir(backupsRoot, { withFileTypes: true });
|
|
9
|
-
}
|
|
10
|
-
catch (error) {
|
|
11
|
-
if (error.code === "ENOENT")
|
|
12
|
-
return null;
|
|
13
|
-
throw error;
|
|
14
|
-
}
|
|
15
|
-
if (entries.length > 10_000)
|
|
16
|
-
throw new Error("storage migration receipt directory limit exceeded");
|
|
17
|
-
let latest = null;
|
|
18
|
-
for (const entry of entries) {
|
|
19
|
-
if (!entry.isDirectory() || !UUID.test(entry.name))
|
|
20
|
-
continue;
|
|
21
|
-
const receipt = await readStorageMigrationReceipt(backupsRoot, entry.name);
|
|
22
|
-
if (receipt === null || receipt.environment !== expected.environment || receipt.profile !== expected.profile)
|
|
23
|
-
continue;
|
|
24
|
-
const safeTerminal = receipt.status === "rolled_back"
|
|
25
|
-
|| (receipt.status === "failed" && receipt.sourceStopAttempted === false);
|
|
26
|
-
if (!safeTerminal)
|
|
27
|
-
continue;
|
|
28
|
-
const at = new Date(receipt.history.at(-1)?.at ?? 0).getTime();
|
|
29
|
-
if (latest === null || at > latest.at)
|
|
30
|
-
latest = { receipt, at };
|
|
31
|
-
}
|
|
32
|
-
return latest?.receipt ?? null;
|
|
33
|
-
}
|
|
34
|
-
export function storageMigrationStatusReplay(receipt, expected) {
|
|
35
|
-
if (expected !== undefined && (receipt.migrationId !== expected.migrationId
|
|
36
|
-
|| receipt.environment !== expected.environment
|
|
37
|
-
|| receipt.workspaceId !== expected.workspaceId
|
|
38
|
-
|| receipt.profile !== expected.profile
|
|
39
|
-
|| (expected.ownerEpoch !== undefined && receipt.ownerEpoch !== expected.ownerEpoch)
|
|
40
|
-
|| (expected.ownershipClaimFingerprint !== undefined
|
|
41
|
-
&& receipt.ownershipClaimFingerprint !== expected.ownershipClaimFingerprint))) {
|
|
42
|
-
throw new Error("storage migration receipt identity mismatch");
|
|
43
|
-
}
|
|
44
|
-
return Object.freeze(storageMigrationReplayStatuses(receipt).map((status) => Object.freeze({
|
|
45
|
-
type: "daemon:storage-migration-status",
|
|
46
|
-
migrationId: receipt.migrationId,
|
|
47
|
-
status,
|
|
48
|
-
environment: receipt.environment,
|
|
49
|
-
workspaceId: receipt.workspaceId,
|
|
50
|
-
profile: receipt.profile,
|
|
51
|
-
...(receipt.ownerEpoch === undefined ? {} : { ownerEpoch: receipt.ownerEpoch }),
|
|
52
|
-
...(receipt.ownershipClaimFingerprint === undefined ? {} : {
|
|
53
|
-
ownershipClaimFingerprint: receipt.ownershipClaimFingerprint,
|
|
54
|
-
}),
|
|
55
|
-
...(status === "verified" ? { finalManifestDigest: receipt.manifestDigest } : {}),
|
|
56
|
-
...(status === "failed"
|
|
57
|
-
? {
|
|
58
|
-
errorCode: receipt.errorCode ?? "storage_migration_failed",
|
|
59
|
-
...(receipt.errorMessage === undefined ? {} : { errorMessage: receipt.errorMessage }),
|
|
60
|
-
...(receipt.sourceStopAttempted === false ? { reopenAdmission: true } : {}),
|
|
61
|
-
}
|
|
62
|
-
: {}),
|
|
63
|
-
})));
|
|
64
|
-
}
|
|
65
|
-
const wait = (milliseconds) => new Promise((resolvePromise) => {
|
|
66
|
-
setTimeout(resolvePromise, milliseconds);
|
|
67
|
-
});
|
|
68
|
-
export async function monitorStorageMigrationStatus(input, send, injected = {}) {
|
|
69
|
-
const readReceipt = injected.readReceipt ?? readStorageMigrationReceipt;
|
|
70
|
-
const readWorkerFailure = injected.readWorkerFailure ?? readStorageWorkerFailureStatus;
|
|
71
|
-
const readWorkerHeartbeatStatus = injected.readWorkerHeartbeat ?? readStorageWorkerHeartbeat;
|
|
72
|
-
const now = injected.now ?? (() => new Date());
|
|
73
|
-
const delay = injected.delay ?? wait;
|
|
74
|
-
const pollIntervalMs = input.pollIntervalMs ?? 250;
|
|
75
|
-
const heartbeatTimeoutMs = input.workerHeartbeatTimeoutMs ?? 30_000;
|
|
76
|
-
const monitorStartedAt = now().getTime();
|
|
77
|
-
let sentCount = 0;
|
|
78
|
-
for (;;) {
|
|
79
|
-
const [receipt, failure] = await Promise.all([
|
|
80
|
-
readReceipt(input.backupsRoot, input.migrationId),
|
|
81
|
-
readWorkerFailure(input.stateRoot, input.migrationId),
|
|
82
|
-
]);
|
|
83
|
-
if (receipt !== null) {
|
|
84
|
-
const frames = storageMigrationStatusReplay(receipt, input);
|
|
85
|
-
for (const frame of frames.slice(sentCount))
|
|
86
|
-
send(frame);
|
|
87
|
-
sentCount = frames.length;
|
|
88
|
-
if (receipt.status === "service_switched" || receipt.status === "completed") {
|
|
89
|
-
return { status: receipt.status, reopenAdmission: false };
|
|
90
|
-
}
|
|
91
|
-
if (receipt.status === "rolled_back")
|
|
92
|
-
return { status: "rolled_back", reopenAdmission: true };
|
|
93
|
-
if (receipt.status === "rollback_failed")
|
|
94
|
-
return { status: "rollback_failed", reopenAdmission: false };
|
|
95
|
-
if (receipt.status === "failed" && receipt.sourceStopAttempted === false) {
|
|
96
|
-
return { status: "failed", reopenAdmission: true };
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
if (failure !== null) {
|
|
100
|
-
const workerFailure = failure;
|
|
101
|
-
send({
|
|
102
|
-
type: "daemon:storage-migration-status",
|
|
103
|
-
migrationId: input.migrationId,
|
|
104
|
-
status: "failed",
|
|
105
|
-
environment: input.environment,
|
|
106
|
-
workspaceId: input.workspaceId,
|
|
107
|
-
profile: input.profile,
|
|
108
|
-
...(input.ownerEpoch === undefined ? {} : { ownerEpoch: input.ownerEpoch }),
|
|
109
|
-
...(input.ownershipClaimFingerprint === undefined ? {} : {
|
|
110
|
-
ownershipClaimFingerprint: input.ownershipClaimFingerprint,
|
|
111
|
-
}),
|
|
112
|
-
errorCode: workerFailure.errorCode,
|
|
113
|
-
errorMessage: workerFailure.errorMessage,
|
|
114
|
-
...(receipt?.sourceStopAttempted === false || receipt === null ? { reopenAdmission: true } : {}),
|
|
115
|
-
});
|
|
116
|
-
return { status: "failed", reopenAdmission: receipt?.sourceStopAttempted === false || receipt === null };
|
|
117
|
-
}
|
|
118
|
-
const heartbeat = await readWorkerHeartbeatStatus(input.stateRoot, input.migrationId);
|
|
119
|
-
const heartbeatAt = heartbeat === null ? monitorStartedAt : new Date(heartbeat.at).getTime();
|
|
120
|
-
if (now().getTime() - heartbeatAt > heartbeatTimeoutMs) {
|
|
121
|
-
send({
|
|
122
|
-
type: "daemon:storage-migration-status",
|
|
123
|
-
migrationId: input.migrationId,
|
|
124
|
-
status: "failed",
|
|
125
|
-
environment: input.environment,
|
|
126
|
-
workspaceId: input.workspaceId,
|
|
127
|
-
profile: input.profile,
|
|
128
|
-
...(input.ownerEpoch === undefined ? {} : { ownerEpoch: input.ownerEpoch }),
|
|
129
|
-
...(input.ownershipClaimFingerprint === undefined ? {} : {
|
|
130
|
-
ownershipClaimFingerprint: input.ownershipClaimFingerprint,
|
|
131
|
-
}),
|
|
132
|
-
errorCode: "worker_heartbeat_stale_recovery_required",
|
|
133
|
-
errorMessage: "storage migration worker heartbeat expired; explicit recovery is required",
|
|
134
|
-
...(receipt?.sourceStopAttempted === false || receipt === null ? { reopenAdmission: true } : {}),
|
|
135
|
-
});
|
|
136
|
-
return { status: "failed", reopenAdmission: receipt?.sourceStopAttempted === false || receipt === null };
|
|
137
|
-
}
|
|
138
|
-
await delay(pollIntervalMs);
|
|
139
|
-
}
|
|
140
|
-
}
|