@nowcrew/daemon 0.6.88-beta.3 → 0.6.88-beta.30

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 (129) hide show
  1. package/README.md +89 -14
  2. package/dist/agent-ability/controller.js +11 -1
  3. package/dist/agent-ability/git-errors.js +22 -0
  4. package/dist/agent-ability/materializer.js +21 -10
  5. package/dist/agent-ability/resolver.js +12 -4
  6. package/dist/agent-environment-contract.js +55 -0
  7. package/dist/agent-environment-control.js +24 -0
  8. package/dist/agent-environment-execution.js +98 -0
  9. package/dist/agent-environments.js +275 -0
  10. package/dist/agent-memory/policy.js +3 -1
  11. package/dist/automatic-daemon-installer.js +51 -2
  12. package/dist/automatic-install-target.js +8 -0
  13. package/dist/computer-main.js +18 -0
  14. package/dist/computer-profile.js +17 -4
  15. package/dist/computer-setup-client.js +318 -0
  16. package/dist/computer-setup-orchestration.js +203 -0
  17. package/dist/computer-setup.js +244 -0
  18. package/dist/config.js +30 -12
  19. package/dist/console-formatter.js +3 -3
  20. package/dist/console.js +15 -2
  21. package/dist/daemon-update-eligibility.js +1 -1
  22. package/dist/execution-journal.js +9 -6
  23. package/dist/execution-protocol.js +2 -1
  24. package/dist/execution-runner.js +16 -10
  25. package/dist/i18n.js +19 -0
  26. package/dist/list-models.js +13 -11
  27. package/dist/local-execution-lease.js +22 -0
  28. package/dist/local-executor.js +88 -92
  29. package/dist/local-memory/candidate.js +43 -0
  30. package/dist/local-memory/index-budget.js +8 -0
  31. package/dist/local-memory/inventory-cli.js +61 -0
  32. package/dist/local-memory/inventory-preview.js +159 -0
  33. package/dist/local-memory/inventory-source.js +159 -0
  34. package/dist/local-memory/pending-match.js +29 -0
  35. package/dist/local-memory/pending-query.js +100 -0
  36. package/dist/local-memory/pending-schema.js +29 -0
  37. package/dist/local-memory/pending-source.js +126 -0
  38. package/dist/local-memory/pending.js +34 -84
  39. package/dist/local-memory/prompt.js +20 -12
  40. package/dist/local-memory/read-cli.js +97 -12
  41. package/dist/local-memory/rollout.js +82 -0
  42. package/dist/local-memory/runtime.js +24 -17
  43. package/dist/local-memory/work-log-execution.js +31 -0
  44. package/dist/local-memory/work-log.js +58 -0
  45. package/dist/local-memory/write.js +27 -58
  46. package/dist/local-runtime-environment.js +50 -0
  47. package/dist/machine-environment-cache.js +147 -0
  48. package/dist/machine-environment-contract.js +10 -0
  49. package/dist/machine-environment-control.js +27 -0
  50. package/dist/machine-environment-policy.js +75 -0
  51. package/dist/machine-environment-process.js +110 -0
  52. package/dist/machine-info.js +70 -25
  53. package/dist/machine-runtime-environment.js +189 -0
  54. package/dist/machine-shell-environment-capture.js +135 -0
  55. package/dist/machine-windows-environment-capture.js +215 -0
  56. package/dist/machine-windows-environment-script.js +158 -0
  57. package/dist/main.js +4 -69
  58. package/dist/marketplace-controller.js +75 -0
  59. package/dist/marketplace-protocol.js +38 -0
  60. package/dist/marketplace-runner.js +234 -0
  61. package/dist/marketplace-serve.js +17 -0
  62. package/dist/message-file-preview.js +246 -0
  63. package/dist/native-thread-context.js +59 -0
  64. package/dist/pre-admission-rejection.js +64 -0
  65. package/dist/project-skills/execution-adapter.js +3 -2
  66. package/dist/project-skills/runtime-launch.js +14 -0
  67. package/dist/prompt.js +15 -15
  68. package/dist/provider-env.js +26 -15
  69. package/dist/runner.js +31 -19
  70. package/dist/runtime-environment-merge.js +16 -0
  71. package/dist/runtime-health-handler.js +10 -0
  72. package/dist/runtime-health.js +3 -1
  73. package/dist/runtime-path.js +22 -12
  74. package/dist/runtime-probe.js +35 -6
  75. package/dist/runtimes/acp-probe-transport.js +69 -0
  76. package/dist/runtimes/claude-models.js +20 -9
  77. package/dist/runtimes/claude-session-start-hook.js +24 -0
  78. package/dist/runtimes/claude-thread-runner.js +266 -0
  79. package/dist/runtimes/claude.js +23 -4
  80. package/dist/runtimes/codex-app-server-runner.js +43 -4
  81. package/dist/runtimes/deepseek-harness-launch.js +10 -0
  82. package/dist/runtimes/deepseek-harness-profile.js +92 -0
  83. package/dist/runtimes/deepseek-harness-runner.js +226 -0
  84. package/dist/runtimes/deepseek-harness.js +76 -15
  85. package/dist/runtimes/harness-probe-retry.js +42 -0
  86. package/dist/runtimes/hermes-models.js +2 -1
  87. package/dist/runtimes/kimi-acp-runner.js +84 -15
  88. package/dist/runtimes/opencode-runner.js +7 -1
  89. package/dist/runtimes/opencode.js +2 -2
  90. package/dist/serve.js +95 -184
  91. package/dist/server-url.js +25 -0
  92. package/dist/session.js +4 -0
  93. package/dist/slog.js +2 -1
  94. package/dist/storage-identity.js +0 -3
  95. package/dist/storage-runtime.js +2 -2
  96. package/dist/storage-serve-adapter.js +6 -72
  97. package/dist/supervised-runtime.js +28 -18
  98. package/dist/thread-input-delivery.js +217 -0
  99. package/dist/token.js +0 -1
  100. package/dist/workspace-control-handler.js +27 -0
  101. package/dist/workspace-fs.js +66 -15
  102. package/dist/workspace.js +1 -1
  103. package/package.json +5 -4
  104. package/dist/codex-home-launch.js +0 -7
  105. package/dist/codex-home-mode-controller.js +0 -68
  106. package/dist/daemon-migration-controller.js +0 -211
  107. package/dist/daemon-migration-wiring.js +0 -22
  108. package/dist/runtimes/codex-home-migration-cli.js +0 -26
  109. package/dist/runtimes/codex-home-migration.js +0 -112
  110. package/dist/runtimes/codex-home.js +0 -236
  111. package/dist/storage-control.js +0 -212
  112. package/dist/storage-copy.js +0 -628
  113. package/dist/storage-credential-guard.js +0 -21
  114. package/dist/storage-doctor.js +0 -228
  115. package/dist/storage-manifest.js +0 -1027
  116. package/dist/storage-migration-controller.js +0 -440
  117. package/dist/storage-migration-manifest.js +0 -105
  118. package/dist/storage-migration-policy.js +0 -100
  119. package/dist/storage-runtime-controller.js +0 -381
  120. package/dist/storage-runtime-readiness.js +0 -240
  121. package/dist/storage-safe-fs.js +0 -302
  122. package/dist/storage-status-delivery.js +0 -224
  123. package/dist/storage-status-replay.js +0 -140
  124. package/dist/storage-target-confirmation.js +0 -129
  125. package/dist/storage-worker-command.js +0 -37
  126. package/dist/storage-worker-executor.js +0 -449
  127. package/dist/storage-worker-handoff.js +0 -151
  128. package/dist/storage-worker-operation.js +0 -294
  129. package/dist/storage-worker-status.js +0 -259
@@ -1,440 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import { createReadStream } from "node:fs";
3
- import { chmod, copyFile, lstat, mkdir, open, readFile, unlink } from "node:fs/promises";
4
- import { basename, dirname, join } from "node:path";
5
- import { z } from "zod";
6
- import { atomicPrivateWrite } from "./atomic-private-write.js";
7
- import { materializeStorageManifest, publishStagedStorage, reconcileStorageManifest, verifyStagedStorage, } from "./storage-copy.js";
8
- import { storageManifestDigest } from "./storage-manifest.js";
9
- const ReceiptStatusSchema = z.enum([
10
- "preflight_passed",
11
- "backing_up",
12
- "initial_sync",
13
- "quiescing",
14
- "final_sync",
15
- "verified",
16
- "service_switched",
17
- "hello_confirmed",
18
- "completed",
19
- "failed",
20
- "rollback_pending",
21
- "rolled_back",
22
- "rollback_failed",
23
- ]);
24
- const ReceiptSchema = z.object({
25
- version: z.literal(1),
26
- migrationId: z.string().uuid(),
27
- environment: z.string().min(1),
28
- workspaceId: z.string().uuid(),
29
- profile: z.string().regex(/^[a-z0-9][a-z0-9_-]{0,47}$/u),
30
- ownerEpoch: z.number().int().positive().safe().optional(),
31
- ownershipClaimFingerprint: z.string().regex(/^sha256:[0-9a-f]{64}$/u).optional(),
32
- manifestDigest: z.string().regex(/^[0-9a-f]{64}$/u),
33
- targetFingerprint: z.string().regex(/^sha256:[0-9a-f]{64}$/u),
34
- status: ReceiptStatusSchema,
35
- history: z.array(z.object({ status: ReceiptStatusSchema, at: z.string().datetime({ offset: true }) }).strict()),
36
- artifacts: z.array(z.object({ name: z.string().min(1), sha256: z.string().regex(/^[0-9a-f]{64}$/u) }).strict()),
37
- errorCode: z.string().min(1).max(128).optional(),
38
- errorMessage: z.string().min(1).max(512).optional(),
39
- sourceStopAttempted: z.boolean().optional(),
40
- sourceStopped: z.boolean().optional(),
41
- targetTouched: z.boolean().optional(),
42
- }).strict().superRefine((receipt, context) => {
43
- if ((receipt.ownerEpoch === undefined) !== (receipt.ownershipClaimFingerprint === undefined)) {
44
- context.addIssue({ code: z.ZodIssueCode.custom, message: "storage receipt ownership identity must be complete" });
45
- }
46
- });
47
- const MigrationLockSchema = z.object({
48
- version: z.literal(1),
49
- migrationId: z.string().uuid(),
50
- pid: z.number().int().positive(),
51
- createdAt: z.string().datetime({ offset: true }),
52
- }).strict();
53
- const REQUIRED_BACKUP_ARTIFACTS = Object.freeze([
54
- "profile.json",
55
- "service-descriptor",
56
- "managed-service-registry.json",
57
- ]);
58
- export class StorageMigrationFailure extends Error {
59
- code;
60
- receipt;
61
- constructor(code, message, receipt, options) {
62
- super(message, options);
63
- this.name = "StorageMigrationFailure";
64
- this.code = code;
65
- if (receipt !== undefined)
66
- this.receipt = receipt;
67
- }
68
- }
69
- const TOKEN = /(?:\bsk_(?:machine|user|agent)_[A-Za-z0-9_-]{4,}\b|\bbearer\s+[^\s]+|(?:password|secret|token)\s*[:=]\s*[^\s]+)/giu;
70
- function safeError(error) {
71
- const message = error instanceof Error ? error.message : String(error);
72
- return message.replaceAll(TOKEN, "<redacted>").slice(0, 512) || "storage migration failed";
73
- }
74
- function targetFingerprint(path) {
75
- return `sha256:${createHash("sha256").update(path).digest("hex")}`;
76
- }
77
- async function digestFile(path) {
78
- const digest = createHash("sha256");
79
- await new Promise((resolvePromise, reject) => {
80
- const stream = createReadStream(path);
81
- stream.on("data", (chunk) => digest.update(chunk));
82
- stream.once("end", resolvePromise);
83
- stream.once("error", reject);
84
- });
85
- return digest.digest("hex");
86
- }
87
- function receiptPath(backupsRoot, migrationId) {
88
- return join(backupsRoot, migrationId, "receipt.json");
89
- }
90
- async function readReceipt(path) {
91
- try {
92
- return ReceiptSchema.parse(JSON.parse(await readFile(path, "utf8")));
93
- }
94
- catch (error) {
95
- if (error.code === "ENOENT")
96
- return null;
97
- throw new StorageMigrationFailure("receipt_invalid", `Invalid storage migration receipt: ${path}`, undefined, { cause: error });
98
- }
99
- }
100
- export function readStorageMigrationReceipt(backupsRoot, migrationId) {
101
- return readReceipt(receiptPath(backupsRoot, migrationId));
102
- }
103
- export async function recoverStorageMigration(input, dependencies) {
104
- const path = receiptPath(input.backupsRoot, input.migrationId);
105
- let receipt = await readReceipt(path);
106
- if (receipt === null || receipt.migrationId !== input.migrationId) {
107
- throw new StorageMigrationFailure("migration_receipt_missing", "storage migration recovery receipt is missing");
108
- }
109
- if (receipt.status === "completed" || receipt.status === "rolled_back")
110
- return receipt;
111
- const release = await acquireMigrationLock(input.backupsRoot, input.migrationId, dependencies.processAlive ?? systemProcessAlive);
112
- const now = dependencies.now ?? (() => new Date());
113
- const recoveryCode = "worker_restarted_recovery_required";
114
- const recoveryMessage = "detached storage migration worker restarted with an incomplete receipt";
115
- const transition = async (status, errorCode = recoveryCode, errorMessage = recoveryMessage) => {
116
- receipt = ReceiptSchema.parse({
117
- ...receipt,
118
- status,
119
- errorCode,
120
- errorMessage,
121
- history: [...receipt.history, { status, at: now().toISOString() }],
122
- });
123
- await writeReceipt(path, receipt);
124
- };
125
- try {
126
- if (receipt.sourceStopAttempted !== true) {
127
- if (receipt.status !== "failed")
128
- await transition("failed");
129
- return receipt;
130
- }
131
- if (receipt.status !== "failed" && receipt.status !== "rollback_pending" && receipt.status !== "rollback_failed") {
132
- await transition("failed");
133
- }
134
- if (receipt.status !== "rollback_pending")
135
- await transition("rollback_pending");
136
- try {
137
- if (receipt.targetTouched === true)
138
- await dependencies.removeTargetService();
139
- await dependencies.restoreSourceService();
140
- await transition("rolled_back");
141
- return receipt;
142
- }
143
- catch (error) {
144
- const message = safeError(error);
145
- await transition("rollback_failed", "rollback_failed", message).catch(() => undefined);
146
- throw new StorageMigrationFailure("rollback_failed", message, receipt, { cause: error });
147
- }
148
- }
149
- finally {
150
- await release().catch(() => undefined);
151
- }
152
- }
153
- const REPLAYABLE_STATUSES = new Set([
154
- "backing_up", "initial_sync", "quiescing", "final_sync", "verified", "service_switched",
155
- "completed", "failed", "rollback_pending", "rolled_back", "rollback_failed",
156
- ]);
157
- export function storageMigrationReplayStatuses(receipt) {
158
- return Object.freeze(receipt.history
159
- .map((entry) => entry.status)
160
- .filter((status) => REPLAYABLE_STATUSES.has(status)));
161
- }
162
- async function writeReceipt(path, receipt) {
163
- await atomicPrivateWrite(path, `${JSON.stringify(ReceiptSchema.parse(receipt), null, 2)}\n`);
164
- }
165
- function systemProcessAlive(pid) {
166
- try {
167
- process.kill(pid, 0);
168
- return true;
169
- }
170
- catch (error) {
171
- return error.code !== "ESRCH";
172
- }
173
- }
174
- async function acquireMigrationLock(backupsRoot, migrationId, processAlive) {
175
- await mkdir(backupsRoot, { recursive: true, mode: 0o700 });
176
- await chmod(backupsRoot, 0o700);
177
- const path = join(backupsRoot, ".active-storage-migration");
178
- for (let attempt = 0; attempt < 2; attempt += 1) {
179
- let handle;
180
- try {
181
- handle = await open(path, "wx", 0o600);
182
- await handle.writeFile(`${JSON.stringify(MigrationLockSchema.parse({
183
- version: 1,
184
- migrationId,
185
- pid: process.pid,
186
- createdAt: new Date().toISOString(),
187
- }))}\n`, "utf8");
188
- await handle.sync();
189
- await handle.close();
190
- return async () => { await unlink(path); };
191
- }
192
- catch (error) {
193
- await handle?.close();
194
- if (error.code !== "EEXIST")
195
- throw error;
196
- let owner;
197
- try {
198
- owner = MigrationLockSchema.parse(JSON.parse(await readFile(path, "utf8")));
199
- }
200
- catch (invalid) {
201
- throw new StorageMigrationFailure("migration_lock_invalid", "storage migration lock owner is invalid", undefined, { cause: invalid });
202
- }
203
- if (processAlive(owner.pid)) {
204
- throw new StorageMigrationFailure("migration_active", "another storage migration requires recovery or completion");
205
- }
206
- await unlink(path);
207
- }
208
- }
209
- throw new StorageMigrationFailure("migration_active", "storage migration lock could not be acquired safely");
210
- }
211
- function assertOwnedStaging(input) {
212
- if (dirname(input.stagingWorkspaceRoot) !== dirname(input.targetWorkspaceRoot)
213
- || !basename(input.stagingWorkspaceRoot).includes(input.doctor.manifest.migrationId)) {
214
- throw new StorageMigrationFailure("staging_identity_mismatch", "staging must be a migration-owned sibling of the target");
215
- }
216
- }
217
- async function backupArtifacts(input, receipt) {
218
- const providedNames = new Set(input.backupArtifacts.map((artifact) => artifact.name));
219
- const missing = REQUIRED_BACKUP_ARTIFACTS.filter((name) => !providedNames.has(name));
220
- if (missing.length > 0) {
221
- throw new StorageMigrationFailure("backup_artifact_missing", `required rollback artifacts are missing: ${missing.join(", ")}`);
222
- }
223
- for (const artifact of input.backupArtifacts) {
224
- if (!(await lstat(artifact.path)).isFile()) {
225
- throw new StorageMigrationFailure("backup_artifact_invalid", "backup artifact must be a file");
226
- }
227
- }
228
- const directory = join(input.backupsRoot, receipt.migrationId);
229
- const artifactsDirectory = join(directory, "artifacts");
230
- await mkdir(artifactsDirectory, { recursive: true, mode: 0o700 });
231
- await chmod(directory, 0o700);
232
- await chmod(artifactsDirectory, 0o700);
233
- await atomicPrivateWrite(join(directory, "manifest.json"), `${JSON.stringify(input.doctor.manifest, null, 2)}\n`);
234
- const artifacts = [];
235
- const names = new Set();
236
- for (const artifact of input.backupArtifacts) {
237
- if (!/^[a-z0-9][a-z0-9_.-]{0,127}$/u.test(artifact.name) || names.has(artifact.name)) {
238
- throw new StorageMigrationFailure("backup_artifact_invalid", "backup artifact names must be unique safe file names");
239
- }
240
- names.add(artifact.name);
241
- const destination = join(artifactsDirectory, artifact.name);
242
- await copyFile(artifact.path, destination);
243
- await chmod(destination, 0o600);
244
- artifacts.push(Object.freeze({ name: artifact.name, sha256: await digestFile(destination) }));
245
- }
246
- return ReceiptSchema.parse({ ...receipt, artifacts });
247
- }
248
- function unacceptedWarnings(doctor, acceptedWarnings) {
249
- return doctor.warnings.map((warning) => warning.code)
250
- .filter((code) => !acceptedWarnings.some((accepted) => accepted === code));
251
- }
252
- function validateFinalDoctor(initial, final, acceptedWarnings) {
253
- if (!final.applyAllowed || final.manifest.migrationId !== initial.manifest.migrationId
254
- || final.environment !== initial.environment || final.workspaceId !== initial.workspaceId
255
- || final.profile !== initial.profile || final.manifest.targetWorkspaceRoot !== initial.manifest.targetWorkspaceRoot
256
- || unacceptedWarnings(final, acceptedWarnings).length > 0) {
257
- throw new StorageMigrationFailure("final_doctor_failed", "final doctor did not confirm the exact migration identity");
258
- }
259
- }
260
- export async function runStorageMigration(input, dependencies) {
261
- if (!input.doctor.applyAllowed)
262
- throw new StorageMigrationFailure("doctor_blocked", "storage doctor did not allow apply");
263
- if (unacceptedWarnings(input.doctor, input.acceptedWarnings).length > 0) {
264
- throw new StorageMigrationFailure("doctor_warnings_unacknowledged", "storage doctor warnings require explicit acknowledgement");
265
- }
266
- if (input.targetWorkspaceRoot !== input.doctor.manifest.targetWorkspaceRoot) {
267
- throw new StorageMigrationFailure("target_identity_mismatch", "target does not match the doctor manifest");
268
- }
269
- assertOwnedStaging(input);
270
- const path = receiptPath(input.backupsRoot, input.doctor.manifest.migrationId);
271
- const existing = await readReceipt(path);
272
- if (existing?.status === "completed" || existing?.status === "rolled_back")
273
- return existing;
274
- if (existing !== null)
275
- throw new StorageMigrationFailure("migration_recovery_required", "an incomplete migration receipt requires recovery", existing);
276
- if (dependencies.isBusy())
277
- throw new StorageMigrationFailure("runtime_busy", "active executions must finish before migration");
278
- await dependencies.assertServiceIdentity().catch((error) => {
279
- throw new StorageMigrationFailure("service_identity_drift", safeError(error), undefined, { cause: error });
280
- });
281
- const release = await acquireMigrationLock(input.backupsRoot, input.doctor.manifest.migrationId, dependencies.processAlive ?? systemProcessAlive);
282
- const now = dependencies.now ?? (() => new Date());
283
- const materialize = dependencies.materialize ?? materializeStorageManifest;
284
- const reconcile = dependencies.reconcile ?? reconcileStorageManifest;
285
- const verify = dependencies.verify ?? verifyStagedStorage;
286
- const publish = dependencies.publish ?? publishStagedStorage;
287
- let receipt = ReceiptSchema.parse({
288
- version: 1,
289
- migrationId: input.doctor.manifest.migrationId,
290
- environment: input.doctor.environment,
291
- workspaceId: input.doctor.workspaceId,
292
- profile: input.doctor.profile,
293
- ...(input.ownership === undefined ? {} : input.ownership),
294
- manifestDigest: input.doctor.manifestDigest,
295
- targetFingerprint: targetFingerprint(input.targetWorkspaceRoot),
296
- status: "preflight_passed",
297
- history: [],
298
- artifacts: [],
299
- sourceStopAttempted: false,
300
- sourceStopped: false,
301
- targetTouched: false,
302
- });
303
- const transition = async (status, errorCode, errorMessage) => {
304
- receipt = ReceiptSchema.parse({
305
- ...receipt,
306
- status,
307
- history: [...receipt.history, { status, at: now().toISOString() }],
308
- ...(errorCode === undefined ? {} : { errorCode }),
309
- ...(errorMessage === undefined ? {} : { errorMessage }),
310
- });
311
- await writeReceipt(path, receipt);
312
- input.onStatus?.(status);
313
- };
314
- let sourceStopAttempted = false;
315
- let sourceStopped = false;
316
- let targetTouched = false;
317
- let failureCode = "storage_migration_failed";
318
- try {
319
- input.assertHealthy?.();
320
- await transition("preflight_passed");
321
- input.assertHealthy?.();
322
- await transition("backing_up");
323
- receipt = await backupArtifacts(input, receipt).catch((error) => {
324
- failureCode = error instanceof StorageMigrationFailure ? error.code : "backup_failed";
325
- throw error;
326
- });
327
- await writeReceipt(path, receipt);
328
- await transition("initial_sync");
329
- await materialize({
330
- manifest: input.doctor.manifest,
331
- sourceRoots: input.sourceRoots,
332
- stagingWorkspaceRoot: input.stagingWorkspaceRoot,
333
- }, input.assertHealthy === undefined ? {} : { assertHealthy: input.assertHealthy }).catch((error) => {
334
- failureCode = "initial_sync_failed";
335
- throw error;
336
- });
337
- await transition("quiescing");
338
- await dependencies.quiesceExecutions().catch((error) => {
339
- failureCode = "quiesce_failed";
340
- throw error;
341
- });
342
- await dependencies.waitForIdle().catch((error) => {
343
- failureCode = "quiesce_failed";
344
- throw error;
345
- });
346
- await dependencies.flushLocalState().catch((error) => {
347
- failureCode = "local_state_flush_failed";
348
- throw error;
349
- });
350
- sourceStopAttempted = true;
351
- receipt = ReceiptSchema.parse({ ...receipt, sourceStopAttempted: true });
352
- await writeReceipt(path, receipt);
353
- await dependencies.stopSourceService().catch((error) => {
354
- failureCode = "source_service_stop_failed";
355
- throw error;
356
- });
357
- sourceStopped = true;
358
- receipt = ReceiptSchema.parse({ ...receipt, sourceStopped: true });
359
- await writeReceipt(path, receipt);
360
- const finalDoctor = await (dependencies.refreshDoctor?.() ?? Promise.resolve(input.doctor));
361
- try {
362
- validateFinalDoctor(input.doctor, finalDoctor, input.acceptedWarnings);
363
- }
364
- catch (error) {
365
- failureCode = "final_doctor_failed";
366
- throw error;
367
- }
368
- await dependencies.assertServiceIdentity().catch((error) => {
369
- failureCode = "service_identity_drift";
370
- throw error;
371
- });
372
- await transition("final_sync");
373
- await reconcile({
374
- previousManifest: input.doctor.manifest,
375
- manifest: finalDoctor.manifest,
376
- sourceRoots: input.sourceRoots,
377
- stagingWorkspaceRoot: input.stagingWorkspaceRoot,
378
- ...(input.assertHealthy === undefined ? {} : { assertHealthy: input.assertHealthy }),
379
- }).catch((error) => {
380
- failureCode = "final_sync_failed";
381
- throw error;
382
- });
383
- receipt = ReceiptSchema.parse({ ...receipt, manifestDigest: storageManifestDigest(finalDoctor.manifest) });
384
- await atomicPrivateWrite(join(input.backupsRoot, receipt.migrationId, "manifest-final.json"), `${JSON.stringify(finalDoctor.manifest, null, 2)}\n`);
385
- await verify({ manifest: finalDoctor.manifest, stagingWorkspaceRoot: input.stagingWorkspaceRoot }).catch((error) => {
386
- failureCode = "verification_failed";
387
- throw error;
388
- });
389
- await transition("verified");
390
- await publish({ stagingWorkspaceRoot: input.stagingWorkspaceRoot, targetWorkspaceRoot: input.targetWorkspaceRoot }).catch((error) => {
391
- failureCode = "publish_failed";
392
- throw error;
393
- });
394
- targetTouched = true;
395
- receipt = ReceiptSchema.parse({ ...receipt, targetTouched: true });
396
- await writeReceipt(path, receipt);
397
- await dependencies.installTargetService().catch((error) => {
398
- failureCode = "target_service_failed";
399
- throw error;
400
- });
401
- await transition("service_switched");
402
- await dependencies.startTargetService().catch((error) => {
403
- failureCode = "target_service_failed";
404
- throw error;
405
- });
406
- await dependencies.confirmHello(receipt).catch((error) => {
407
- failureCode = "hello_timeout";
408
- throw error;
409
- });
410
- await transition("hello_confirmed");
411
- await transition("completed");
412
- return receipt;
413
- }
414
- catch (error) {
415
- const message = safeError(error);
416
- if (!sourceStopAttempted) {
417
- await transition("failed", failureCode, message).catch(() => undefined);
418
- throw new StorageMigrationFailure(failureCode, message, receipt, { cause: error });
419
- }
420
- await transition("failed", failureCode, message).catch(() => undefined);
421
- await transition("rollback_pending").catch(() => undefined);
422
- try {
423
- if (targetTouched)
424
- await dependencies.removeTargetService();
425
- await dependencies.restoreSourceService();
426
- await transition("rolled_back", failureCode, message);
427
- throw new StorageMigrationFailure(failureCode, message, receipt, { cause: error });
428
- }
429
- catch (rollbackError) {
430
- if (rollbackError instanceof StorageMigrationFailure)
431
- throw rollbackError;
432
- const rollbackMessage = safeError(rollbackError);
433
- await transition("rollback_failed", "rollback_failed", rollbackMessage).catch(() => undefined);
434
- throw new StorageMigrationFailure("rollback_failed", rollbackMessage, receipt, { cause: rollbackError });
435
- }
436
- }
437
- finally {
438
- await release().catch(() => undefined);
439
- }
440
- }
@@ -1,105 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import { posix, win32 } from "node:path";
3
- const HASH = /^[a-f0-9]{64}$/u;
4
- const AGENT_HANDLE = /^[a-z0-9][a-z0-9_-]{0,63}$/u;
5
- export class CredentialMaterialError extends Error {
6
- constructor(source) {
7
- super(`credential material detected in ${source}; rotate the credential before migration`);
8
- this.name = "CredentialMaterialError";
9
- }
10
- }
11
- function normalizedRelativePath(value) {
12
- const normalized = value.replaceAll("\\", "/");
13
- if (normalized.length === 0 || posix.isAbsolute(normalized) || win32.isAbsolute(normalized)) {
14
- throw new Error(`migration path must be relative: ${value}`);
15
- }
16
- const segments = normalized.split("/");
17
- if (segments.some((segment) => segment.length === 0 || segment === "." || segment === "..")) {
18
- throw new Error(`migration path must be normalized and relative: ${value}`);
19
- }
20
- const canonical = posix.normalize(normalized);
21
- if (canonical !== normalized || canonical === "." || canonical.startsWith("../")) {
22
- throw new Error(`migration path must be normalized and relative: ${value}`);
23
- }
24
- return canonical;
25
- }
26
- function digestOf(input) {
27
- const path = normalizedRelativePath(input.path);
28
- if (!HASH.test(input.sha256))
29
- throw new Error(`invalid sha256 for migration path: ${path}`);
30
- return Object.freeze({ path, sha256: input.sha256 });
31
- }
32
- function mapDaemonFile(input) {
33
- const file = digestOf(input);
34
- return Object.freeze({
35
- sourceRoot: "daemon-home",
36
- sourcePath: file.path,
37
- logicalDestination: `daemon/${file.path}`,
38
- sha256: file.sha256,
39
- });
40
- }
41
- function mapAgentFile(input) {
42
- const file = digestOf(input);
43
- const [handle, ...rest] = file.path.split("/");
44
- if (handle === undefined || !AGENT_HANDLE.test(handle) || rest.length === 0) {
45
- throw new Error(`agent migration path must start with an agent handle: ${file.path}`);
46
- }
47
- return Object.freeze({
48
- sourceRoot: "agents-root",
49
- sourcePath: file.path,
50
- logicalDestination: `agent-items/${handle}/${rest.join("/")}`,
51
- sha256: file.sha256,
52
- });
53
- }
54
- /** Build the immutable source -> logical destination contract used by backup and copy. */
55
- export function buildStorageMigrationMappings(input) {
56
- const mappings = [
57
- ...input.daemonFiles.map(mapDaemonFile),
58
- ...input.agentFiles.map(mapAgentFile),
59
- ].sort((left, right) => left.logicalDestination.localeCompare(right.logicalDestination));
60
- const destinations = new Set();
61
- for (const mapping of mappings) {
62
- if (destinations.has(mapping.logicalDestination)) {
63
- throw new Error(`duplicate migration destination: ${mapping.logicalDestination}`);
64
- }
65
- destinations.add(mapping.logicalDestination);
66
- }
67
- return Object.freeze(mappings);
68
- }
69
- const CREDENTIAL_PATTERN = /(?:\bsk_(?:machine|user|agent)_[A-Za-z0-9_-]{8,}\b|\bbearer\s+[A-Za-z0-9._~+/=-]{12,}\b|(?:api[-_]?key|access[-_]?token|refresh[-_]?token|password|secret|credential|authorization)\s*[:=]\s*["']?[^\s"';&]{8,})/iu;
70
- export function assertNoCredentialMaterial(value, source) {
71
- if (value !== null && CREDENTIAL_PATTERN.test(value))
72
- throw new CredentialMaterialError(source);
73
- }
74
- const SAFE_RECORD_KEYS = [
75
- "version", "generation", "platform", "serviceId", "profile", "daemonHome", "agentsRoot",
76
- "nodePath", "entryPath", "packageRoot", "npmPrefix", "descriptorPath", "descriptorSha256", "createdAt",
77
- ];
78
- function safeManagedRecord(record) {
79
- if (record === null)
80
- return null;
81
- assertNoCredentialMaterial(JSON.stringify(record), "managed service registry");
82
- const safe = Object.fromEntries(SAFE_RECORD_KEYS.flatMap((key) => (record[key] === undefined ? [] : [[key, record[key]]])));
83
- return Object.freeze(safe);
84
- }
85
- /** Project service identity for a backup manifest without persisting descriptor or secrets. */
86
- export function createSafeServiceManifest(input) {
87
- assertNoCredentialMaterial(input.spec.descriptor, "service descriptor");
88
- for (const [index, argument] of input.spec.daemonCommand.entries()) {
89
- assertNoCredentialMaterial(argument, `service command argument ${index}`);
90
- }
91
- const descriptorSha256 = input.spec.descriptor === null
92
- ? null
93
- : createHash("sha256").update(input.spec.descriptor).digest("hex");
94
- return Object.freeze({
95
- id: input.spec.id,
96
- platform: input.spec.platform,
97
- profile: input.spec.profile,
98
- descriptorPath: input.spec.descriptorPath,
99
- descriptorSha256,
100
- daemonCommand: Object.freeze([...input.spec.daemonCommand]),
101
- installed: input.snapshot.installed,
102
- running: input.snapshot.running,
103
- managedRecord: safeManagedRecord(input.managedRecord),
104
- });
105
- }
@@ -1,100 +0,0 @@
1
- import { readFile, realpath, stat } from "node:fs/promises";
2
- import { basename, dirname, isAbsolute, resolve } from "node:path";
3
- import { z } from "zod";
4
- import { assertStorageControlMaterialTokenFree } from "./storage-credential-guard.js";
5
- const CONTROL = /[\0\r\n]/u;
6
- const AbsolutePathSchema = z.string().min(1).max(32768)
7
- .refine((value) => isAbsolute(value) && !CONTROL.test(value), "must be an absolute single-line path");
8
- const RelativePathSchema = z.string().min(1).max(32768)
9
- .refine((value) => !isAbsolute(value) && !CONTROL.test(value), "must be a relative single-line path")
10
- .refine((value) => !value.split(/[\\/]/u).some((segment) => segment === "" || segment === "." || segment === ".."), "must be normalized");
11
- const SourceKindSchema = z.enum(["daemon-home", "agents-root", "legacy-agents-root"]);
12
- export const StorageMigrationWarningCodeSchema = z.enum([
13
- "credential_link_excluded",
14
- "target_transformations_required",
15
- ]);
16
- const PolicySchema = z.object({
17
- acceptedWarnings: z.array(StorageMigrationWarningCodeSchema).max(StorageMigrationWarningCodeSchema.options.length),
18
- exclusions: z.array(z.object({
19
- sourceKind: SourceKindSchema,
20
- sourcePath: RelativePathSchema,
21
- reason: z.string().min(1).max(256),
22
- }).strict()).max(100_000),
23
- transformations: z.array(z.object({
24
- sourceKind: SourceKindSchema,
25
- sourcePath: RelativePathSchema,
26
- logicalDestination: RelativePathSchema,
27
- sourceSha256: z.string().regex(/^[0-9a-f]{64}$/u),
28
- targetSha256: z.string().regex(/^[0-9a-f]{64}$/u),
29
- targetMode: z.string().regex(/^[0-7]{4}$/u),
30
- transform: z.literal("redact_live_like_tokens_v1"),
31
- }).strict()).max(100_000),
32
- }).strict();
33
- const DocumentSchema = z.object({
34
- version: z.literal(1),
35
- profile: z.string().regex(/^[a-z0-9][a-z0-9_-]{0,47}$/u),
36
- environment: z.string().regex(/^[a-z0-9][a-z0-9_-]{0,31}$/u),
37
- nowworkHome: AbsolutePathSchema,
38
- sourceDaemonHome: AbsolutePathSchema,
39
- sourceAgentsRoot: AbsolutePathSchema,
40
- policy: PolicySchema,
41
- }).strict();
42
- async function canonicalPath(value) {
43
- let candidate = resolve(value);
44
- const missing = [];
45
- for (;;) {
46
- try {
47
- return resolve(await realpath(candidate), ...missing);
48
- }
49
- catch (error) {
50
- const code = error.code;
51
- if (code !== "ENOENT" && code !== "ENOTDIR")
52
- throw error;
53
- }
54
- const parent = dirname(candidate);
55
- if (parent === candidate)
56
- return resolve(value);
57
- missing.unshift(basename(candidate));
58
- candidate = parent;
59
- }
60
- }
61
- async function samePath(left, right) {
62
- const [canonicalLeft, canonicalRight] = await Promise.all([canonicalPath(left), canonicalPath(right)]);
63
- return canonicalLeft === canonicalRight;
64
- }
65
- export async function readStorageMigrationPolicy(path, expected) {
66
- try {
67
- if (!isAbsolute(path) || CONTROL.test(path))
68
- throw new Error("policy path must be absolute");
69
- const metadata = await stat(path);
70
- if (!metadata.isFile() || (metadata.mode & 0o077) !== 0) {
71
- throw new Error("policy file permissions must be private");
72
- }
73
- if (process.getuid !== undefined && metadata.uid !== process.getuid()) {
74
- throw new Error("policy file must be owned by the daemon user");
75
- }
76
- const raw = await readFile(path, "utf8");
77
- assertStorageControlMaterialTokenFree(raw, "storage migration policy");
78
- const document = DocumentSchema.parse(JSON.parse(raw));
79
- const [sameNowworkHome, sameDaemonHome, sameAgentsRoot] = await Promise.all([
80
- samePath(document.nowworkHome, expected.nowworkHome),
81
- samePath(document.sourceDaemonHome, expected.sourceDaemonHome),
82
- samePath(document.sourceAgentsRoot, expected.sourceAgentsRoot),
83
- ]);
84
- if (document.profile !== expected.profile || document.environment !== expected.environment
85
- || !sameNowworkHome || !sameDaemonHome || !sameAgentsRoot) {
86
- throw new Error("policy binding does not match the active storage migration");
87
- }
88
- return Object.freeze({
89
- acceptedWarnings: Object.freeze([...new Set(document.policy.acceptedWarnings)]),
90
- exclusions: Object.freeze(document.policy.exclusions.map((entry) => Object.freeze(entry))),
91
- transformations: Object.freeze(document.policy.transformations.map((entry) => Object.freeze(entry))),
92
- });
93
- }
94
- catch (error) {
95
- const detail = error instanceof Error
96
- ? error.message.replace(/sk_(?:machine|user|agent)_[A-Za-z0-9_-]+/gu, "<redacted>").slice(0, 256)
97
- : "validation failed";
98
- throw new Error(`Invalid storage migration policy: ${detail}`, { cause: error });
99
- }
100
- }