@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.
Files changed (116) hide show
  1. package/README.md +64 -3
  2. package/dist/agent-ability/materializer.js +21 -10
  3. package/dist/agent-environment-contract.js +55 -0
  4. package/dist/agent-environment-control.js +24 -0
  5. package/dist/agent-environment-execution.js +98 -0
  6. package/dist/agent-environments.js +275 -0
  7. package/dist/automatic-daemon-installer.js +49 -1
  8. package/dist/computer-main.js +18 -0
  9. package/dist/computer-profile.js +17 -4
  10. package/dist/computer-setup-client.js +318 -0
  11. package/dist/computer-setup-orchestration.js +203 -0
  12. package/dist/computer-setup.js +244 -0
  13. package/dist/config.js +30 -12
  14. package/dist/console-formatter.js +3 -3
  15. package/dist/console.js +15 -2
  16. package/dist/daemon-update-eligibility.js +1 -1
  17. package/dist/daemon-updater.js +2 -12
  18. package/dist/execution-journal.js +9 -6
  19. package/dist/execution-protocol.js +1 -0
  20. package/dist/execution-runner.js +13 -8
  21. package/dist/i18n.js +19 -0
  22. package/dist/list-models.js +12 -12
  23. package/dist/local-execution-lease.js +22 -0
  24. package/dist/local-executor.js +57 -84
  25. package/dist/local-memory/candidate.js +43 -0
  26. package/dist/local-memory/index-budget.js +8 -0
  27. package/dist/local-memory/inventory-cli.js +61 -0
  28. package/dist/local-memory/inventory-preview.js +159 -0
  29. package/dist/local-memory/inventory-source.js +159 -0
  30. package/dist/local-memory/pending-match.js +29 -0
  31. package/dist/local-memory/pending-query.js +100 -0
  32. package/dist/local-memory/pending-schema.js +29 -0
  33. package/dist/local-memory/pending-source.js +126 -0
  34. package/dist/local-memory/pending.js +34 -84
  35. package/dist/local-memory/prompt.js +19 -12
  36. package/dist/local-memory/read-cli.js +97 -12
  37. package/dist/local-memory/rollout.js +82 -0
  38. package/dist/local-memory/runtime.js +23 -17
  39. package/dist/local-memory/work-log-execution.js +31 -0
  40. package/dist/local-memory/work-log.js +58 -0
  41. package/dist/local-memory/write.js +27 -58
  42. package/dist/local-runtime-environment.js +50 -0
  43. package/dist/machine-environment-cache.js +147 -0
  44. package/dist/machine-environment-contract.js +10 -0
  45. package/dist/machine-environment-control.js +27 -0
  46. package/dist/machine-environment-policy.js +75 -0
  47. package/dist/machine-environment-process.js +110 -0
  48. package/dist/machine-info.js +70 -25
  49. package/dist/machine-runtime-environment.js +189 -0
  50. package/dist/machine-shell-environment-capture.js +135 -0
  51. package/dist/machine-windows-environment-capture.js +215 -0
  52. package/dist/machine-windows-environment-script.js +158 -0
  53. package/dist/main.js +4 -69
  54. package/dist/marketplace-command.js +21 -0
  55. package/dist/marketplace-controller.js +80 -0
  56. package/dist/marketplace-protocol.js +38 -0
  57. package/dist/marketplace-remote.js +48 -0
  58. package/dist/marketplace-runner.js +235 -0
  59. package/dist/marketplace-serve.js +41 -0
  60. package/dist/marketplace-source.js +103 -0
  61. package/dist/message-file-preview.js +246 -0
  62. package/dist/pre-admission-rejection.js +64 -0
  63. package/dist/project-skills/execution-adapter.js +3 -2
  64. package/dist/project-skills/runtime-launch.js +14 -0
  65. package/dist/provider-env.js +26 -15
  66. package/dist/runner.js +17 -4
  67. package/dist/runtime-environment-merge.js +16 -0
  68. package/dist/runtime-health-handler.js +10 -0
  69. package/dist/runtime-health.js +3 -1
  70. package/dist/runtime-path.js +22 -12
  71. package/dist/runtime-probe.js +35 -6
  72. package/dist/runtimes/acp-probe-transport.js +69 -0
  73. package/dist/runtimes/claude-models.js +20 -9
  74. package/dist/runtimes/deepseek-harness-launch.js +10 -0
  75. package/dist/runtimes/deepseek-harness-profile.js +92 -0
  76. package/dist/runtimes/deepseek-harness-runner.js +226 -0
  77. package/dist/runtimes/deepseek-harness.js +76 -15
  78. package/dist/runtimes/harness-probe-retry.js +42 -0
  79. package/dist/runtimes/hermes-models.js +2 -1
  80. package/dist/runtimes/kimi-acp-runner.js +78 -13
  81. package/dist/runtimes/opencode.js +2 -2
  82. package/dist/serve.js +88 -178
  83. package/dist/storage-identity.js +0 -3
  84. package/dist/storage-serve-adapter.js +6 -72
  85. package/dist/supervised-runtime.js +17 -2
  86. package/dist/token.js +0 -1
  87. package/dist/workspace-control-handler.js +27 -0
  88. package/dist/workspace-fs.js +66 -15
  89. package/dist/workspace.js +1 -1
  90. package/package.json +13 -12
  91. package/dist/codex-home-launch.js +0 -7
  92. package/dist/codex-home-mode-controller.js +0 -68
  93. package/dist/daemon-migration-controller.js +0 -211
  94. package/dist/daemon-migration-wiring.js +0 -22
  95. package/dist/runtimes/codex-home-migration-cli.js +0 -26
  96. package/dist/runtimes/codex-home-migration.js +0 -112
  97. package/dist/runtimes/codex-home.js +0 -236
  98. package/dist/storage-control.js +0 -212
  99. package/dist/storage-copy.js +0 -628
  100. package/dist/storage-credential-guard.js +0 -21
  101. package/dist/storage-doctor.js +0 -228
  102. package/dist/storage-manifest.js +0 -1027
  103. package/dist/storage-migration-controller.js +0 -440
  104. package/dist/storage-migration-manifest.js +0 -105
  105. package/dist/storage-migration-policy.js +0 -100
  106. package/dist/storage-runtime-controller.js +0 -381
  107. package/dist/storage-runtime-readiness.js +0 -241
  108. package/dist/storage-safe-fs.js +0 -302
  109. package/dist/storage-status-delivery.js +0 -224
  110. package/dist/storage-status-replay.js +0 -140
  111. package/dist/storage-target-confirmation.js +0 -129
  112. package/dist/storage-worker-command.js +0 -37
  113. package/dist/storage-worker-executor.js +0 -447
  114. package/dist/storage-worker-handoff.js +0 -151
  115. package/dist/storage-worker-operation.js +0 -294
  116. package/dist/storage-worker-status.js +0 -259
@@ -1,129 +0,0 @@
1
- import { readFile } from "node:fs/promises";
2
- import { isAbsolute, join } from "node:path";
3
- import { z } from "zod";
4
- import { atomicPrivateWrite } from "./atomic-private-write.js";
5
- import { readStorageMigrationReceipt } from "./storage-migration-controller.js";
6
- const FingerprintSchema = z.string().regex(/^sha256:[0-9a-f]{64}$/u);
7
- const IdentitySchema = z.object({
8
- type: z.literal("daemon:storage-migration-confirmed"),
9
- migrationId: z.string().uuid(),
10
- environment: z.string().regex(/^[a-z0-9][a-z0-9_-]{0,31}$/u),
11
- workspaceId: z.string().uuid(),
12
- profile: z.string().regex(/^[a-z0-9][a-z0-9_-]{0,47}$/u),
13
- manifestDigest: z.string().regex(/^[0-9a-f]{64}$/u),
14
- targetStorageFingerprint: FingerprintSchema,
15
- }).strict();
16
- const AcknowledgementSchema = IdentitySchema.omit({ type: true }).extend({
17
- version: z.literal(1),
18
- confirmedAt: z.string().datetime({ offset: true }),
19
- }).strict();
20
- const delay = (milliseconds) => new Promise((resolvePromise) => {
21
- setTimeout(resolvePromise, milliseconds);
22
- });
23
- export function storageMigrationAcknowledgementPath(backupsRoot, migrationId) {
24
- if (!isAbsolute(backupsRoot))
25
- throw new Error("storage migration backups root must be absolute");
26
- return join(backupsRoot, z.string().uuid().parse(migrationId), "hello-confirmed.json");
27
- }
28
- export async function waitForStorageMigrationAcknowledgement(input) {
29
- const expected = IdentitySchema.omit({ type: true }).parse({
30
- migrationId: input.migrationId,
31
- environment: input.environment,
32
- workspaceId: input.workspaceId,
33
- profile: input.profile,
34
- manifestDigest: input.manifestDigest,
35
- targetStorageFingerprint: input.targetStorageFingerprint,
36
- });
37
- const timeoutMs = z.number().int().min(10).max(600_000).parse(input.timeoutMs ?? 120_000);
38
- const pollIntervalMs = z.number().int().min(1).max(5_000).parse(input.pollIntervalMs ?? 250);
39
- const path = storageMigrationAcknowledgementPath(input.backupsRoot, expected.migrationId);
40
- const deadline = Date.now() + timeoutMs;
41
- while (Date.now() <= deadline) {
42
- try {
43
- const acknowledgement = AcknowledgementSchema.parse(JSON.parse(await readFile(path, "utf8")));
44
- const { version: _version, confirmedAt: _confirmedAt, ...identity } = acknowledgement;
45
- if (JSON.stringify(identity) !== JSON.stringify(expected)) {
46
- throw new Error("storage migration acknowledgement identity mismatch");
47
- }
48
- return;
49
- }
50
- catch (error) {
51
- if (error.code !== "ENOENT")
52
- throw error;
53
- }
54
- await delay(pollIntervalMs);
55
- }
56
- throw new Error("storage migration hello confirmation timeout");
57
- }
58
- export function createStorageTargetConfirmationHandler(input) {
59
- const profile = z.string().regex(/^[a-z0-9][a-z0-9_-]{0,47}$/u).parse(input.profile);
60
- const completionTimeoutMs = z.number().int().min(10).max(600_000).parse(input.completionTimeoutMs ?? 120_000);
61
- const pollIntervalMs = z.number().int().min(1).max(5_000).parse(input.pollIntervalMs ?? 250);
62
- if (input.identity.storageMigrationId === undefined) {
63
- throw new Error("storage target confirmation requires a migration identity");
64
- }
65
- let active = null;
66
- const handle = async (raw, send) => {
67
- const rawType = typeof raw === "object" && raw !== null && "type" in raw ? raw.type : undefined;
68
- if (rawType !== "daemon:storage-migration-confirmed")
69
- return false;
70
- const confirmation = IdentitySchema.parse(raw);
71
- if (confirmation.migrationId !== input.identity.storageMigrationId
72
- || confirmation.environment !== input.identity.storageEnvironment
73
- || confirmation.workspaceId !== input.identity.storageWorkspaceId
74
- || confirmation.profile !== profile
75
- || confirmation.targetStorageFingerprint !== input.identity.storageIdFingerprint) {
76
- throw new Error("storage migration confirmation identity mismatch");
77
- }
78
- const key = JSON.stringify(confirmation);
79
- if (active !== null) {
80
- if (active.key !== key)
81
- throw new Error("storage migration confirmation changed while processing");
82
- return active.promise;
83
- }
84
- const processConfirmation = async () => {
85
- const receipt = await readStorageMigrationReceipt(input.backupsRoot, confirmation.migrationId);
86
- if (receipt === null || receipt.status !== "service_switched"
87
- || receipt.environment !== confirmation.environment
88
- || receipt.workspaceId !== confirmation.workspaceId
89
- || receipt.profile !== confirmation.profile
90
- || receipt.manifestDigest !== confirmation.manifestDigest) {
91
- throw new Error("storage migration confirmation receipt identity mismatch");
92
- }
93
- const { type: _type, ...confirmationIdentity } = confirmation;
94
- await atomicPrivateWrite(storageMigrationAcknowledgementPath(input.backupsRoot, confirmation.migrationId), `${JSON.stringify(AcknowledgementSchema.parse({
95
- version: 1,
96
- ...confirmationIdentity,
97
- confirmedAt: new Date().toISOString(),
98
- }), null, 2)}\n`);
99
- const deadline = Date.now() + completionTimeoutMs;
100
- while (Date.now() <= deadline) {
101
- const current = await readStorageMigrationReceipt(input.backupsRoot, confirmation.migrationId);
102
- if (current?.status === "completed") {
103
- send({
104
- type: "daemon:storage-migration-status",
105
- migrationId: confirmation.migrationId,
106
- status: "completed",
107
- environment: confirmation.environment,
108
- workspaceId: confirmation.workspaceId,
109
- profile: confirmation.profile,
110
- });
111
- return true;
112
- }
113
- if (current !== null && ["failed", "rolled_back", "rollback_failed"].includes(current.status)) {
114
- throw new Error(`storage migration ended before completion: ${current.status}`);
115
- }
116
- await delay(pollIntervalMs);
117
- }
118
- throw new Error("storage migration worker completion timeout");
119
- };
120
- const promise = processConfirmation();
121
- active = { key, promise };
122
- void promise.catch(() => {
123
- if (active?.key === key)
124
- active = null;
125
- });
126
- return promise;
127
- };
128
- return Object.freeze({ handle });
129
- }
@@ -1,37 +0,0 @@
1
- import { posix, win32 } from "node:path";
2
- import { executeStorageMigrationWorker, recoverStorageMigrationWorker, } from "./storage-worker-executor.js";
3
- import { completeStorageMigrationWorkerServiceForHandoff } from "./storage-worker-handoff.js";
4
- import { startStorageWorkerHeartbeat, writeStorageWorkerFailureStatus, } from "./storage-worker-status.js";
5
- export async function runStorageWorkerCommand(args, execute = executeStorageMigrationWorker, writeFailure = writeStorageWorkerFailureStatus, startHeartbeat = startStorageWorkerHeartbeat, completeService = completeStorageMigrationWorkerServiceForHandoff, recover = recoverStorageMigrationWorker) {
6
- const command = args[0];
7
- if (command !== "storage-migration-worker" && command !== "storage-migration-recover")
8
- return null;
9
- const path = args[1] === "--job" ? args[2] : undefined;
10
- if (args.length !== 3 || path === undefined || path.length === 0
11
- || /[\0\r\n]/u.test(path) || (!posix.isAbsolute(path) && !win32.isAbsolute(path))) {
12
- throw new Error("Invalid storage migration worker command");
13
- }
14
- const stopHeartbeat = await startHeartbeat(path);
15
- const assertHeartbeatHealthy = stopHeartbeat.assertHealthy;
16
- try {
17
- if (command === "storage-migration-recover") {
18
- await (assertHeartbeatHealthy === undefined
19
- ? recover(path)
20
- : recover(path, { assertHeartbeatHealthy }));
21
- }
22
- else {
23
- await (assertHeartbeatHealthy === undefined
24
- ? execute(path)
25
- : execute(path, { assertHeartbeatHealthy }));
26
- }
27
- }
28
- catch (error) {
29
- await writeFailure(path, error).catch(() => undefined);
30
- throw error;
31
- }
32
- finally {
33
- await stopHeartbeat().catch(() => undefined);
34
- await completeService(path);
35
- }
36
- return 0;
37
- }
@@ -1,447 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import { realpathSync } from "node:fs";
3
- import { buildServiceSpec, captureExactService, serviceAction, systemCommandRunner, } from "./computer-service.js";
4
- import { listProfiles, loadProfile, removeProfile, saveProfile, } from "./computer-profile.js";
5
- import { assertManagedServiceIdentity, installManagedService, uninstallManagedService, uninstallUnregisteredManagedService, } from "./managed-service-lifecycle.js";
6
- import { readManagedServiceRegistry, normalizeManagedServiceRecord, serviceDescriptorSha256, } from "./managed-service-registry.js";
7
- import { readInstallations, writeInstallations, } from "./storage-config.js";
8
- import { runStorageDoctor } from "./storage-doctor.js";
9
- import { readStorageMigrationReceipt, recoverStorageMigration, runStorageMigration, StorageMigrationFailure, } from "./storage-migration-controller.js";
10
- import { materializeStorageManifest, publishStagedStorage, reconcileStorageManifest, verifyStagedStorage, } from "./storage-copy.js";
11
- import { readStorageWorkerHandoff } from "./storage-worker-handoff.js";
12
- import { readStorageWorkerOperation } from "./storage-worker-operation.js";
13
- import { storageIdFingerprint } from "./storage-identity.js";
14
- import { waitForStorageMigrationAcknowledgement } from "./storage-target-confirmation.js";
15
- import { resolveDaemonLocalStateRoot } from "./daemon-local-home.js";
16
- import { serverUrlsMatch } from "./server-url.js";
17
- const defaultServiceDependencies = () => ({
18
- assertManagedServiceIdentity,
19
- serviceAction,
20
- uninstallManagedService,
21
- uninstallUnregisteredManagedService,
22
- installManagedService,
23
- captureExactService,
24
- readManagedServices: readManagedServiceRegistry,
25
- listProfiles,
26
- loadProfile,
27
- saveProfile,
28
- removeProfile,
29
- readInstallations,
30
- writeInstallations,
31
- });
32
- function serviceSpec(operation, record, profileHome, layout = "target") {
33
- return buildServiceSpec({
34
- platform: operation.service.platform,
35
- profile: operation.profile,
36
- userHome: realpathSync.native(operation.service.userHome),
37
- uid: operation.service.uid,
38
- nodePath: record.nodePath,
39
- entryPath: record.entryPath,
40
- profileHome,
41
- ...(layout === "source" || operation.service.serviceEnvironment === undefined
42
- ? {}
43
- : { environment: operation.service.serviceEnvironment }),
44
- });
45
- }
46
- function assertRecordMatchesSpec(record, spec) {
47
- const normalizedDescriptorPath = spec.descriptorPath === null
48
- ? null
49
- : normalizeManagedServiceRecord({ ...record, descriptorPath: spec.descriptorPath }).descriptorPath;
50
- const mismatches = [
51
- spec.descriptor === null ? "descriptor" : null,
52
- spec.descriptorPath === null ? "descriptorPath" : null,
53
- record.platform !== spec.platform ? "platform" : null,
54
- record.serviceId !== spec.id ? "serviceId" : null,
55
- record.profile !== spec.profile ? "profile" : null,
56
- record.descriptorPath !== normalizedDescriptorPath ? "descriptorPath" : null,
57
- spec.descriptor !== null && record.descriptorSha256 !== serviceDescriptorSha256(spec.descriptor)
58
- ? "descriptorSha256"
59
- : null,
60
- ].filter((value) => value !== null);
61
- if (mismatches.length > 0) {
62
- throw new Error(`Managed service record does not match operation service '${record.serviceId}': ${mismatches.join(", ")}`);
63
- }
64
- }
65
- function sameRecord(left, right) {
66
- return JSON.stringify(left) === JSON.stringify(right);
67
- }
68
- function sameInstallation(left, right) {
69
- return JSON.stringify(left) === JSON.stringify(right);
70
- }
71
- function sameProfile(left, right) {
72
- return JSON.stringify(left) === JSON.stringify(right);
73
- }
74
- export function createStorageWorkerServiceLifecycle(operation, injected = {}) {
75
- const dependencies = { ...defaultServiceDependencies(), ...injected };
76
- const sourceLocalHome = resolveDaemonLocalStateRoot({
77
- userHome: operation.service.userHome,
78
- platform: operation.service.platform,
79
- profile: operation.service.sourceRecord.profile,
80
- daemonHome: operation.service.sourceRecord.daemonHome,
81
- });
82
- const targetLocalHome = resolveDaemonLocalStateRoot({
83
- userHome: operation.service.userHome,
84
- platform: operation.service.platform,
85
- profile: operation.service.targetRecord.profile,
86
- daemonHome: operation.service.targetRecord.daemonHome,
87
- ...(operation.service.serviceEnvironment?.NOWWORK_LOCAL_HOME === undefined
88
- ? {} : { explicit: operation.service.serviceEnvironment.NOWWORK_LOCAL_HOME }),
89
- ...(operation.service.serviceEnvironment?.NOWWORK_LOCAL_HOME_MODE === undefined
90
- ? {} : { mode: operation.service.serviceEnvironment.NOWWORK_LOCAL_HOME_MODE }),
91
- });
92
- const sourceRegistry = { userHome: operation.service.userHome, localHome: sourceLocalHome };
93
- const targetRegistry = { userHome: operation.service.userHome, localHome: targetLocalHome };
94
- const sourceRecord = operation.service.sourceRecord;
95
- const targetRecord = operation.service.targetRecord;
96
- const sourceSpec = serviceSpec(operation, sourceRecord, operation.migration.sourceRoots["daemon-home"]);
97
- const targetSpec = serviceSpec(operation, targetRecord, operation.installation.configRoot);
98
- assertRecordMatchesSpec(sourceRecord, sourceSpec);
99
- assertRecordMatchesSpec(targetRecord, targetSpec);
100
- let sourceProfile = null;
101
- let savedTargetProfile = null;
102
- let baselineInstallations = null;
103
- let targetProfileCreated = false;
104
- let targetInstallationWritten = false;
105
- const assertSourceIdentity = () => dependencies.assertManagedServiceIdentity({
106
- spec: sourceSpec,
107
- record: sourceRecord,
108
- registry: sourceRegistry,
109
- });
110
- const readTargetInstallations = () => dependencies.readInstallations({
111
- configRoot: operation.installation.configRoot,
112
- });
113
- const assertTargetMetadataUnchanged = async () => {
114
- const profiles = await dependencies.listProfiles(operation.installation.configRoot);
115
- if (profiles.includes(operation.profile))
116
- throw new Error(`Target profile '${operation.profile}' already exists`);
117
- const current = await readTargetInstallations();
118
- if (baselineInstallations !== null && JSON.stringify(current) !== JSON.stringify(baselineInstallations)) {
119
- throw new Error("Target installation registry changed after worker preflight");
120
- }
121
- if (current.some((record) => record.profile === operation.profile
122
- || record.installationId === operation.installation.target.installationId)) {
123
- throw new Error("Target installation identity already exists");
124
- }
125
- return current;
126
- };
127
- const preflight = async () => {
128
- await assertSourceIdentity();
129
- const source = await dependencies.captureExactService(sourceSpec, systemCommandRunner);
130
- if (!source.installed || !source.exact || source.running !== operation.service.sourceWasRunning) {
131
- throw new Error("Source service state does not match worker operation");
132
- }
133
- const target = await dependencies.captureExactService(targetSpec, systemCommandRunner);
134
- if (target.installed)
135
- throw new Error(`Target service '${targetSpec.id}' already exists`);
136
- sourceProfile = await dependencies.loadProfile(operation.profile, sourceRecord.daemonHome);
137
- if (sourceProfile.name !== operation.profile
138
- || !serverUrlsMatch(sourceProfile.serverUrl, operation.installation.target.serverUrl)) {
139
- throw new Error("Source profile does not match target installation identity");
140
- }
141
- baselineInstallations = await assertTargetMetadataUnchanged();
142
- };
143
- const stopSourceService = async () => {
144
- await dependencies.serviceAction(sourceSpec, "stop", systemCommandRunner);
145
- const stopped = await dependencies.captureExactService(sourceSpec, systemCommandRunner);
146
- if (!stopped.installed || !stopped.exact || stopped.running) {
147
- throw new Error(`Source service '${sourceSpec.id}' did not stop exactly`);
148
- }
149
- };
150
- const installTargetService = async () => {
151
- if (sourceProfile === null || baselineInstallations === null)
152
- throw new Error("Worker service preflight is required");
153
- const installations = await assertTargetMetadataUnchanged();
154
- await assertSourceIdentity();
155
- await dependencies.uninstallManagedService({
156
- spec: sourceSpec,
157
- record: sourceRecord,
158
- runner: systemCommandRunner,
159
- registry: sourceRegistry,
160
- });
161
- const { version: _version, ...profile } = sourceProfile;
162
- savedTargetProfile = await dependencies.saveProfile({
163
- ...profile,
164
- agentsRoot: targetRecord.agentsRoot,
165
- }, operation.installation.configRoot);
166
- targetProfileCreated = true;
167
- await dependencies.writeInstallations([...installations, operation.installation.target].sort((left, right) => left.profile.localeCompare(right.profile)), { configRoot: operation.installation.configRoot });
168
- targetInstallationWritten = true;
169
- await dependencies.installManagedService({
170
- spec: targetSpec,
171
- record: targetRecord,
172
- runner: systemCommandRunner,
173
- registry: targetRegistry,
174
- });
175
- const target = await dependencies.captureExactService(targetSpec, systemCommandRunner);
176
- if (!target.installed || !target.exact || target.running) {
177
- throw new Error(`Target service '${targetSpec.id}' was not installed exactly in stopped state`);
178
- }
179
- };
180
- const startTargetService = async () => {
181
- await dependencies.serviceAction(targetSpec, "start", systemCommandRunner);
182
- const target = await dependencies.captureExactService(targetSpec, systemCommandRunner);
183
- if (!target.installed || !target.exact || !target.running) {
184
- throw new Error(`Target service '${targetSpec.id}' did not start exactly`);
185
- }
186
- };
187
- const removeTargetService = async () => {
188
- const records = await dependencies.readManagedServices(targetRegistry);
189
- const registered = records.find((record) => record.serviceId === targetRecord.serviceId
190
- || (record.profile === targetRecord.profile && record.daemonHome === targetRecord.daemonHome));
191
- if (registered !== undefined && !sameRecord(registered, targetRecord)) {
192
- throw new Error(`Target service '${targetSpec.id}' identity changed before rollback`);
193
- }
194
- if (registered !== undefined) {
195
- await dependencies.uninstallManagedService({
196
- spec: targetSpec,
197
- record: targetRecord,
198
- runner: systemCommandRunner,
199
- registry: targetRegistry,
200
- });
201
- }
202
- else {
203
- const target = await dependencies.captureExactService(targetSpec, systemCommandRunner);
204
- if (target.installed && !target.exact)
205
- throw new Error(`Target service '${targetSpec.id}' changed before rollback`);
206
- if (target.installed) {
207
- await dependencies.uninstallUnregisteredManagedService({
208
- spec: targetSpec,
209
- daemonHome: targetRecord.daemonHome,
210
- npmPrefix: targetRecord.npmPrefix,
211
- runner: systemCommandRunner,
212
- registry: targetRegistry,
213
- });
214
- }
215
- }
216
- const installations = await readTargetInstallations();
217
- const targetInstallation = installations.find((record) => record.profile === operation.profile
218
- || record.installationId === operation.installation.target.installationId);
219
- if (targetInstallation !== undefined) {
220
- if (!sameInstallation(targetInstallation, operation.installation.target)) {
221
- throw new Error("Target installation identity changed before rollback");
222
- }
223
- await dependencies.writeInstallations(installations.filter((record) => !sameInstallation(record, operation.installation.target)), { configRoot: operation.installation.configRoot });
224
- targetInstallationWritten = false;
225
- }
226
- const profiles = await dependencies.listProfiles(operation.installation.configRoot);
227
- if (profiles.includes(operation.profile)) {
228
- const current = await dependencies.loadProfile(operation.profile, operation.installation.configRoot);
229
- const original = sourceProfile
230
- ?? await dependencies.loadProfile(operation.profile, sourceRecord.daemonHome);
231
- const { version: _version, ...profile } = original;
232
- const expected = savedTargetProfile ?? { version: 1, ...profile, agentsRoot: targetRecord.agentsRoot };
233
- if (!sameProfile(current, expected)) {
234
- throw new Error("Target profile identity changed before rollback");
235
- }
236
- await dependencies.removeProfile(operation.profile, operation.installation.configRoot);
237
- targetProfileCreated = false;
238
- }
239
- };
240
- const restoreSourceService = async () => {
241
- const current = await dependencies.captureExactService(sourceSpec, systemCommandRunner);
242
- if (current.installed && !current.exact) {
243
- throw new Error(`Source service '${sourceSpec.id}' changed before rollback`);
244
- }
245
- if (!current.installed) {
246
- await dependencies.installManagedService({
247
- spec: sourceSpec,
248
- record: sourceRecord,
249
- runner: systemCommandRunner,
250
- registry: sourceRegistry,
251
- });
252
- }
253
- const afterInstall = current.installed
254
- ? current
255
- : await dependencies.captureExactService(sourceSpec, systemCommandRunner);
256
- if (operation.service.sourceWasRunning && !afterInstall.running) {
257
- await dependencies.serviceAction(sourceSpec, "start", systemCommandRunner);
258
- }
259
- else if (!operation.service.sourceWasRunning && afterInstall.running) {
260
- await dependencies.serviceAction(sourceSpec, "stop", systemCommandRunner);
261
- }
262
- const restored = await dependencies.captureExactService(sourceSpec, systemCommandRunner);
263
- if (!restored.installed || !restored.exact || restored.running !== operation.service.sourceWasRunning) {
264
- throw new Error(`Source service '${sourceSpec.id}' was not restored exactly`);
265
- }
266
- };
267
- return Object.freeze({
268
- preflight,
269
- assertServiceIdentity: assertSourceIdentity,
270
- isBusy: () => false,
271
- quiesceExecutions: async () => undefined,
272
- waitForIdle: async () => undefined,
273
- flushLocalState: async () => undefined,
274
- stopSourceService,
275
- installTargetService,
276
- startTargetService,
277
- removeTargetService,
278
- restoreSourceService,
279
- confirmHello: (receipt) => waitForStorageMigrationAcknowledgement({
280
- backupsRoot: operation.migration.backupsRoot,
281
- migrationId: operation.migrationId,
282
- environment: operation.environment,
283
- workspaceId: operation.workspaceId,
284
- profile: operation.profile,
285
- manifestDigest: receipt.manifestDigest,
286
- targetStorageFingerprint: storageIdFingerprint(operation.installation.target.storageId),
287
- }),
288
- });
289
- }
290
- function validateFreshDoctor(operation, doctor) {
291
- if (!doctor.applyAllowed
292
- || doctor.manifestDigest !== operation.expectedManifestDigest
293
- || doctor.manifest.migrationId !== operation.migrationId
294
- || doctor.environment !== operation.environment
295
- || doctor.workspaceId !== operation.workspaceId
296
- || doctor.profile !== operation.profile
297
- || doctor.manifest.targetWorkspaceRoot !== operation.migration.targetWorkspaceRoot) {
298
- throw new StorageMigrationFailure("worker_doctor_mismatch", "fresh worker doctor digest or identity does not match the approved operation");
299
- }
300
- }
301
- function workerDoctorInput(operation, generatedAt = operation.doctorInput.generatedAt) {
302
- const { freeSpaceMultiplier, manifestStoreRoot, ...input } = operation.doctorInput;
303
- return {
304
- ...input,
305
- generatedAt,
306
- ...(manifestStoreRoot === undefined ? {} : { manifestStoreRoot }),
307
- ...(freeSpaceMultiplier === undefined ? {} : { freeSpaceMultiplier }),
308
- };
309
- }
310
- function workerSourceRoots(operation) {
311
- return Object.fromEntries(Object.entries(operation.migration.sourceRoots)
312
- .filter((entry) => entry[1] !== undefined));
313
- }
314
- export async function executeStorageMigrationWorker(handoffPath, injected = {}) {
315
- const dependencies = {
316
- runDoctor: (input) => runStorageDoctor(input),
317
- runMigration: runStorageMigration,
318
- recoverMigration: recoverStorageMigration,
319
- readReceipt: readStorageMigrationReceipt,
320
- createServiceLifecycle: (operation) => createStorageWorkerServiceLifecycle(operation),
321
- now: () => new Date(),
322
- ...injected,
323
- };
324
- const handoff = await readStorageWorkerHandoff(handoffPath);
325
- const operation = await readStorageWorkerOperation(handoff.operationPath, handoff.operationSha256);
326
- const assertHeartbeatHealthy = dependencies.assertHeartbeatHealthy ?? (() => undefined);
327
- if (handoff.migrationId !== operation.migrationId || handoff.profile !== operation.profile) {
328
- throw new StorageMigrationFailure("worker_handoff_mismatch", "worker handoff does not match operation identity");
329
- }
330
- const lifecycle = dependencies.createServiceLifecycle(operation);
331
- const existing = await dependencies.readReceipt(operation.migration.backupsRoot, operation.migrationId);
332
- if (existing !== null) {
333
- if (existing.environment !== operation.environment || existing.workspaceId !== operation.workspaceId
334
- || existing.profile !== operation.profile) {
335
- throw new StorageMigrationFailure("worker_receipt_mismatch", "worker receipt does not match operation identity");
336
- }
337
- if (existing.status === "completed" || existing.status === "rolled_back")
338
- return existing;
339
- return dependencies.recoverMigration({
340
- backupsRoot: operation.migration.backupsRoot,
341
- migrationId: operation.migrationId,
342
- }, { ...lifecycle, now: dependencies.now });
343
- }
344
- assertHeartbeatHealthy();
345
- await lifecycle.preflight();
346
- assertHeartbeatHealthy();
347
- const doctor = await dependencies.runDoctor(workerDoctorInput(operation));
348
- assertHeartbeatHealthy();
349
- validateFreshDoctor(operation, doctor);
350
- const guardedMaterialize = async (input, hooks = {}) => materializeStorageManifest(input, {
351
- ...hooks,
352
- afterSourceLstat: async (path) => {
353
- await hooks.afterSourceLstat?.(path);
354
- assertHeartbeatHealthy();
355
- },
356
- afterTargetParentOpen: async (path) => {
357
- await hooks.afterTargetParentOpen?.(path);
358
- assertHeartbeatHealthy();
359
- },
360
- });
361
- const guardedVerify = async (input, hooks = {}) => verifyStagedStorage(input, {
362
- ...hooks,
363
- afterTargetLstat: async (path) => {
364
- await hooks.afterTargetLstat?.(path);
365
- assertHeartbeatHealthy();
366
- },
367
- });
368
- const guardedReconcile = async (input) => {
369
- assertHeartbeatHealthy();
370
- const result = await reconcileStorageManifest(input);
371
- assertHeartbeatHealthy();
372
- return result;
373
- };
374
- const guardedPublish = async (input, deps = {}) => {
375
- assertHeartbeatHealthy();
376
- await publishStagedStorage(input, deps);
377
- assertHeartbeatHealthy();
378
- };
379
- const receipt = await dependencies.runMigration({
380
- doctor,
381
- sourceRoots: workerSourceRoots(operation),
382
- stagingWorkspaceRoot: operation.migration.stagingWorkspaceRoot,
383
- targetWorkspaceRoot: operation.migration.targetWorkspaceRoot,
384
- backupsRoot: operation.migration.backupsRoot,
385
- backupArtifacts: operation.migration.backupArtifacts,
386
- ...(operation.migration.ownership === undefined ? {} : { ownership: operation.migration.ownership }),
387
- acceptedWarnings: operation.migration.acceptedWarnings,
388
- onStatus: () => assertHeartbeatHealthy(),
389
- assertHealthy: assertHeartbeatHealthy,
390
- }, {
391
- ...lifecycle,
392
- now: dependencies.now,
393
- materialize: guardedMaterialize,
394
- reconcile: guardedReconcile,
395
- verify: guardedVerify,
396
- publish: guardedPublish,
397
- refreshDoctor: async () => {
398
- const refreshed = await dependencies.runDoctor(workerDoctorInput(operation, dependencies.now().toISOString()));
399
- if (refreshed.manifest.migrationId !== operation.migrationId
400
- || refreshed.environment !== operation.environment
401
- || refreshed.workspaceId !== operation.workspaceId
402
- || refreshed.profile !== operation.profile) {
403
- throw new StorageMigrationFailure("final_doctor_failed", "final worker doctor identity changed");
404
- }
405
- return refreshed;
406
- },
407
- });
408
- assertHeartbeatHealthy();
409
- return receipt;
410
- }
411
- export async function recoverStorageMigrationWorker(handoffPath, injected = {}) {
412
- const dependencies = {
413
- runDoctor: (input) => runStorageDoctor(input),
414
- runMigration: runStorageMigration,
415
- recoverMigration: recoverStorageMigration,
416
- readReceipt: readStorageMigrationReceipt,
417
- createServiceLifecycle: (operation) => createStorageWorkerServiceLifecycle(operation),
418
- now: () => new Date(),
419
- ...injected,
420
- };
421
- const handoff = await readStorageWorkerHandoff(handoffPath);
422
- const operation = await readStorageWorkerOperation(handoff.operationPath, handoff.operationSha256);
423
- const assertHeartbeatHealthy = dependencies.assertHeartbeatHealthy ?? (() => undefined);
424
- assertHeartbeatHealthy();
425
- if (handoff.migrationId !== operation.migrationId || handoff.profile !== operation.profile) {
426
- throw new StorageMigrationFailure("worker_handoff_mismatch", "worker handoff does not match operation identity");
427
- }
428
- const receipt = await dependencies.readReceipt(operation.migration.backupsRoot, operation.migrationId);
429
- if (receipt === null) {
430
- throw new StorageMigrationFailure("migration_receipt_missing", "explicit rollback requires an existing migration receipt");
431
- }
432
- if (receipt.environment !== operation.environment || receipt.workspaceId !== operation.workspaceId
433
- || receipt.profile !== operation.profile) {
434
- throw new StorageMigrationFailure("worker_receipt_mismatch", "worker receipt does not match operation identity");
435
- }
436
- if (receipt.status === "completed" || receipt.status === "rolled_back")
437
- return receipt;
438
- const recovered = await dependencies.recoverMigration({
439
- backupsRoot: operation.migration.backupsRoot,
440
- migrationId: operation.migrationId,
441
- }, { ...dependencies.createServiceLifecycle(operation), now: dependencies.now });
442
- assertHeartbeatHealthy();
443
- return recovered;
444
- }
445
- export function storageWorkerOperationDigest(operation) {
446
- return createHash("sha256").update(`${JSON.stringify(operation, null, 2)}\n`).digest("hex");
447
- }