@nowcrew/daemon 0.6.67 → 0.6.68

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 (61) hide show
  1. package/dist/agent-ability/materializer.js +1 -20
  2. package/dist/agent-ability/resolver.js +10 -7
  3. package/dist/agent-ability/runtime.js +0 -3
  4. package/dist/automatic-install-target.js +10 -15
  5. package/dist/computer-cli.js +1 -10
  6. package/dist/computer-service.js +1 -41
  7. package/dist/config.js +0 -44
  8. package/dist/daemon-migration-controller.js +18 -16
  9. package/dist/execution-protocol.js +1 -0
  10. package/dist/execution-runner.js +8 -5
  11. package/dist/local-execution-prompt.js +11 -15
  12. package/dist/local-executor.js +53 -93
  13. package/dist/machine-info.js +4 -3
  14. package/dist/main.js +0 -83
  15. package/dist/managed-service-lifecycle.js +2 -47
  16. package/dist/managed-service-startup.js +0 -2
  17. package/dist/profile-layout.js +4 -10
  18. package/dist/project-skills/controller.js +0 -2
  19. package/dist/prompt.js +1 -19
  20. package/dist/provider-env.js +21 -3
  21. package/dist/runner.js +2 -4
  22. package/dist/runtimes/codex-app-server-runner.js +23 -55
  23. package/dist/runtimes/codex-contract.js +5 -20
  24. package/dist/runtimes/codex-deepseek-config.js +17 -7
  25. package/dist/runtimes/codex.js +3 -4
  26. package/dist/self-training-workspace.js +15 -0
  27. package/dist/serve.js +3 -68
  28. package/dist/supervised-runtime.js +1 -3
  29. package/dist/workspace.js +14 -34
  30. package/package.json +1 -1
  31. package/dist/local-memory/index-budget.js +0 -74
  32. package/dist/local-memory/pending.js +0 -111
  33. package/dist/local-memory/runtime.js +0 -87
  34. package/dist/local-memory/types.js +0 -1
  35. package/dist/local-memory/write.js +0 -448
  36. package/dist/runtimes/codex-command-safety.js +0 -75
  37. package/dist/runtimes/codex-turn-retry.js +0 -69
  38. package/dist/storage-config.js +0 -175
  39. package/dist/storage-control.js +0 -193
  40. package/dist/storage-copy.js +0 -609
  41. package/dist/storage-credential-guard.js +0 -21
  42. package/dist/storage-doctor.js +0 -228
  43. package/dist/storage-identity.js +0 -29
  44. package/dist/storage-layout.js +0 -143
  45. package/dist/storage-manifest.js +0 -855
  46. package/dist/storage-migration-controller.js +0 -417
  47. package/dist/storage-migration-manifest.js +0 -105
  48. package/dist/storage-migration-policy.js +0 -100
  49. package/dist/storage-runtime-controller.js +0 -339
  50. package/dist/storage-runtime-readiness.js +0 -184
  51. package/dist/storage-runtime.js +0 -39
  52. package/dist/storage-safe-fs.js +0 -302
  53. package/dist/storage-serve-adapter.js +0 -65
  54. package/dist/storage-status-delivery.js +0 -209
  55. package/dist/storage-status-replay.js +0 -125
  56. package/dist/storage-target-confirmation.js +0 -129
  57. package/dist/storage-worker-command.js +0 -27
  58. package/dist/storage-worker-executor.js +0 -384
  59. package/dist/storage-worker-handoff.js +0 -151
  60. package/dist/storage-worker-operation.js +0 -284
  61. package/dist/storage-worker-status.js +0 -232
@@ -1,151 +0,0 @@
1
- import { execFile } from "node:child_process";
2
- import { readFile, rm } from "node:fs/promises";
3
- import { join, posix, resolve, win32 } from "node:path";
4
- import { promisify } from "node:util";
5
- import { z } from "zod";
6
- import { atomicPrivateWrite } from "./atomic-private-write.js";
7
- import { readStorageWorkerOperation } from "./storage-worker-operation.js";
8
- const AbsolutePathSchema = z.string().min(1).max(32768)
9
- .refine((value) => !/[\0\r\n]/u.test(value), "path must be a single line")
10
- .refine((value) => posix.isAbsolute(value) || win32.isAbsolute(value), "path must be absolute");
11
- const HandoffSchema = z.object({
12
- version: z.literal(1),
13
- migrationId: z.string().uuid(),
14
- profile: z.string().regex(/^[a-z0-9][a-z0-9_-]{0,47}$/u),
15
- operationPath: AbsolutePathSchema,
16
- operationSha256: z.string().regex(/^[0-9a-f]{64}$/u),
17
- }).strict();
18
- export async function writeStorageWorkerHandoff(raw, input) {
19
- let handoff;
20
- try {
21
- handoff = HandoffSchema.parse(raw);
22
- AbsolutePathSchema.parse(input.stateRoot);
23
- }
24
- catch (error) {
25
- throw new Error("Invalid storage worker handoff", { cause: error });
26
- }
27
- const path = join(input.stateRoot, "migrations", handoff.migrationId, "handoff.json");
28
- await atomicPrivateWrite(path, `${JSON.stringify(handoff, null, 2)}\n`);
29
- return path;
30
- }
31
- export async function readStorageWorkerHandoff(path) {
32
- try {
33
- AbsolutePathSchema.parse(path);
34
- return HandoffSchema.parse(JSON.parse(await readFile(path, "utf8")));
35
- }
36
- catch (error) {
37
- throw new Error("Invalid storage worker handoff", { cause: error });
38
- }
39
- }
40
- const execFileAsync = promisify(execFile);
41
- const systemSupervisorRun = async (command, args) => {
42
- try {
43
- const result = await execFileAsync(command, [...args], {
44
- encoding: "utf8",
45
- timeout: 15_000,
46
- maxBuffer: 64 * 1024,
47
- });
48
- return { exitCode: 0, stdout: result.stdout, stderr: result.stderr };
49
- }
50
- catch (error) {
51
- const failure = error;
52
- return {
53
- exitCode: typeof failure.code === "number" ? failure.code : 127,
54
- stdout: failure.stdout ?? "",
55
- stderr: failure.stderr ?? failure.message,
56
- };
57
- }
58
- };
59
- function workerUnitName(migrationId) {
60
- return `nowwork-storage-migration-${migrationId}.service`;
61
- }
62
- function workerUnitPath(userHome, migrationId) {
63
- return resolve(userHome, ".config", "systemd", "user", workerUnitName(migrationId));
64
- }
65
- function workerLaunchdLabel(migrationId) {
66
- return `com.nowcrew.storage-migration.${migrationId}`;
67
- }
68
- function workerLaunchdPath(userHome, migrationId) {
69
- return resolve(userHome, "Library", "LaunchAgents", `${workerLaunchdLabel(migrationId)}.plist`);
70
- }
71
- function xml(value) {
72
- return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;")
73
- .replaceAll('"', "&quot;").replaceAll("'", "&apos;");
74
- }
75
- function systemdArg(value) {
76
- if (/[\0\r\n]/u.test(value))
77
- throw new Error("storage worker service argument must be a single line");
78
- return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll("%", "%%")}"`;
79
- }
80
- async function checkedSupervisor(run, command, args) {
81
- const result = await run(command, args);
82
- if (result.exitCode !== 0)
83
- throw new Error(result.stderr.trim() || result.stdout.trim() || `exit ${result.exitCode}`);
84
- }
85
- export async function launchStorageMigrationWorker(input, runSupervisor = systemSupervisorRun) {
86
- AbsolutePathSchema.parse(input.entryPath);
87
- AbsolutePathSchema.parse(input.handoffPath);
88
- AbsolutePathSchema.parse(input.userHome);
89
- const migrationId = z.string().uuid().parse(input.migrationId).toLowerCase();
90
- const worker = [process.execPath, input.entryPath, "storage-migration-worker", "--job", input.handoffPath];
91
- if (input.platform === "darwin") {
92
- if (input.uid === undefined || !Number.isSafeInteger(input.uid) || input.uid < 0) {
93
- throw new Error("Persistent storage migration launchd worker requires a numeric uid");
94
- }
95
- const label = workerLaunchdLabel(migrationId);
96
- const path = workerLaunchdPath(input.userHome, migrationId);
97
- const args = worker.map((arg) => ` <string>${xml(arg)}</string>`).join("\n");
98
- const descriptor = `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0">\n<dict>\n <key>Label</key><string>${xml(label)}</string>\n <key>ProgramArguments</key>\n <array>\n${args}\n </array>\n <key>RunAtLoad</key><true/>\n <key>KeepAlive</key>\n <dict>\n <key>SuccessfulExit</key><false/>\n </dict>\n <key>ProcessType</key><string>Background</string>\n</dict>\n</plist>\n`;
99
- await atomicPrivateWrite(path, descriptor);
100
- try {
101
- await checkedSupervisor(runSupervisor, "launchctl", ["bootstrap", `gui/${input.uid}`, path]);
102
- }
103
- catch (error) {
104
- await runSupervisor("launchctl", ["bootout", `gui/${input.uid}/${label}`]).catch(() => undefined);
105
- await rm(path, { force: true });
106
- throw new Error("Storage migration persistent service launch failed", { cause: error });
107
- }
108
- return;
109
- }
110
- const unit = workerUnitName(migrationId);
111
- const path = workerUnitPath(input.userHome, migrationId);
112
- const descriptor = `[Unit]\nDescription=NowWork storage migration ${migrationId}\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=exec\nExecStart=${worker.map(systemdArg).join(" ")}\nRestart=on-abnormal\nRestartSec=5\n\n[Install]\nWantedBy=default.target\n`;
113
- await atomicPrivateWrite(path, descriptor);
114
- try {
115
- await checkedSupervisor(runSupervisor, "systemctl", ["--user", "daemon-reload"]);
116
- await checkedSupervisor(runSupervisor, "systemctl", ["--user", "enable", "--now", unit]);
117
- }
118
- catch (error) {
119
- await runSupervisor("systemctl", ["--user", "disable", "--now", unit]).catch(() => undefined);
120
- await rm(path, { force: true });
121
- await runSupervisor("systemctl", ["--user", "daemon-reload"]).catch(() => undefined);
122
- throw new Error("Storage migration persistent service launch failed", { cause: error });
123
- }
124
- }
125
- export async function completeStorageMigrationWorkerService(input, runSupervisor = systemSupervisorRun) {
126
- const migrationId = z.string().uuid().parse(input.migrationId).toLowerCase();
127
- AbsolutePathSchema.parse(input.userHome);
128
- if (input.platform === "darwin") {
129
- if (input.uid === undefined || !Number.isSafeInteger(input.uid) || input.uid < 0) {
130
- throw new Error("Persistent storage migration launchd worker requires a numeric uid");
131
- }
132
- const label = workerLaunchdLabel(migrationId);
133
- await rm(workerLaunchdPath(input.userHome, migrationId), { force: true });
134
- await runSupervisor("launchctl", ["bootout", `gui/${input.uid}/${label}`]);
135
- return;
136
- }
137
- const unit = workerUnitName(migrationId);
138
- await checkedSupervisor(runSupervisor, "systemctl", ["--user", "disable", unit]);
139
- await rm(workerUnitPath(input.userHome, migrationId), { force: true });
140
- await checkedSupervisor(runSupervisor, "systemctl", ["--user", "daemon-reload"]);
141
- }
142
- export async function completeStorageMigrationWorkerServiceForHandoff(path) {
143
- const handoff = await readStorageWorkerHandoff(path);
144
- const operation = await readStorageWorkerOperation(handoff.operationPath, handoff.operationSha256);
145
- await completeStorageMigrationWorkerService({
146
- migrationId: operation.migrationId,
147
- platform: operation.service.platform,
148
- userHome: operation.service.userHome,
149
- ...(operation.service.uid === undefined ? {} : { uid: operation.service.uid }),
150
- });
151
- }
@@ -1,284 +0,0 @@
1
- import { createHash, timingSafeEqual } from "node:crypto";
2
- import { realpathSync } from "node:fs";
3
- import { readFile } from "node:fs/promises";
4
- import { basename, dirname, join, posix, win32 } from "node:path";
5
- import { z } from "zod";
6
- import { atomicPrivateWrite } from "./atomic-private-write.js";
7
- import { assertStorageControlMaterialTokenFree } from "./storage-credential-guard.js";
8
- import { storageLayout } from "./storage-layout.js";
9
- import { StorageMigrationWarningCodeSchema } from "./storage-migration-policy.js";
10
- const CONTROL = /[\0\r\n]/u;
11
- const PROFILE = /^[a-z0-9][a-z0-9_-]{0,47}$/u;
12
- const ENVIRONMENT = /^[a-z0-9][a-z0-9_-]{0,31}$/u;
13
- const HASH = /^[0-9a-f]{64}$/u;
14
- const AbsolutePathSchema = z.string().min(1).max(32768)
15
- .refine((value) => !CONTROL.test(value), "path must be a single line")
16
- .refine((value) => posix.isAbsolute(value) || win32.isAbsolute(value), "path must be absolute");
17
- const RelativePathSchema = z.string().min(1).max(32768)
18
- .refine((value) => !CONTROL.test(value), "path must be a single line")
19
- .refine((value) => !posix.isAbsolute(value) && !win32.isAbsolute(value), "path must be relative")
20
- .refine((value) => !value.split(/[\\/]/u).some((segment) => segment === "" || segment === "." || segment === ".."), "path must be normalized");
21
- const SourceKindSchema = z.enum(["daemon-home", "agents-root", "legacy-agents-root"]);
22
- const PolicySchema = z.object({
23
- exclusions: z.array(z.object({
24
- sourceKind: SourceKindSchema,
25
- sourcePath: RelativePathSchema,
26
- reason: z.string().min(1).max(256),
27
- }).strict()).max(100_000),
28
- transformations: z.array(z.object({
29
- sourceKind: SourceKindSchema,
30
- sourcePath: RelativePathSchema,
31
- logicalDestination: RelativePathSchema,
32
- sourceSha256: z.string().regex(HASH),
33
- targetSha256: z.string().regex(HASH),
34
- targetMode: z.string().regex(/^[0-7]{4}$/u),
35
- transform: z.literal("redact_live_like_tokens_v1"),
36
- }).strict()).max(100_000),
37
- }).strict();
38
- const DoctorInputSchema = z.object({
39
- migrationId: z.string().uuid(),
40
- generatedAt: z.string().datetime({ offset: true }),
41
- environment: z.string().regex(ENVIRONMENT),
42
- workspaceId: z.string().uuid(),
43
- profile: z.string().regex(PROFILE),
44
- targetWorkspaceRoot: AbsolutePathSchema,
45
- sources: z.array(z.object({
46
- kind: SourceKindSchema,
47
- root: AbsolutePathSchema,
48
- logicalPrefix: RelativePathSchema,
49
- }).strict()).min(1).max(3),
50
- policy: PolicySchema,
51
- manifestStoreRoot: AbsolutePathSchema.optional(),
52
- state: z.object({
53
- profileIdentityExact: z.boolean(),
54
- duplicateWorkspaceMappings: z.number().int().nonnegative(),
55
- activeAgentExecutions: z.number().int().nonnegative(),
56
- activeScheduledRuns: z.number().int().nonnegative(),
57
- }).strict(),
58
- freeSpaceMultiplier: z.number().finite().min(1).max(10).optional(),
59
- }).strict();
60
- const ManagedServiceRecordSchema = z.object({
61
- version: z.literal(1),
62
- generation: z.string().uuid(),
63
- platform: z.enum(["darwin", "linux"]),
64
- serviceId: z.string().min(1).max(256),
65
- profile: z.string().regex(PROFILE),
66
- daemonHome: AbsolutePathSchema,
67
- agentsRoot: AbsolutePathSchema,
68
- nodePath: AbsolutePathSchema,
69
- entryPath: AbsolutePathSchema,
70
- packageRoot: AbsolutePathSchema,
71
- npmPrefix: AbsolutePathSchema,
72
- descriptorPath: AbsolutePathSchema,
73
- descriptorSha256: z.string().regex(HASH),
74
- createdAt: z.string().datetime({ offset: true }),
75
- }).strict();
76
- const InstallationRecordSchema = z.object({
77
- version: z.literal(1),
78
- installationId: z.string().uuid(),
79
- profile: z.string().regex(PROFILE),
80
- serverUrl: z.string().url().max(2048).refine((value) => {
81
- const url = new URL(value);
82
- return (url.protocol === "http:" || url.protocol === "https:") && !url.username && !url.password;
83
- }, "serverUrl must be HTTP(S) without credentials"),
84
- environment: z.string().regex(ENVIRONMENT),
85
- workspaceId: z.string().uuid(),
86
- storageId: z.string().min(1).max(128).regex(/^[A-Za-z0-9_-]+$/u),
87
- nowworkHome: AbsolutePathSchema,
88
- runtimeRoot: AbsolutePathSchema,
89
- credentialRef: z.string().regex(/^profile:[a-z0-9][a-z0-9_-]{0,47}$/u),
90
- storageMigrationId: z.string().uuid(),
91
- createdAt: z.string().datetime({ offset: true }),
92
- updatedAt: z.string().datetime({ offset: true }),
93
- }).strict();
94
- const StorageServiceEnvironmentSchema = z.object({
95
- NOWWORK_HOME: AbsolutePathSchema.optional(),
96
- NOWWORK_ENV_TAG: z.string().regex(ENVIRONMENT).optional(),
97
- NOWWORK_STORAGE_POLICY_PATH: AbsolutePathSchema.optional(),
98
- }).strict();
99
- const OperationSchema = z.object({
100
- version: z.literal(1),
101
- migrationId: z.string().uuid(),
102
- createdAt: z.string().datetime({ offset: true }),
103
- environment: z.string().regex(ENVIRONMENT),
104
- workspaceId: z.string().uuid(),
105
- profile: z.string().regex(PROFILE),
106
- expectedManifestDigest: z.string().regex(HASH),
107
- doctorInput: DoctorInputSchema,
108
- migration: z.object({
109
- sourceRoots: z.object({
110
- "daemon-home": AbsolutePathSchema.optional(),
111
- "agents-root": AbsolutePathSchema.optional(),
112
- "legacy-agents-root": AbsolutePathSchema.optional(),
113
- }).strict(),
114
- stagingWorkspaceRoot: AbsolutePathSchema,
115
- targetWorkspaceRoot: AbsolutePathSchema,
116
- backupsRoot: AbsolutePathSchema,
117
- backupArtifacts: z.array(z.object({
118
- name: z.string().regex(/^[a-z0-9][a-z0-9_.-]{0,127}$/u),
119
- path: AbsolutePathSchema,
120
- }).strict()).max(32),
121
- acceptedWarnings: z.array(StorageMigrationWarningCodeSchema)
122
- .max(StorageMigrationWarningCodeSchema.options.length),
123
- }).strict(),
124
- service: z.object({
125
- platform: z.enum(["darwin", "linux"]),
126
- userHome: AbsolutePathSchema,
127
- uid: z.number().int().nonnegative().optional(),
128
- serviceEnvironment: StorageServiceEnvironmentSchema.optional(),
129
- sourceWasRunning: z.boolean(),
130
- sourceRecord: ManagedServiceRecordSchema,
131
- targetRecord: ManagedServiceRecordSchema,
132
- }).strict(),
133
- installation: z.object({
134
- configRoot: AbsolutePathSchema,
135
- target: InstallationRecordSchema,
136
- }).strict(),
137
- }).strict();
138
- const pathsFor = (_platform) => posix;
139
- function canonicalPath(value, platform) {
140
- const paths = pathsFor(platform);
141
- const absolute = paths.resolve(value);
142
- try {
143
- return realpathSync.native(absolute);
144
- }
145
- catch (error) {
146
- const code = error.code;
147
- if (code === "ENOTDIR")
148
- return absolute;
149
- if (code !== "ENOENT")
150
- throw error;
151
- }
152
- const missing = [];
153
- let ancestor = absolute;
154
- for (;;) {
155
- const parent = paths.dirname(ancestor);
156
- if (parent === ancestor)
157
- return absolute;
158
- missing.unshift(paths.basename(ancestor));
159
- ancestor = parent;
160
- try {
161
- return paths.resolve(realpathSync.native(ancestor), ...missing);
162
- }
163
- catch (error) {
164
- const code = error.code;
165
- if (code === "ENOTDIR")
166
- return absolute;
167
- if (code !== "ENOENT")
168
- throw error;
169
- }
170
- }
171
- }
172
- function samePath(left, right, platform) {
173
- return canonicalPath(left, platform) === canonicalPath(right, platform);
174
- }
175
- function isInside(root, candidate, platform) {
176
- const paths = pathsFor(platform);
177
- const child = paths.relative(canonicalPath(root, platform), canonicalPath(candidate, platform));
178
- return child === "" || (!child.startsWith("..") && !paths.isAbsolute(child));
179
- }
180
- function assertSemanticIdentity(operation) {
181
- const identity = [operation.doctorInput, operation.installation.target];
182
- if (identity.some((value) => value.workspaceId !== operation.workspaceId
183
- || value.environment !== operation.environment || value.profile !== operation.profile)
184
- || operation.doctorInput.migrationId !== operation.migrationId
185
- || operation.installation.target.storageMigrationId !== operation.migrationId
186
- || operation.installation.target.credentialRef !== `profile:${operation.profile}`) {
187
- throw new Error("storage worker operation identity does not match");
188
- }
189
- if (operation.doctorInput.state.profileIdentityExact !== true
190
- || operation.doctorInput.state.duplicateWorkspaceMappings !== 0
191
- || operation.doctorInput.state.activeAgentExecutions !== 0
192
- || operation.doctorInput.state.activeScheduledRuns !== 0) {
193
- throw new Error("storage worker operation doctor state is not safe");
194
- }
195
- const { platform, sourceRecord, targetRecord } = operation.service;
196
- if (sourceRecord.platform !== platform || targetRecord.platform !== platform
197
- || sourceRecord.profile !== operation.profile || targetRecord.profile !== operation.profile
198
- || sourceRecord.serviceId === targetRecord.serviceId
199
- || !samePath(sourceRecord.nodePath, targetRecord.nodePath, platform)
200
- || !samePath(sourceRecord.entryPath, targetRecord.entryPath, platform)
201
- || !samePath(sourceRecord.packageRoot, targetRecord.packageRoot, platform)
202
- || !samePath(sourceRecord.npmPrefix, targetRecord.npmPrefix, platform)
203
- || !samePath(targetRecord.daemonHome, operation.installation.configRoot, platform)) {
204
- throw new Error("storage worker service identity does not match");
205
- }
206
- const sourceKinds = new Set();
207
- for (const source of operation.doctorInput.sources) {
208
- if (sourceKinds.has(source.kind) || !samePath(operation.migration.sourceRoots[source.kind] ?? "", source.root, platform)) {
209
- throw new Error("storage worker source roots do not match doctor input");
210
- }
211
- sourceKinds.add(source.kind);
212
- }
213
- const configuredSourceKinds = Object.entries(operation.migration.sourceRoots)
214
- .filter(([, value]) => value !== undefined)
215
- .map(([kind]) => kind);
216
- if (configuredSourceKinds.length !== sourceKinds.size || configuredSourceKinds.some((kind) => !sourceKinds.has(kind))
217
- || !samePath(operation.migration.sourceRoots["daemon-home"] ?? "", sourceRecord.daemonHome, platform)
218
- || !samePath(operation.migration.sourceRoots["agents-root"] ?? "", sourceRecord.agentsRoot, platform)) {
219
- throw new Error("storage worker source service roots do not match");
220
- }
221
- const projected = storageLayout({
222
- nowworkHome: operation.installation.target.nowworkHome,
223
- environment: operation.environment,
224
- workspaceId: operation.workspaceId,
225
- runtimeBase: join(operation.service.userHome, ".nowwork-worker-runtime-validation"),
226
- platform,
227
- });
228
- if (!samePath(projected.workspaceRoot, operation.migration.targetWorkspaceRoot, platform)
229
- || !samePath(projected.workspaceRoot, operation.doctorInput.targetWorkspaceRoot, platform)
230
- || !samePath(projected.agentItemsRoot, targetRecord.agentsRoot, platform)) {
231
- throw new Error("storage worker target layout does not match installation identity");
232
- }
233
- if (dirname(operation.migration.stagingWorkspaceRoot) !== dirname(operation.migration.targetWorkspaceRoot)
234
- || !basename(operation.migration.stagingWorkspaceRoot).includes(operation.migrationId)) {
235
- throw new Error("storage worker staging identity does not match target");
236
- }
237
- if (isInside(operation.installation.target.nowworkHome, operation.migration.backupsRoot, platform)
238
- || isInside(operation.installation.target.nowworkHome, operation.installation.configRoot, platform)) {
239
- throw new Error("storage worker local state must remain outside NOWWORK_HOME");
240
- }
241
- }
242
- function parseOperation(raw) {
243
- const operation = OperationSchema.parse(raw);
244
- assertSemanticIdentity(operation);
245
- return operation;
246
- }
247
- function sha256(value) {
248
- return createHash("sha256").update(value).digest("hex");
249
- }
250
- export async function writeStorageWorkerOperation(raw, input) {
251
- try {
252
- const operation = parseOperation(raw);
253
- const stateRoot = AbsolutePathSchema.parse(input.stateRoot);
254
- const path = join(stateRoot, "migrations", operation.migrationId, "operation.json");
255
- const serialized = `${JSON.stringify(operation, null, 2)}\n`;
256
- assertStorageControlMaterialTokenFree(serialized, "storage worker operation");
257
- await atomicPrivateWrite(path, serialized);
258
- return Object.freeze({ path, sha256: sha256(serialized) });
259
- }
260
- catch (error) {
261
- throw new Error("Invalid storage worker operation", { cause: error });
262
- }
263
- }
264
- export async function readStorageWorkerOperation(path, expectedSha256) {
265
- try {
266
- AbsolutePathSchema.parse(path);
267
- z.string().regex(HASH).parse(expectedSha256);
268
- const raw = await readFile(path);
269
- assertStorageControlMaterialTokenFree(raw.toString("utf8"), "storage worker operation");
270
- const actual = Buffer.from(sha256(raw), "hex");
271
- const expected = Buffer.from(expectedSha256, "hex");
272
- if (!timingSafeEqual(actual, expected))
273
- throw new Error("storage worker operation digest mismatch");
274
- const operation = parseOperation(JSON.parse(raw.toString("utf8")));
275
- if (basename(path) !== "operation.json" || basename(dirname(path)) !== operation.migrationId
276
- || basename(dirname(dirname(path))) !== "migrations") {
277
- throw new Error("storage worker operation path does not match migration identity");
278
- }
279
- return operation;
280
- }
281
- catch (error) {
282
- throw new Error("Invalid storage worker operation", { cause: error });
283
- }
284
- }
@@ -1,232 +0,0 @@
1
- import { readFile, unlink } from "node:fs/promises";
2
- import { basename, dirname, isAbsolute, join } from "node:path";
3
- import { Worker } from "node:worker_threads";
4
- import { z } from "zod";
5
- import { atomicPrivateWrite } from "./atomic-private-write.js";
6
- const WorkerFailureSchema = z.object({
7
- version: z.literal(1),
8
- migrationId: z.string().uuid(),
9
- status: z.literal("failed"),
10
- errorCode: z.string().regex(/^[a-z0-9._-]{1,128}$/u),
11
- errorMessage: z.string().min(1).max(512),
12
- at: z.string().datetime({ offset: true }),
13
- }).strict();
14
- const WorkerHeartbeatSchema = z.object({
15
- version: z.literal(1),
16
- migrationId: z.string().uuid(),
17
- pid: z.number().int().positive(),
18
- at: z.string().datetime({ offset: true }),
19
- }).strict();
20
- const TOKEN = /(?:\bsk_(?:machine|user|agent)_[A-Za-z0-9_-]{4,}\b|\bbearer\s+[^\s]+|(?:password|secret|token)\s*[:=]\s*[^\s]+)/giu;
21
- const HEARTBEAT_WORKER_KIND = "nowwork-storage-worker-heartbeat-v1";
22
- async function unlinkHeartbeat(path) {
23
- await unlink(path).catch((error) => {
24
- if (error.code !== "ENOENT")
25
- throw error;
26
- });
27
- }
28
- /**
29
- * Keep the liveness writer in a worker with its own event loop. The source is
30
- * intentionally plain JavaScript because `new Worker(new URL(import.meta.url))`
31
- * cannot resolve the source tree's `.js` specifiers under tsx/Vitest; production
32
- * builds and tests therefore exercise the same isolated execution boundary.
33
- */
34
- const HEARTBEAT_WORKER_SOURCE = String.raw `
35
- const { parentPort, workerData } = require("node:worker_threads");
36
- const { mkdir, chmod, rename, rm, unlink, writeFile } = require("node:fs/promises");
37
- const { dirname } = require("node:path");
38
- const { randomUUID } = require("node:crypto");
39
-
40
- const atomicPrivateWrite = async (path, content) => {
41
- const directory = dirname(path);
42
- await mkdir(directory, { recursive: true, mode: 0o700 });
43
- await chmod(directory, 0o700);
44
- const temporary = path + "." + randomUUID() + ".tmp";
45
- try {
46
- await writeFile(temporary, content, { encoding: "utf8", mode: 0o600, flag: "wx" });
47
- await rename(temporary, path);
48
- await chmod(path, 0o600);
49
- } finally {
50
- await rm(temporary, { force: true });
51
- }
52
- };
53
- const removeHeartbeat = async () => {
54
- try { await unlink(workerData.path); } catch (error) {
55
- if (error && error.code !== "ENOENT") throw error;
56
- }
57
- };
58
- let pending = Promise.resolve();
59
- let stopping = false;
60
- let firstWrite = true;
61
- const write = () => {
62
- const initial = firstWrite;
63
- firstWrite = false;
64
- pending = pending.catch(() => undefined).then(() => atomicPrivateWrite(workerData.path, JSON.stringify({
65
- version: 1,
66
- migrationId: workerData.migrationId,
67
- pid: process.pid,
68
- at: initial && workerData.initialAt ? workerData.initialAt : new Date().toISOString(),
69
- }, null, 2) + "\n"));
70
- return pending;
71
- };
72
- const report = (message) => { try { parentPort.postMessage(message); } catch {} };
73
- (async () => {
74
- try { await write(); report({ type: "ready" }); }
75
- catch (error) { report({ type: "error", message: error instanceof Error ? error.message : String(error) }); return; }
76
- const timer = setInterval(() => {
77
- if (!stopping) void write().catch((error) => report({ type: "error", message: error instanceof Error ? error.message : String(error) }));
78
- }, workerData.intervalMs);
79
- timer.unref?.();
80
- parentPort.on("message", (message) => {
81
- if (stopping || !message || message.type !== "stop") return;
82
- stopping = true;
83
- clearInterval(timer);
84
- void pending.then(async () => { await removeHeartbeat(); report({ type: "stopped" }); }, async () => {
85
- await removeHeartbeat().catch(() => undefined); report({ type: "stopped" });
86
- });
87
- });
88
- })();
89
- `;
90
- function identityFromHandoffPath(handoffPath) {
91
- if (!isAbsolute(handoffPath) || basename(handoffPath) !== "handoff.json") {
92
- throw new Error("storage worker handoff path is invalid");
93
- }
94
- const migrationDirectory = dirname(handoffPath);
95
- const migrationsDirectory = dirname(migrationDirectory);
96
- if (basename(migrationsDirectory) !== "migrations")
97
- throw new Error("storage worker handoff path is invalid");
98
- return {
99
- stateRoot: dirname(migrationsDirectory),
100
- migrationId: z.string().uuid().parse(basename(migrationDirectory)).toLowerCase(),
101
- };
102
- }
103
- export function storageWorkerFailurePath(stateRoot, migrationId) {
104
- if (!isAbsolute(stateRoot))
105
- throw new Error("storage worker state root must be absolute");
106
- return join(stateRoot, "migrations", z.string().uuid().parse(migrationId), "worker-status.json");
107
- }
108
- export function storageWorkerHeartbeatPath(stateRoot, migrationId) {
109
- if (!isAbsolute(stateRoot))
110
- throw new Error("storage worker state root must be absolute");
111
- return join(stateRoot, "migrations", z.string().uuid().parse(migrationId), "worker-heartbeat.json");
112
- }
113
- export async function startStorageWorkerHeartbeat(handoffPath, input = {}) {
114
- const identity = identityFromHandoffPath(handoffPath);
115
- const intervalMs = z.number().int().min(100).max(60_000).parse(input.intervalMs ?? 5_000);
116
- const path = storageWorkerHeartbeatPath(identity.stateRoot, identity.migrationId);
117
- // The worker owns heartbeat I/O so a synchronous CPU-heavy scan in the
118
- // migration process cannot delay the monitor's 30-second freshness window.
119
- const worker = new Worker(HEARTBEAT_WORKER_SOURCE, {
120
- eval: true,
121
- // `--input-type=module` is inherited by workers launched from an eval
122
- // smoke test; strip it so the plain CommonJS worker source can use require.
123
- execArgv: process.execArgv.filter((arg) => !arg.startsWith("--input-type=")),
124
- workerData: {
125
- kind: HEARTBEAT_WORKER_KIND,
126
- path,
127
- migrationId: identity.migrationId,
128
- intervalMs,
129
- ...(input.now === undefined ? {} : { initialAt: input.now().toISOString() }),
130
- },
131
- });
132
- worker.unref();
133
- let startupSettled = false;
134
- let heartbeatError = null;
135
- let stopResolve = null;
136
- const onWorkerMessage = (message) => {
137
- if (message.type === "error") {
138
- heartbeatError = new Error(`storage worker heartbeat failed: ${message.message}`);
139
- if (!startupSettled)
140
- startupReject?.(heartbeatError);
141
- }
142
- else if (message.type === "stopped") {
143
- stopResolve?.();
144
- stopResolve = null;
145
- }
146
- };
147
- let startupReject = null;
148
- worker.on("message", onWorkerMessage);
149
- // Keep this listener for the entire worker lifetime. A heartbeat write may
150
- // fail after startup; dropping the listener would turn that operational
151
- // error into an unhandled Worker 'error' event and crash the parent.
152
- const onWorkerError = (error) => {
153
- heartbeatError = error;
154
- if (!startupSettled)
155
- startupReject?.(error);
156
- };
157
- worker.on("error", onWorkerError);
158
- await new Promise((resolvePromise, reject) => {
159
- startupReject = reject;
160
- const onReady = (message) => {
161
- if (message.type !== "ready")
162
- return;
163
- startupSettled = true;
164
- worker.off("message", onReady);
165
- resolvePromise();
166
- };
167
- worker.on("message", onReady);
168
- }).catch(async (error) => {
169
- startupSettled = true;
170
- await worker.terminate().catch(() => undefined);
171
- worker.off("message", onWorkerMessage);
172
- worker.off("error", onWorkerError);
173
- await unlinkHeartbeat(path).catch(() => undefined);
174
- throw error;
175
- });
176
- let stopped = false;
177
- return async () => {
178
- if (stopped)
179
- return;
180
- stopped = true;
181
- await new Promise((resolvePromise) => {
182
- const onExit = () => {
183
- worker.off("exit", onExit);
184
- stopResolve = null;
185
- resolvePromise();
186
- };
187
- stopResolve = resolvePromise;
188
- worker.once("exit", onExit);
189
- worker.postMessage({ type: "stop" });
190
- });
191
- await worker.terminate().catch(() => undefined);
192
- worker.off("message", onWorkerMessage);
193
- worker.off("error", onWorkerError);
194
- await unlinkHeartbeat(path);
195
- };
196
- }
197
- export async function writeStorageWorkerFailureStatus(handoffPath, error) {
198
- const identity = identityFromHandoffPath(handoffPath);
199
- const rawCode = error instanceof Error && "code" in error ? String(error.code) : "worker_failed";
200
- const errorCode = /^[a-z0-9._-]{1,128}$/u.test(rawCode) ? rawCode : "worker_failed";
201
- const rawMessage = error instanceof Error ? error.message : String(error);
202
- const errorMessage = rawMessage.replaceAll(TOKEN, "<redacted>").slice(0, 512) || "storage worker failed";
203
- const status = WorkerFailureSchema.parse({
204
- version: 1,
205
- migrationId: identity.migrationId,
206
- status: "failed",
207
- errorCode,
208
- errorMessage,
209
- at: new Date().toISOString(),
210
- });
211
- await atomicPrivateWrite(storageWorkerFailurePath(identity.stateRoot, identity.migrationId), `${JSON.stringify(status, null, 2)}\n`);
212
- }
213
- export async function readStorageWorkerFailureStatus(stateRoot, migrationId) {
214
- try {
215
- return WorkerFailureSchema.parse(JSON.parse(await readFile(storageWorkerFailurePath(stateRoot, migrationId), "utf8")));
216
- }
217
- catch (error) {
218
- if (error.code === "ENOENT")
219
- return null;
220
- throw new Error("Invalid storage worker failure status", { cause: error });
221
- }
222
- }
223
- export async function readStorageWorkerHeartbeat(stateRoot, migrationId) {
224
- try {
225
- return WorkerHeartbeatSchema.parse(JSON.parse(await readFile(storageWorkerHeartbeatPath(stateRoot, migrationId), "utf8")));
226
- }
227
- catch (error) {
228
- if (error.code === "ENOENT")
229
- return null;
230
- throw new Error("Invalid storage worker heartbeat", { cause: error });
231
- }
232
- }