@nowcrew/daemon 0.6.60 → 0.6.64

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +9 -8
  2. package/dist/agent-ability/materializer.js +1 -20
  3. package/dist/automatic-install-target.js +10 -15
  4. package/dist/computer-cli.js +1 -10
  5. package/dist/computer-service.js +1 -41
  6. package/dist/config.js +0 -35
  7. package/dist/daemon-migration-controller.js +18 -16
  8. package/dist/execution-runner.js +13 -35
  9. package/dist/local-executor.js +1 -44
  10. package/dist/machine-info.js +2 -3
  11. package/dist/main.js +0 -83
  12. package/dist/managed-service-lifecycle.js +2 -47
  13. package/dist/managed-service-startup.js +0 -2
  14. package/dist/profile-layout.js +4 -10
  15. package/dist/runner.js +0 -1
  16. package/dist/serve.js +3 -64
  17. package/dist/workspace.js +1 -24
  18. package/package.json +10 -9
  19. package/dist/local-memory/index-budget.js +0 -74
  20. package/dist/local-memory/pending.js +0 -111
  21. package/dist/local-memory/runtime.js +0 -87
  22. package/dist/local-memory/types.js +0 -1
  23. package/dist/local-memory/write.js +0 -448
  24. package/dist/storage-config.js +0 -175
  25. package/dist/storage-control.js +0 -131
  26. package/dist/storage-copy.js +0 -444
  27. package/dist/storage-credential-guard.js +0 -21
  28. package/dist/storage-doctor.js +0 -175
  29. package/dist/storage-identity.js +0 -29
  30. package/dist/storage-layout.js +0 -143
  31. package/dist/storage-manifest.js +0 -440
  32. package/dist/storage-migration-controller.js +0 -417
  33. package/dist/storage-migration-manifest.js +0 -105
  34. package/dist/storage-migration-policy.js +0 -100
  35. package/dist/storage-runtime-controller.js +0 -313
  36. package/dist/storage-runtime-readiness.js +0 -184
  37. package/dist/storage-runtime.js +0 -39
  38. package/dist/storage-safe-fs.js +0 -293
  39. package/dist/storage-serve-adapter.js +0 -65
  40. package/dist/storage-status-delivery.js +0 -201
  41. package/dist/storage-status-replay.js +0 -125
  42. package/dist/storage-target-confirmation.js +0 -129
  43. package/dist/storage-worker-command.js +0 -27
  44. package/dist/storage-worker-executor.js +0 -383
  45. package/dist/storage-worker-handoff.js +0 -151
  46. package/dist/storage-worker-operation.js +0 -283
  47. package/dist/storage-worker-status.js +0 -105
@@ -1,444 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import { constants } from "node:fs";
3
- import { lstat, realpath, rename, } from "node:fs/promises";
4
- import { dirname, isAbsolute, relative, resolve } from "node:path";
5
- import { containsStorageCredentialMaterial } from "./storage-credential-guard.js";
6
- import { chmodAnchoredDirectory, createAnchoredFile, createAnchoredSymlink, ensureAnchoredDirectory, inspectAnchoredEntry, openAnchoredFile, readAnchoredDirectory, removeAnchoredEntry, } from "./storage-safe-fs.js";
7
- export const REDACTED_STORAGE_TOKEN = "[REDACTED: historical generic_sk credential removed during NOWWORK_HOME migration]";
8
- const WORKSPACE_DIRECTORIES = ["agent-items", "daemon", "repos", "suite-tools"];
9
- const LIVE_LIKE_TOKEN = /(?:bearer[ \t]+[a-z0-9._~-]{20,}|sk-[a-z0-9_-]{20,}|AKIA[0-9A-Z]{16})/giu;
10
- const MAX_TRANSFORM_FILE_BYTES = 8 * 1024 * 1024;
11
- const CREDENTIAL_SCAN_OVERLAP_BYTES = 4 * 1024;
12
- const FILE_OPERATION_CONCURRENCY = 8;
13
- function containedPath(root, logicalPath) {
14
- if (logicalPath.length === 0 || isAbsolute(logicalPath) || logicalPath.includes("\0")
15
- || logicalPath.includes("\n") || logicalPath.includes("\r")) {
16
- throw new Error(`unsafe logical migration path: ${logicalPath}`);
17
- }
18
- const target = resolve(root, logicalPath);
19
- const child = relative(root, target);
20
- if (child === "" || child.startsWith("..") || isAbsolute(child)) {
21
- throw new Error(`logical migration path escapes staging: ${logicalPath}`);
22
- }
23
- return target;
24
- }
25
- function sourcePath(input) {
26
- const root = input.sourceRoots[input.entry.sourceKind];
27
- if (root === undefined)
28
- throw new Error(`missing source root: ${input.entry.sourceKind}`);
29
- return containedPath(root, input.entry.sourcePath);
30
- }
31
- async function exists(path) {
32
- try {
33
- await lstat(path);
34
- return true;
35
- }
36
- catch (error) {
37
- if (error.code === "ENOENT")
38
- return false;
39
- throw error;
40
- }
41
- }
42
- async function copyVerifiedFile(source, target, stagingWorkspaceRoot, entry, replaceExisting, afterSourceLstat, afterTargetParentOpen) {
43
- if (entry.sha256 === undefined)
44
- throw new Error(`manifest file digest is missing: ${source}`);
45
- const sourceHandle = await openAnchoredFile(source, constants.O_RDONLY).catch((error) => {
46
- throw new Error(`migration source cannot be opened without following links: ${entry.sourcePath}`, { cause: error });
47
- });
48
- try {
49
- const opened = await sourceHandle.stat();
50
- if (!opened.isFile())
51
- throw new Error(`migration source is no longer a file: ${entry.sourcePath}`);
52
- await afterSourceLstat?.(source);
53
- let sourceAfterHook;
54
- try {
55
- sourceAfterHook = inspectAnchoredEntry(dirname(source), source);
56
- }
57
- catch (error) {
58
- throw new Error(`migration source changed before copy: ${entry.sourcePath}`, { cause: error });
59
- }
60
- if (sourceAfterHook.kind !== "file" || sourceAfterHook.stats.dev !== opened.dev
61
- || sourceAfterHook.stats.ino !== opened.ino) {
62
- throw new Error(`migration source changed before copy: ${entry.sourcePath}`);
63
- }
64
- if (replaceExisting)
65
- removeReplaceableTarget(stagingWorkspaceRoot, target, entry.logicalDestination ?? entry.sourcePath);
66
- const targetHandle = await createAnchoredFile(stagingWorkspaceRoot, target, 0o600, afterTargetParentOpen).catch((error) => {
67
- throw new Error(`migration target cannot be opened without following links: ${entry.logicalDestination}`, { cause: error });
68
- });
69
- const targetOpened = await targetHandle.stat();
70
- const digest = createHash("sha256");
71
- const sourceStream = sourceHandle.createReadStream({ autoClose: false });
72
- try {
73
- for await (const rawChunk of sourceStream) {
74
- const chunk = Buffer.from(rawChunk);
75
- digest.update(chunk);
76
- await targetHandle.write(chunk, 0, chunk.length, null);
77
- }
78
- await targetHandle.sync();
79
- await targetHandle.chmod(Number.parseInt(entry.mode, 8));
80
- }
81
- finally {
82
- await targetHandle.close();
83
- }
84
- const after = await sourceHandle.stat();
85
- const sourceCurrent = inspectAnchoredEntry(dirname(source), source);
86
- if (opened.dev !== after.dev || opened.ino !== after.ino || opened.size !== after.size
87
- || opened.mtimeMs !== after.mtimeMs || digest.digest("hex") !== entry.sha256
88
- || sourceCurrent.kind !== "file" || sourceCurrent.stats.dev !== opened.dev
89
- || sourceCurrent.stats.ino !== opened.ino) {
90
- throw new Error(`source digest mismatch: ${source}`);
91
- }
92
- let targetCurrent;
93
- try {
94
- targetCurrent = inspectAnchoredEntry(stagingWorkspaceRoot, target);
95
- }
96
- catch (error) {
97
- throw new Error(`migration target changed while copying: ${entry.logicalDestination}`, { cause: error });
98
- }
99
- if (targetCurrent.kind !== "file" || targetCurrent.stats.dev !== targetOpened.dev
100
- || targetCurrent.stats.ino !== targetOpened.ino) {
101
- throw new Error(`migration target changed while copying: ${entry.logicalDestination}`);
102
- }
103
- }
104
- finally {
105
- await sourceHandle.close();
106
- }
107
- }
108
- async function createSafeDirectory(root, target) {
109
- try {
110
- ensureAnchoredDirectory(root, target, 0o700);
111
- }
112
- catch (error) {
113
- throw new Error(`migration target ancestor is a symbolic link or non-directory: ${target}`, { cause: error });
114
- }
115
- }
116
- async function createSafeParent(root, path) {
117
- await createSafeDirectory(root, dirname(path));
118
- }
119
- function removeReplaceableTarget(root, path, logicalDestination) {
120
- try {
121
- const existing = inspectAnchoredEntry(root, path);
122
- if (existing.kind === "symlink") {
123
- throw new Error(`migration target is a symbolic link: ${logicalDestination}`);
124
- }
125
- }
126
- catch (error) {
127
- if (error.code === "ENOENT")
128
- return;
129
- throw new Error(`migration target ancestor is a symbolic link or non-directory: ${logicalDestination}`, { cause: error });
130
- }
131
- removeAnchoredEntry(root, path);
132
- }
133
- async function assertRealpathWithin(root, candidate, message) {
134
- const [canonicalRoot, canonicalCandidate] = await Promise.all([realpath(root), realpath(candidate)]);
135
- const child = relative(canonicalRoot, canonicalCandidate);
136
- if (child.startsWith("..") || isAbsolute(child))
137
- throw new Error(message);
138
- }
139
- async function mapInBatches(items, concurrency, operation) {
140
- const results = [];
141
- for (let offset = 0; offset < items.length; offset += concurrency) {
142
- results.push(...await Promise.all(items.slice(offset, offset + concurrency).map(operation)));
143
- }
144
- return results;
145
- }
146
- async function transformFile(source, target, stagingWorkspaceRoot, entry, replaceExisting, afterSourceLstat, afterTargetParentOpen) {
147
- if (entry.sha256 === undefined)
148
- throw new Error(`manifest file digest is missing: ${source}`);
149
- const sourceHandle = await openAnchoredFile(source, constants.O_RDONLY).catch((error) => {
150
- throw new Error(`migration source cannot be opened without following links: ${entry.sourcePath}`, { cause: error });
151
- });
152
- let input;
153
- try {
154
- const opened = await sourceHandle.stat();
155
- if (!opened.isFile())
156
- throw new Error(`migration source is no longer a file: ${entry.sourcePath}`);
157
- await afterSourceLstat?.(source);
158
- let sourceAfterHook;
159
- try {
160
- sourceAfterHook = inspectAnchoredEntry(dirname(source), source);
161
- }
162
- catch (error) {
163
- throw new Error(`migration source changed before transform: ${entry.sourcePath}`, { cause: error });
164
- }
165
- if (sourceAfterHook.kind !== "file" || sourceAfterHook.stats.dev !== opened.dev
166
- || sourceAfterHook.stats.ino !== opened.ino) {
167
- throw new Error(`migration source changed before transform: ${entry.sourcePath}`);
168
- }
169
- if (opened.size > MAX_TRANSFORM_FILE_BYTES) {
170
- throw new Error(`target transformation source exceeds bounded size: ${entry.sourcePath}`);
171
- }
172
- input = await sourceHandle.readFile("utf8");
173
- const after = await sourceHandle.stat();
174
- const sourceCurrent = inspectAnchoredEntry(dirname(source), source);
175
- if (opened.dev !== after.dev || opened.ino !== after.ino || opened.size !== after.size
176
- || opened.mtimeMs !== after.mtimeMs || createHash("sha256").update(input).digest("hex") !== entry.sha256
177
- || sourceCurrent.kind !== "file" || sourceCurrent.stats.dev !== opened.dev
178
- || sourceCurrent.stats.ino !== opened.ino) {
179
- throw new Error(`source digest mismatch: ${source}`);
180
- }
181
- }
182
- finally {
183
- await sourceHandle.close();
184
- }
185
- const output = input.replaceAll(LIVE_LIKE_TOKEN, REDACTED_STORAGE_TOKEN);
186
- if (output === input || LIVE_LIKE_TOKEN.test(output))
187
- throw new Error(`target transformation did not remove live-like tokens: ${entry.sourcePath}`);
188
- LIVE_LIKE_TOKEN.lastIndex = 0;
189
- if (entry.targetSha256 === undefined || createHash("sha256").update(output).digest("hex") !== entry.targetSha256) {
190
- throw new Error(`transformed target digest mismatch: ${entry.sourcePath}`);
191
- }
192
- if (replaceExisting)
193
- removeReplaceableTarget(stagingWorkspaceRoot, target, entry.logicalDestination ?? entry.sourcePath);
194
- const targetHandle = await createAnchoredFile(stagingWorkspaceRoot, target, 0o600, afterTargetParentOpen).catch((error) => {
195
- throw new Error(`migration target cannot be opened without following links: ${entry.logicalDestination}`, { cause: error });
196
- });
197
- const targetOpened = await targetHandle.stat();
198
- try {
199
- await targetHandle.writeFile(output, "utf8");
200
- await targetHandle.sync();
201
- await targetHandle.chmod(Number.parseInt(entry.targetMode ?? "0600", 8));
202
- }
203
- finally {
204
- await targetHandle.close();
205
- }
206
- let targetCurrent;
207
- try {
208
- targetCurrent = inspectAnchoredEntry(stagingWorkspaceRoot, target);
209
- }
210
- catch (error) {
211
- throw new Error(`migration target changed while transforming: ${entry.logicalDestination}`, { cause: error });
212
- }
213
- if (targetCurrent.kind !== "file" || targetCurrent.stats.dev !== targetOpened.dev
214
- || targetCurrent.stats.ino !== targetOpened.ino) {
215
- throw new Error(`migration target changed while transforming: ${entry.logicalDestination}`);
216
- }
217
- return Buffer.byteLength(output);
218
- }
219
- export async function materializeStorageManifest(input, injected = {}) {
220
- if (await exists(input.stagingWorkspaceRoot)) {
221
- if (!(await lstat(input.stagingWorkspaceRoot)).isDirectory()
222
- || (!input.mergeIntoExisting
223
- && (await readAnchoredDirectory(input.stagingWorkspaceRoot, input.stagingWorkspaceRoot)).length > 0)) {
224
- throw new Error("staging collision: staging workspace root is not empty");
225
- }
226
- }
227
- else {
228
- ensureAnchoredDirectory(dirname(input.stagingWorkspaceRoot), input.stagingWorkspaceRoot, 0o700);
229
- }
230
- let createdDirectories = 0;
231
- for (const directory of WORKSPACE_DIRECTORIES) {
232
- const path = containedPath(input.stagingWorkspaceRoot, directory);
233
- await createSafeDirectory(input.stagingWorkspaceRoot, path);
234
- createdDirectories += 1;
235
- }
236
- const directoryModes = [];
237
- const symlinks = [];
238
- const files = [];
239
- for (const entry of input.manifest.entries) {
240
- if (entry.disposition === "skip" || entry.logicalDestination === null)
241
- continue;
242
- const target = containedPath(input.stagingWorkspaceRoot, entry.logicalDestination);
243
- if (entry.type === "directory") {
244
- await createSafeDirectory(input.stagingWorkspaceRoot, target);
245
- directoryModes.push({ path: target, mode: Number.parseInt(entry.mode, 8) });
246
- createdDirectories += 1;
247
- continue;
248
- }
249
- if (entry.type === "file") {
250
- files.push(entry);
251
- continue;
252
- }
253
- if (entry.type === "symlink") {
254
- if (entry.link?.classification !== "internal" || !entry.link.portable || entry.link.relativeTarget === undefined) {
255
- throw new Error(`manifest contains a non-portable copied symlink: ${entry.sourcePath}`);
256
- }
257
- symlinks.push({ entry, target });
258
- continue;
259
- }
260
- throw new Error(`unsupported copied manifest entry: ${entry.sourcePath}`);
261
- }
262
- const fileResults = await mapInBatches(files, FILE_OPERATION_CONCURRENCY, async (entry) => {
263
- const target = containedPath(input.stagingWorkspaceRoot, entry.logicalDestination);
264
- const source = sourcePath({ sourceRoots: input.sourceRoots, entry });
265
- if (entry.disposition === "transform") {
266
- return {
267
- transformed: true,
268
- bytesWritten: await transformFile(source, target, input.stagingWorkspaceRoot, entry, input.mergeIntoExisting === true, injected.afterSourceLstat, injected.afterTargetParentOpen),
269
- };
270
- }
271
- await copyVerifiedFile(source, target, input.stagingWorkspaceRoot, entry, input.mergeIntoExisting === true, injected.afterSourceLstat, injected.afterTargetParentOpen);
272
- return { transformed: false, bytesWritten: entry.sizeBytes };
273
- });
274
- for (const { entry, target } of symlinks) {
275
- await createSafeParent(input.stagingWorkspaceRoot, target);
276
- if (input.mergeIntoExisting)
277
- removeReplaceableTarget(input.stagingWorkspaceRoot, target, entry.logicalDestination ?? entry.sourcePath);
278
- createAnchoredSymlink(input.stagingWorkspaceRoot, target, entry.link.relativeTarget);
279
- }
280
- for (const { entry, target } of symlinks) {
281
- await assertRealpathWithin(input.stagingWorkspaceRoot, target, `staged symlink escapes workspace: ${entry.sourcePath}`);
282
- }
283
- directoryModes.sort((left, right) => right.path.length - left.path.length);
284
- for (const directory of directoryModes) {
285
- chmodAnchoredDirectory(input.stagingWorkspaceRoot, directory.path, directory.mode);
286
- }
287
- const transformedFiles = fileResults.filter((result) => result.transformed).length;
288
- return Object.freeze({
289
- copiedFiles: fileResults.length - transformedFiles,
290
- transformedFiles,
291
- copiedSymlinks: symlinks.length,
292
- createdDirectories,
293
- bytesWritten: fileResults.reduce((total, result) => total + result.bytesWritten, 0),
294
- });
295
- }
296
- function copiedEntries(manifest) {
297
- return new Map(manifest.entries
298
- .filter((entry) => entry.disposition !== "skip" && entry.logicalDestination !== null)
299
- .map((entry) => [entry.logicalDestination, entry]));
300
- }
301
- export async function reconcileStorageManifest(input) {
302
- if (input.previousManifest.migrationId !== input.manifest.migrationId
303
- || input.previousManifest.workspaceId !== input.manifest.workspaceId
304
- || input.previousManifest.targetWorkspaceRoot !== input.manifest.targetWorkspaceRoot) {
305
- throw new Error("storage reconciliation manifest identity mismatch");
306
- }
307
- const stagingRoot = await lstat(input.stagingWorkspaceRoot);
308
- if (!stagingRoot.isDirectory() || stagingRoot.isSymbolicLink()) {
309
- throw new Error("storage reconciliation requires existing staging");
310
- }
311
- const previous = copiedEntries(input.previousManifest);
312
- const next = copiedEntries(input.manifest);
313
- const removals = [...previous.entries()].filter(([logical, entry]) => {
314
- const current = next.get(logical);
315
- return current === undefined || current.type !== entry.type;
316
- }).sort(([left], [right]) => right.length - left.length);
317
- for (const [logical] of removals) {
318
- const target = containedPath(input.stagingWorkspaceRoot, logical);
319
- try {
320
- removeAnchoredEntry(input.stagingWorkspaceRoot, target);
321
- }
322
- catch (error) {
323
- throw new Error(`migration target ancestor is a symbolic link or non-directory: ${logical}`, { cause: error });
324
- }
325
- }
326
- const changed = input.manifest.entries.filter((entry) => {
327
- if (entry.logicalDestination === null || entry.disposition === "skip")
328
- return false;
329
- const prior = previous.get(entry.logicalDestination);
330
- return prior === undefined || JSON.stringify(prior) !== JSON.stringify(entry);
331
- });
332
- return materializeStorageManifest({
333
- manifest: { ...input.manifest, entries: changed },
334
- sourceRoots: input.sourceRoots,
335
- stagingWorkspaceRoot: input.stagingWorkspaceRoot,
336
- mergeIntoExisting: true,
337
- });
338
- }
339
- async function inspectStorageFile(root, path, afterTargetLstat) {
340
- const handle = await openAnchoredFile(path, constants.O_RDONLY).catch((error) => {
341
- throw new Error(`staged file cannot be opened without following links: ${path}`, { cause: error });
342
- });
343
- try {
344
- const opened = await handle.stat();
345
- if (!opened.isFile())
346
- throw new Error(`staged file is no longer a regular file: ${path}`);
347
- await afterTargetLstat?.(path);
348
- const digest = createHash("sha256");
349
- let tail = "";
350
- let credentialMaterial = false;
351
- const stream = handle.createReadStream({ autoClose: false });
352
- for await (const chunk of stream) {
353
- digest.update(chunk);
354
- const text = tail + Buffer.from(chunk).toString("latin1");
355
- if (containsStorageCredentialMaterial(text))
356
- credentialMaterial = true;
357
- tail = text.slice(-CREDENTIAL_SCAN_OVERLAP_BYTES);
358
- }
359
- const after = await handle.stat();
360
- const pathAfter = inspectAnchoredEntry(root, path);
361
- if (pathAfter.kind !== "file" || opened.dev !== after.dev || opened.ino !== after.ino
362
- || opened.size !== after.size || opened.mtimeMs !== after.mtimeMs
363
- || opened.dev !== pathAfter.stats.dev || opened.ino !== pathAfter.stats.ino) {
364
- throw new Error(`staged file changed while verifying: ${path}`);
365
- }
366
- return Object.freeze({ digest: digest.digest("hex"), credentialMaterial });
367
- }
368
- finally {
369
- await handle.close();
370
- }
371
- }
372
- async function scanLiveLikeMatches(root, alreadyScanned) {
373
- let matches = 0;
374
- const pending = [root];
375
- while (pending.length > 0) {
376
- const directory = pending.pop();
377
- for (const name of await readAnchoredDirectory(root, directory)) {
378
- const path = resolve(directory, name);
379
- const entry = inspectAnchoredEntry(root, path);
380
- if (entry.kind === "directory")
381
- pending.push(path);
382
- else if (entry.kind === "file" && !alreadyScanned.has(path)
383
- && (await inspectStorageFile(root, path)).credentialMaterial)
384
- matches += 1;
385
- }
386
- }
387
- return matches;
388
- }
389
- export async function verifyStagedStorage(input, injected = {}) {
390
- let checkedEntries = 0;
391
- let liveLikeMatches = 0;
392
- const scannedFiles = new Set();
393
- for (const directory of WORKSPACE_DIRECTORIES) {
394
- const target = containedPath(input.stagingWorkspaceRoot, directory);
395
- const targetEntry = inspectAnchoredEntry(input.stagingWorkspaceRoot, target);
396
- if (targetEntry.kind !== "directory" || (targetEntry.stats.mode & 0o777) !== 0o700) {
397
- throw new Error(`required workspace directory is invalid: ${directory}`);
398
- }
399
- }
400
- for (const entry of input.manifest.entries) {
401
- const logical = entry.logicalDestination;
402
- if (entry.disposition === "skip" || logical === null)
403
- continue;
404
- const target = containedPath(input.stagingWorkspaceRoot, logical);
405
- const targetEntry = inspectAnchoredEntry(input.stagingWorkspaceRoot, target);
406
- if (entry.type === "directory" && targetEntry.kind !== "directory")
407
- throw new Error(`staged type mismatch: ${logical}`);
408
- if (entry.type === "file") {
409
- if (targetEntry.kind !== "file")
410
- throw new Error(`staged type mismatch: ${logical}`);
411
- const expected = entry.disposition === "transform" ? entry.targetSha256 : entry.sha256;
412
- const inspected = await inspectStorageFile(input.stagingWorkspaceRoot, target, injected.afterTargetLstat);
413
- scannedFiles.add(target);
414
- if (expected === undefined || inspected.digest !== expected)
415
- throw new Error(`staged digest mismatch: ${logical}`);
416
- if (inspected.credentialMaterial)
417
- liveLikeMatches += 1;
418
- const expectedMode = Number.parseInt(entry.disposition === "transform" ? entry.targetMode ?? "0600" : entry.mode, 8);
419
- if ((targetEntry.stats.mode & 0o7777) !== expectedMode)
420
- throw new Error(`staged mode mismatch: ${logical}`);
421
- }
422
- if (entry.type === "symlink") {
423
- if (targetEntry.kind !== "symlink" || targetEntry.linkTarget !== entry.link?.relativeTarget) {
424
- throw new Error(`staged symlink mismatch: ${logical}`);
425
- }
426
- await assertRealpathWithin(input.stagingWorkspaceRoot, target, `staged symlink escapes workspace: ${logical}`);
427
- }
428
- checkedEntries += 1;
429
- }
430
- liveLikeMatches += await scanLiveLikeMatches(input.stagingWorkspaceRoot, scannedFiles);
431
- if (liveLikeMatches !== 0)
432
- throw new Error(`staging contains credential material in ${liveLikeMatches} file(s)`);
433
- return Object.freeze({ valid: true, checkedEntries, liveLikeMatches: 0 });
434
- }
435
- export async function publishStagedStorage(input, dependencies = {}) {
436
- if (dirname(input.stagingWorkspaceRoot) !== dirname(input.targetWorkspaceRoot)) {
437
- throw new Error("staging and target workspace roots must be siblings on one filesystem");
438
- }
439
- if (await exists(input.targetWorkspaceRoot))
440
- throw new Error("target collision: target workspace root already exists");
441
- if (!(await lstat(input.stagingWorkspaceRoot)).isDirectory())
442
- throw new Error("staging workspace root is not a directory");
443
- await (dependencies.rename ?? rename)(input.stagingWorkspaceRoot, input.targetWorkspaceRoot);
444
- }
@@ -1,21 +0,0 @@
1
- const CREDENTIAL_PATTERNS = Object.freeze([
2
- /-----BEGIN (?:[A-Z ]+ )?PRIVATE KEY-----/iu,
3
- /\bAuthorization\s*:\s*(?:Bearer|Basic)\s+\S{8,}/iu,
4
- /\bCookie\s*:\s*[^\n]*(?:session|token|auth)[^\n]{8,}/iu,
5
- /\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis):\/\/[^\s:/]+:[^\s@]+@/iu,
6
- /\bsk_(?:machine|user|agent)_[A-Za-z0-9_-]{4,}\b/u,
7
- /\bAKIA[0-9A-Z]{16}\b/u,
8
- /\b(?:sk|gh[oprsu]|xox[baprs])-[-A-Za-z0-9_]{12,}\b/u,
9
- /(?:^|[^A-Za-z0-9])(?:[A-Z0-9]+_)*(?:PASSWORD|PASSWD|SECRET|TOKEN|API_KEY|ACCESS_KEY|PRIVATE_KEY)["']?\s*[:=]\s*["']?[^\s"';&,}]{8,}/imu,
10
- /\b(?:AWS_SECRET_ACCESS_KEY|TENCENTCLOUD_SECRET_KEY|SECRET_KEY|API_KEY|ACCESS_TOKEN|AUTH_TOKEN|SESSION_TOKEN)\s*[:=]\s*\S{8,}/iu,
11
- /\b(?:api[-_]?key|access[-_]?token|refresh[-_]?token|password|passwd|secret|credential|authorization|session|token)\s*[:=]\s*["']?[^\s"';&]{8,}/iu,
12
- /\bbearer[ \t]+[A-Za-z0-9._~+/=-]{12,}\b/iu,
13
- ]);
14
- export function containsStorageCredentialMaterial(value) {
15
- return CREDENTIAL_PATTERNS.some((pattern) => pattern.test(value));
16
- }
17
- export function assertStorageControlMaterialTokenFree(value, source) {
18
- if (containsStorageCredentialMaterial(value)) {
19
- throw new Error(`credential material detected in ${source}`);
20
- }
21
- }
@@ -1,175 +0,0 @@
1
- import { constants } from "node:fs";
2
- import { access, lstat, readdir, realpath, statfs } from "node:fs/promises";
3
- import { dirname, isAbsolute, join, relative, resolve } from "node:path";
4
- import { generateStorageManifest, parseStorageManifest, storageManifestDigest, } from "./storage-manifest.js";
5
- import { validateStorageEnvironment } from "./storage-layout.js";
6
- export function discoverLegacyStorageSources(input) {
7
- if (!isAbsolute(input.legacyRoot))
8
- throw new Error("legacy root must be absolute");
9
- for (const value of [input.daemonDirectory, input.agentsDirectory]) {
10
- if (!/^[a-z0-9][a-z0-9_-]{0,127}$/u.test(value))
11
- throw new Error("legacy directory name is invalid");
12
- }
13
- return Object.freeze([
14
- Object.freeze({ kind: "daemon-home", root: join(input.legacyRoot, input.daemonDirectory), logicalPrefix: "daemon" }),
15
- Object.freeze({ kind: "agents-root", root: join(input.legacyRoot, input.agentsDirectory), logicalPrefix: "agent-items" }),
16
- ]);
17
- }
18
- async function nearestExistingAncestor(path) {
19
- let candidate = path;
20
- for (;;) {
21
- try {
22
- await lstat(candidate);
23
- return candidate;
24
- }
25
- catch (error) {
26
- if (error.code !== "ENOENT")
27
- throw error;
28
- }
29
- const parent = dirname(candidate);
30
- if (parent === candidate)
31
- throw new Error("target storage has no existing ancestor");
32
- candidate = parent;
33
- }
34
- }
35
- async function canonicalPotentialPath(path) {
36
- const absolute = resolve(path);
37
- const ancestor = await nearestExistingAncestor(absolute);
38
- const canonicalAncestor = await realpath(ancestor);
39
- return resolve(canonicalAncestor, relative(ancestor, absolute));
40
- }
41
- async function pathsOverlap(left, right) {
42
- const [canonicalLeft, canonicalRight] = await Promise.all([
43
- canonicalPotentialPath(left),
44
- canonicalPotentialPath(right),
45
- ]);
46
- const leftToRight = relative(canonicalLeft, canonicalRight);
47
- const rightToLeft = relative(canonicalRight, canonicalLeft);
48
- const contained = (value) => value === "" || (!value.startsWith("..") && !isAbsolute(value));
49
- return contained(leftToRight) || contained(rightToLeft);
50
- }
51
- async function defaultProbeTarget(targetWorkspaceRoot) {
52
- try {
53
- const ancestor = await nearestExistingAncestor(targetWorkspaceRoot);
54
- await access(ancestor, constants.R_OK | constants.W_OK);
55
- let targetState = "absent";
56
- try {
57
- const target = await lstat(targetWorkspaceRoot);
58
- targetState = target.isDirectory() && (await readdir(targetWorkspaceRoot)).length === 0 ? "empty" : "non_empty";
59
- }
60
- catch (error) {
61
- if (error.code !== "ENOENT")
62
- throw error;
63
- }
64
- const filesystem = await statfs(ancestor, { bigint: true });
65
- const available = filesystem.bavail * filesystem.bsize;
66
- return Object.freeze({
67
- status: "available",
68
- targetState,
69
- availableBytes: available > BigInt(Number.MAX_SAFE_INTEGER) ? Number.MAX_SAFE_INTEGER : Number(available),
70
- writable: true,
71
- // Staging and final roots are siblings on this filesystem. A destructive rename probe belongs
72
- // to the separately approved write preflight, not this read-only doctor.
73
- atomicRenameSupported: true,
74
- });
75
- }
76
- catch (error) {
77
- const code = error.code;
78
- return Object.freeze({
79
- status: code === "EACCES" || code === "EPERM" ? "permission_denied" : "unavailable",
80
- targetState: "absent",
81
- availableBytes: 0,
82
- writable: false,
83
- atomicRenameSupported: false,
84
- });
85
- }
86
- }
87
- const issue = (code, detail) => Object.freeze({ code, detail });
88
- function inspectManifestLinks(manifest) {
89
- const warnings = [];
90
- const blockers = [];
91
- const credentialLinks = manifest.entries.filter((entry) => entry.link?.classification === "external" && entry.reason === "credential_material").length;
92
- const unapprovedLinks = manifest.entries.filter((entry) => entry.link?.classification === "external" && entry.reason !== "credential_material" && entry.reason !== "local_runtime").length;
93
- const brokenLinks = manifest.entries.filter((entry) => entry.link?.classification === "broken").length;
94
- const unsupportedItems = manifest.entries.filter((entry) => entry.type === "unsupported").length;
95
- if (credentialLinks > 0)
96
- warnings.push(issue("credential_link_excluded", `${credentialLinks} credential link(s) excluded from migration`));
97
- if (unapprovedLinks > 0)
98
- blockers.push(issue("external_symlink_unapproved", `${unapprovedLinks} external link(s) require an explicit exclusion or portable mapping`));
99
- if (brokenLinks > 0)
100
- blockers.push(issue("broken_symlink", `${brokenLinks} broken link(s) cannot be migrated safely`));
101
- if (unsupportedItems > 0)
102
- blockers.push(issue("unsupported_file_type", `${unsupportedItems} unsupported filesystem item(s) found`));
103
- if (manifest.summary.transformations > 0) {
104
- warnings.push(issue("target_transformations_required", `${manifest.summary.transformations} target-only transformation(s) must be verified during apply`));
105
- }
106
- return { warnings, blockers };
107
- }
108
- export async function runStorageDoctor(input, dependencies = {}) {
109
- const environment = validateStorageEnvironment(input.environment);
110
- const manifest = typeof input.manifest === "string"
111
- ? parseStorageManifest(input.manifest)
112
- : input.manifest === undefined
113
- ? await generateStorageManifest({
114
- migrationId: input.migrationId,
115
- generatedAt: input.generatedAt,
116
- environment,
117
- workspaceId: input.workspaceId,
118
- targetWorkspaceRoot: input.targetWorkspaceRoot,
119
- sources: input.sources,
120
- policy: input.policy,
121
- })
122
- : parseStorageManifest(JSON.stringify(input.manifest));
123
- const target = await (dependencies.probeTarget ?? defaultProbeTarget)(input.targetWorkspaceRoot);
124
- const warnings = [];
125
- const blockers = [];
126
- if (manifest.migrationId !== input.migrationId || manifest.workspaceId !== input.workspaceId.toLowerCase()
127
- || manifest.environment !== environment || manifest.targetWorkspaceRoot !== input.targetWorkspaceRoot) {
128
- blockers.push(issue("manifest_identity_mismatch", "manifest identity does not match the requested migration"));
129
- }
130
- if (!input.state.profileIdentityExact)
131
- blockers.push(issue("profile_identity_drift", "profile, service or managed registry identity drifted"));
132
- if ((await Promise.all(input.sources.map((source) => pathsOverlap(source.root, input.targetWorkspaceRoot))))
133
- .some(Boolean)) {
134
- blockers.push(issue("source_target_overlap", "target workspace and source roots must not contain one another"));
135
- }
136
- if (input.state.duplicateWorkspaceMappings > 0)
137
- blockers.push(issue("duplicate_workspace_mapping", "more than one profile maps to this workspace"));
138
- if (input.state.activeAgentExecutions > 0 || input.state.activeScheduledRuns > 0) {
139
- blockers.push(issue("active_execution", "active agent execution or scheduled run must finish before migration"));
140
- }
141
- if (target.status === "unavailable")
142
- blockers.push(issue("target_unavailable", "target storage is unavailable"));
143
- if (target.status === "permission_denied" || !target.writable)
144
- blockers.push(issue("target_permission_denied", "target storage is not writable by the daemon user"));
145
- if (target.targetState === "non_empty")
146
- blockers.push(issue("target_non_empty", "target workspace root is not empty"));
147
- const multiplier = input.freeSpaceMultiplier ?? 1.2;
148
- if (!Number.isFinite(multiplier) || multiplier < 1)
149
- throw new Error("free space multiplier must be at least 1");
150
- const requiredBytes = Math.ceil(manifest.summary.bytesToCopy * multiplier);
151
- if (target.status === "available" && target.availableBytes < requiredBytes) {
152
- blockers.push(issue("insufficient_space", `target requires at least ${requiredBytes} available bytes`));
153
- }
154
- if (!target.atomicRenameSupported)
155
- blockers.push(issue("atomic_rename_unsupported", "target filesystem rename semantics are not verified"));
156
- const links = inspectManifestLinks(manifest);
157
- warnings.push(...links.warnings);
158
- blockers.push(...links.blockers);
159
- const applyAllowed = blockers.length === 0;
160
- const status = !applyAllowed ? "blocked" : warnings.length > 0 ? "warnings" : "safe";
161
- const exitCode = status === "safe" ? 0 : status === "warnings" ? 2 : 3;
162
- return Object.freeze({
163
- status,
164
- exitCode,
165
- applyAllowed,
166
- environment,
167
- workspaceId: input.workspaceId.toLowerCase(),
168
- profile: input.profile,
169
- manifest,
170
- manifestDigest: storageManifestDigest(manifest),
171
- target,
172
- warnings: Object.freeze(warnings),
173
- blockers: Object.freeze(blockers),
174
- });
175
- }
@@ -1,29 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import { STORAGE_LAYOUT_VERSION } from "./storage-layout.js";
3
- function fingerprint(namespace, value) {
4
- return `sha256:${createHash("sha256").update(`${namespace}\0${value}`).digest("hex")}`;
5
- }
6
- export function storageIdFingerprint(storageId) {
7
- return fingerprint("storage", storageId);
8
- }
9
- export function storageHelloIdentity(input) {
10
- const { installation, layout } = input;
11
- if (installation.environment !== layout.environment
12
- || installation.workspaceId !== layout.workspaceId
13
- || installation.nowworkHome !== layout.persistentRoot
14
- || installation.runtimeRoot !== layout.runtimeRoot) {
15
- throw new Error("installation storage identity does not match resolved layout");
16
- }
17
- return Object.freeze({
18
- storageLayoutVersion: STORAGE_LAYOUT_VERSION,
19
- storageEnvironment: layout.environment,
20
- storageWorkspaceId: layout.workspaceId,
21
- storageIdFingerprint: storageIdFingerprint(installation.storageId),
22
- workspaceRootFingerprint: fingerprint("workspace-root", layout.workspaceRoot),
23
- daemonRootFingerprint: fingerprint("daemon-root", layout.daemonRoot),
24
- agentRootFingerprint: fingerprint("agent-root", layout.agentItemsRoot),
25
- ...(installation.storageMigrationId === undefined
26
- ? {}
27
- : { storageMigrationId: installation.storageMigrationId }),
28
- });
29
- }