@nowcrew/daemon 0.6.77-beta.6 → 0.6.77-beta.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -0
- package/dist/automatic-daemon-installer.js +7 -0
- package/dist/automatic-install-target.js +34 -9
- package/dist/computer-cli.js +25 -3
- package/dist/computer-profile.js +15 -2
- package/dist/config.js +24 -4
- package/dist/daemon-local-home.js +73 -0
- package/dist/daemon-migration-controller.js +9 -1
- package/dist/execution-runner.js +3 -2
- package/dist/local-executor.js +5 -0
- package/dist/main.js +1 -1
- package/dist/project-skills/runtime-root-bootstrap.js +6 -0
- package/dist/project-skills/runtime-root-startup.js +2 -1
- package/dist/project-skills/serve-startup.js +23 -0
- package/dist/runner.js +2 -1
- package/dist/serve.js +4 -6
- package/dist/storage-control.js +12 -1
- package/dist/storage-identity.js +15 -1
- package/dist/storage-migration-controller.js +25 -4
- package/dist/storage-ownership.js +55 -0
- package/dist/storage-runtime-controller.js +29 -7
- package/dist/storage-runtime-readiness.js +16 -1
- package/dist/storage-runtime.js +18 -2
- package/dist/storage-status-delivery.js +4 -0
- package/dist/storage-status-replay.js +16 -1
- package/dist/storage-worker-executor.js +1 -0
- package/dist/storage-worker-operation.js +4 -0
- package/package.json +10 -9
package/README.md
CHANGED
|
@@ -479,6 +479,26 @@ failure matrix, rollout limits, and the server-native follow-up design.
|
|
|
479
479
|
|
|
480
480
|
## Code Map
|
|
481
481
|
|
|
482
|
+
## Local daemon installation root (experimental)
|
|
483
|
+
|
|
484
|
+
New installations use `~/.nowwork` for machine-local daemon configuration and npm runtimes. The
|
|
485
|
+
Workspace persistence root is still selected independently by `NOWWORK_HOME`; it may be a local
|
|
486
|
+
directory or an ECS-specific NAS directory. Runtime packages, locks, sockets, PIDs and temporary
|
|
487
|
+
files must remain on the machine running the daemon.
|
|
488
|
+
|
|
489
|
+
The selector is controlled by `NOWWORK_LOCAL_HOME_MODE`:
|
|
490
|
+
|
|
491
|
+
- `auto` (default): use an existing `.nowwork` installation, otherwise preserve an existing `.crew`
|
|
492
|
+
installation; a brand-new profile is created under `.nowwork`.
|
|
493
|
+
- `nowwork`: require the `.nowwork` installation root.
|
|
494
|
+
- `legacy`: force the old `.crew` root for rollback.
|
|
495
|
+
|
|
496
|
+
`NOWWORK_STORAGE_MODE` is accepted as a compatibility alias. `NOWWORK_LOCAL_HOME` can override the
|
|
497
|
+
local root and should point to a local filesystem, never to the NAS Workspace root; the mount/device
|
|
498
|
+
check remains a doctor/migration gate. If the same profile
|
|
499
|
+
is found in both roots, startup/install is refused until the duplicate is resolved explicitly. The
|
|
500
|
+
old `.crew` tree is not deleted by this compatibility layer.
|
|
501
|
+
|
|
482
502
|
- `src/serve.ts`: connection, negotiation, routing, sync, and legacy boundary.
|
|
483
503
|
- `src/daemon-update-eligibility.ts`, `src/daemon-updater.ts`, and `src/daemon-update-controller.ts`:
|
|
484
504
|
managed-update eligibility, exact npm replacement, host-wide admission barrier, and restart handoff.
|
|
@@ -39,6 +39,7 @@ function defaultDependencies() {
|
|
|
39
39
|
platform,
|
|
40
40
|
userHome,
|
|
41
41
|
uid: process.getuid?.(),
|
|
42
|
+
environment: process.env,
|
|
42
43
|
nodePath: process.execPath,
|
|
43
44
|
version: daemonVersion(),
|
|
44
45
|
...(process.env.PATH ? { runtimePath: process.env.PATH } : {}),
|
|
@@ -231,6 +232,10 @@ function recoveryError(primary, failures) {
|
|
|
231
232
|
}
|
|
232
233
|
export async function installDaemonAutomatically(input, dependencies = defaultDependencies()) {
|
|
233
234
|
const machineToken = MachineTokenSchema.parse(input.machineToken);
|
|
235
|
+
const requestedLocalHome = input.requestedLocalHome ?? dependencies.environment.NOWWORK_LOCAL_HOME;
|
|
236
|
+
const requestedLocalHomeMode = input.requestedLocalHomeMode
|
|
237
|
+
?? dependencies.environment.NOWWORK_LOCAL_HOME_MODE
|
|
238
|
+
?? dependencies.environment.NOWWORK_STORAGE_MODE;
|
|
234
239
|
const target = resolveAutomaticInstallTarget({
|
|
235
240
|
serverUrl: input.serverUrl,
|
|
236
241
|
...(input.requestedProfile === undefined ? {} : { requestedProfile: input.requestedProfile }),
|
|
@@ -238,6 +243,8 @@ export async function installDaemonAutomatically(input, dependencies = defaultDe
|
|
|
238
243
|
...(input.requestedRegistryUrl === undefined ? {} : { requestedRegistryUrl: input.requestedRegistryUrl }),
|
|
239
244
|
userHome: dependencies.userHome,
|
|
240
245
|
platform: dependencies.platform,
|
|
246
|
+
...(requestedLocalHome === undefined ? {} : { requestedLocalHome }),
|
|
247
|
+
...(requestedLocalHomeMode === undefined ? {} : { requestedLocalHomeMode }),
|
|
241
248
|
});
|
|
242
249
|
if (!isDaemonReleaseVersion(dependencies.version)) {
|
|
243
250
|
throw new Error(`Invoking daemon package has an invalid release version '${dependencies.version}'`);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { posix, win32 } from "node:path";
|
|
2
2
|
import { profileLayout, validateLocalInstanceName } from "./profile-layout.js";
|
|
3
3
|
import { normalizeDaemonNpmRegistry } from "./npm-registry.js";
|
|
4
|
+
import { resolveDaemonLocalHome } from "./daemon-local-home.js";
|
|
4
5
|
const PROFILE_NAME = /^[a-z0-9][a-z0-9_-]{0,47}$/;
|
|
5
6
|
const KNOWN_TARGETS = {
|
|
6
7
|
"nowwork-in.nowcoder.com": {
|
|
@@ -72,13 +73,21 @@ function knownTargetForProfile(environment, requestedProfile, input, parsed) {
|
|
|
72
73
|
const legacyProfile = environment === "production" ? "in" : environment;
|
|
73
74
|
assertReleaseTagForServerUrl(input.serverUrl, input.requestedReleaseTag);
|
|
74
75
|
if (requestedProfile === undefined || requestedProfile === legacyProfile) {
|
|
76
|
+
const local = resolveDaemonLocalHome({
|
|
77
|
+
userHome: input.userHome,
|
|
78
|
+
platform: input.platform,
|
|
79
|
+
profile: legacyProfile,
|
|
80
|
+
...(input.requestedLocalHome === undefined ? {} : { explicit: input.requestedLocalHome }),
|
|
81
|
+
...(input.requestedLocalHomeMode === undefined ? {} : { mode: input.requestedLocalHomeMode }),
|
|
82
|
+
...(input.pathExists === undefined ? {} : { exists: input.pathExists }),
|
|
83
|
+
});
|
|
75
84
|
return Object.freeze({
|
|
76
85
|
kind: "known",
|
|
77
86
|
profile: legacyProfile,
|
|
78
87
|
serverUrl: normalizedServerUrl(parsed),
|
|
79
|
-
daemonHome:
|
|
80
|
-
agentsRoot: paths.join(
|
|
81
|
-
npmPrefix:
|
|
88
|
+
daemonHome: local.daemonHome,
|
|
89
|
+
agentsRoot: paths.join(local.root, environment === "production" ? "agents" : "agents-dev"),
|
|
90
|
+
npmPrefix: local.runtimePrefix,
|
|
82
91
|
releaseTag: environment === "dev" ? "beta" : "latest",
|
|
83
92
|
});
|
|
84
93
|
}
|
|
@@ -99,15 +108,23 @@ function knownTargetForProfile(environment, requestedProfile, input, parsed) {
|
|
|
99
108
|
userHome: input.userHome,
|
|
100
109
|
platform: input.platform,
|
|
101
110
|
});
|
|
111
|
+
const local = resolveDaemonLocalHome({
|
|
112
|
+
userHome: input.userHome,
|
|
113
|
+
platform: input.platform,
|
|
114
|
+
profile: layout.profile,
|
|
115
|
+
...(input.requestedLocalHome === undefined ? {} : { explicit: input.requestedLocalHome }),
|
|
116
|
+
...(input.requestedLocalHomeMode === undefined ? {} : { mode: input.requestedLocalHomeMode }),
|
|
117
|
+
...(input.pathExists === undefined ? {} : { exists: input.pathExists }),
|
|
118
|
+
});
|
|
102
119
|
return Object.freeze({
|
|
103
120
|
kind: "known",
|
|
104
121
|
environment,
|
|
105
122
|
localInstanceName,
|
|
106
123
|
profile: layout.profile,
|
|
107
124
|
serverUrl: normalizedServerUrl(parsed),
|
|
108
|
-
daemonHome:
|
|
109
|
-
agentsRoot: layout.
|
|
110
|
-
npmPrefix:
|
|
125
|
+
daemonHome: local.daemonHome,
|
|
126
|
+
agentsRoot: paths.join(local.root, "agents-" + layout.environment + "-" + layout.localInstanceName),
|
|
127
|
+
npmPrefix: local.runtimePrefix,
|
|
111
128
|
releaseTag: environment === "dev" ? "beta" : "latest",
|
|
112
129
|
});
|
|
113
130
|
}
|
|
@@ -127,13 +144,21 @@ export function resolveAutomaticInstallTarget(input) {
|
|
|
127
144
|
return registryUrl === undefined ? target : Object.freeze({ ...target, registryUrl });
|
|
128
145
|
}
|
|
129
146
|
const profile = requireCustomProfile(hostname, input.requestedProfile);
|
|
147
|
+
const local = resolveDaemonLocalHome({
|
|
148
|
+
userHome: input.userHome,
|
|
149
|
+
platform: input.platform,
|
|
150
|
+
profile,
|
|
151
|
+
...(input.requestedLocalHome === undefined ? {} : { explicit: input.requestedLocalHome }),
|
|
152
|
+
...(input.requestedLocalHomeMode === undefined ? {} : { mode: input.requestedLocalHomeMode }),
|
|
153
|
+
...(input.pathExists === undefined ? {} : { exists: input.pathExists }),
|
|
154
|
+
});
|
|
130
155
|
return Object.freeze({
|
|
131
156
|
kind: "custom",
|
|
132
157
|
profile,
|
|
133
158
|
serverUrl: normalizedServerUrl(parsed),
|
|
134
|
-
daemonHome:
|
|
135
|
-
agentsRoot: paths.join(
|
|
136
|
-
npmPrefix:
|
|
159
|
+
daemonHome: local.daemonHome,
|
|
160
|
+
agentsRoot: paths.join(local.root, `agents-${profile}`),
|
|
161
|
+
npmPrefix: local.runtimePrefix,
|
|
137
162
|
releaseTag: input.requestedReleaseTag ?? "latest",
|
|
138
163
|
...(registryUrl === undefined ? {} : { registryUrl }),
|
|
139
164
|
});
|
package/dist/computer-cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import { resolve } from "node:path";
|
|
3
|
+
import { posix, resolve, win32 } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { assertProfileAgentsRootUnique, daemonHome, inspectProfileAgentsRoot, listProfiles, loadProfile, PROFILE_AGENTS_ROOT_CONFLICT_MESSAGE, ProfileAgentsRootConflictError, profileIsPrivate, profilePath, publicProfile, removeProfile, resolveAgentsRoot, saveProfile, windowsDpapiProtector, } from "./computer-profile.js";
|
|
6
6
|
import { buildServiceSpec, doctorService, hardenWindowsProfile, installService, readServiceDescriptor, serviceAction, serviceStatus, systemCommandRunner, uninstallService, upgradeDaemon, windowsProfileIsPrivate, } from "./computer-service.js";
|
|
@@ -18,6 +18,7 @@ import { assertManagedServiceIdentity, buildManagedServiceRecord, installManaged
|
|
|
18
18
|
import { inspectManagedServiceDiagnostics } from "./managed-service-diagnostics.js";
|
|
19
19
|
import { readManagedServiceRegistry } from "./managed-service-registry.js";
|
|
20
20
|
import { installDaemonAutomatically, } from "./automatic-daemon-installer.js";
|
|
21
|
+
import { resolveDaemonLocalHome } from "./daemon-local-home.js";
|
|
21
22
|
const COMPUTER_COMMANDS = new Set(["profile", "doctor", "install", "uninstall", "start", "stop", "restart", "status", "upgrade"]);
|
|
22
23
|
export function builtDaemonEntry(moduleUrl = import.meta.url) {
|
|
23
24
|
return moduleUrl.endsWith(".js")
|
|
@@ -67,6 +68,7 @@ function usage(td) {
|
|
|
67
68
|
" crew-daemon install --server-url <url> --api-key <sk_machine_*> [--profile <custom-name>] [--release-tag <latest|beta>] [--registry <https-url>]",
|
|
68
69
|
" crew-daemon doctor|install|uninstall|start|stop|restart|status --profile <name>",
|
|
69
70
|
" crew-daemon upgrade [--profile <name>] [--release-tag <latest|beta>] [--registry <https-url>]",
|
|
71
|
+
" NOWWORK_LOCAL_HOME_MODE=auto|nowwork|legacy NOWWORK_LOCAL_HOME=~/.nowwork",
|
|
70
72
|
"",
|
|
71
73
|
td("profile save reads the machine token from CREW_MACHINE_TOKEN, or stdin with --token-stdin."),
|
|
72
74
|
].join("\n") + "\n";
|
|
@@ -154,6 +156,7 @@ export async function runComputerCommand(argv, overrides = {}) {
|
|
|
154
156
|
?? deps.env.CREW_MACHINE_TOKEN)?.trim();
|
|
155
157
|
if (!machineToken)
|
|
156
158
|
throw new Error(td("Missing machine token for automatic installation"));
|
|
159
|
+
const requestedLocalHomeMode = deps.env.NOWWORK_LOCAL_HOME_MODE ?? deps.env.NOWWORK_STORAGE_MODE;
|
|
157
160
|
const result = await deps.installAutomatically({
|
|
158
161
|
serverUrl: parsed.values["server-url"],
|
|
159
162
|
machineToken,
|
|
@@ -162,11 +165,28 @@ export async function runComputerCommand(argv, overrides = {}) {
|
|
|
162
165
|
...(configuredRegistryUrl === undefined
|
|
163
166
|
? {}
|
|
164
167
|
: { requestedRegistryUrl: configuredRegistryUrl }),
|
|
168
|
+
...(deps.env.NOWWORK_LOCAL_HOME === undefined
|
|
169
|
+
? {}
|
|
170
|
+
: { requestedLocalHome: deps.env.NOWWORK_LOCAL_HOME }),
|
|
171
|
+
...(requestedLocalHomeMode === undefined ? {} : { requestedLocalHomeMode }),
|
|
165
172
|
});
|
|
166
173
|
deps.stdout(`${td("Installed daemon {{version}} for profile '{{profile}}'; service '{{service}}' is running.", { version: result.version, profile: result.profile, service: result.serviceId })}\n`);
|
|
167
174
|
return 0;
|
|
168
175
|
}
|
|
169
|
-
const
|
|
176
|
+
const resolvedProfile = profileName ?? positionalName;
|
|
177
|
+
const configuredLocalHomeMode = deps.env.NOWWORK_LOCAL_HOME_MODE ?? deps.env.NOWWORK_STORAGE_MODE;
|
|
178
|
+
const home = resolvedProfile !== undefined
|
|
179
|
+
&& (deps.env.NOWWORK_LOCAL_HOME !== undefined
|
|
180
|
+
|| deps.env.NOWWORK_LOCAL_HOME_MODE !== undefined
|
|
181
|
+
|| deps.env.NOWWORK_STORAGE_MODE !== undefined)
|
|
182
|
+
? resolveDaemonLocalHome({
|
|
183
|
+
userHome: deps.userHome,
|
|
184
|
+
platform: deps.platform,
|
|
185
|
+
profile: resolvedProfile,
|
|
186
|
+
...(deps.env.NOWWORK_LOCAL_HOME === undefined ? {} : { explicit: deps.env.NOWWORK_LOCAL_HOME }),
|
|
187
|
+
...(configuredLocalHomeMode === undefined ? {} : { mode: configuredLocalHomeMode }),
|
|
188
|
+
}).daemonHome
|
|
189
|
+
: daemonHome(deps.env);
|
|
170
190
|
const profileStorage = {
|
|
171
191
|
platform: deps.platform,
|
|
172
192
|
userHome: deps.userHome,
|
|
@@ -221,7 +241,9 @@ export async function runComputerCommand(argv, overrides = {}) {
|
|
|
221
241
|
}),
|
|
222
242
|
...(parsed.values["agents-root"]
|
|
223
243
|
? { agentsRoot: resolveAgentsRoot(parsed.values["agents-root"], deps.userHome, deps.platform) }
|
|
224
|
-
:
|
|
244
|
+
: (deps.env.NOWWORK_LOCAL_HOME !== undefined || configuredLocalHomeMode !== undefined)
|
|
245
|
+
? { agentsRoot: resolveAgentsRoot((deps.platform === "win32" ? win32 : posix).join((deps.platform === "win32" ? win32 : posix).dirname(home), `agents-${name}`), deps.userHome, deps.platform) }
|
|
246
|
+
: {}),
|
|
225
247
|
...(deps.env.PATH ? { runtimePath: deps.env.PATH } : {}),
|
|
226
248
|
};
|
|
227
249
|
await saveProfile(profile, home, profileStorage);
|
package/dist/computer-profile.js
CHANGED
|
@@ -9,6 +9,7 @@ import { acquireProfileSaveLock, } from "./computer-profile-lock.js";
|
|
|
9
9
|
import { formatDaemonText } from "./i18n.js";
|
|
10
10
|
import { atomicPrivateWrite } from "./atomic-private-write.js";
|
|
11
11
|
import { normalizeDaemonNpmRegistry } from "./npm-registry.js";
|
|
12
|
+
import { resolveDaemonLocalHome } from "./daemon-local-home.js";
|
|
12
13
|
const PROFILE_NAME = /^[a-z0-9][a-z0-9_-]{0,47}$/;
|
|
13
14
|
export const MachineTokenSchema = z.string().startsWith("sk_machine_").max(4096);
|
|
14
15
|
const ServerUrlSchema = z.string().url().max(2048).refine((value) => {
|
|
@@ -94,8 +95,20 @@ export class ProfileAgentsRootConflictError extends Error {
|
|
|
94
95
|
}
|
|
95
96
|
const quotePosixArgument = (value) => `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
96
97
|
const quotePowerShellArgument = (value) => `'${value.replaceAll("'", "''")}'`;
|
|
97
|
-
export function daemonHome(env = process.env) {
|
|
98
|
-
|
|
98
|
+
export function daemonHome(env = process.env, profile) {
|
|
99
|
+
if (env.CREW_DAEMON_HOME !== undefined)
|
|
100
|
+
return resolve(env.CREW_DAEMON_HOME);
|
|
101
|
+
const mode = env.NOWWORK_LOCAL_HOME_MODE ?? env.NOWWORK_STORAGE_MODE;
|
|
102
|
+
if (profile !== undefined && (env.NOWWORK_LOCAL_HOME !== undefined || mode !== undefined)) {
|
|
103
|
+
return resolveDaemonLocalHome({
|
|
104
|
+
userHome: homedir(),
|
|
105
|
+
platform: process.platform,
|
|
106
|
+
profile,
|
|
107
|
+
...(env.NOWWORK_LOCAL_HOME === undefined ? {} : { explicit: env.NOWWORK_LOCAL_HOME }),
|
|
108
|
+
...(mode === undefined ? {} : { mode }),
|
|
109
|
+
}).daemonHome;
|
|
110
|
+
}
|
|
111
|
+
return resolve(homedir(), ".crew/daemon");
|
|
99
112
|
}
|
|
100
113
|
export function resolveAgentsRoot(configured, userHome = homedir(), platform = process.platform) {
|
|
101
114
|
const pathApi = platform === "win32" ? win32 : posix;
|
package/dist/config.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** daemon 配置:从 env 读取,解析 crew CLI 路径 (注入给 agent 的 wrapper 用)。 */
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
-
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
6
|
import { detectDaemonLang, translateDaemon } from "./i18n.js";
|
|
@@ -28,6 +28,23 @@ export const MIN_EXECUTION_EVENT_BYTES = 512;
|
|
|
28
28
|
export const MAX_EXECUTION_TIMEOUT_MS = 2_147_483_647;
|
|
29
29
|
export class ConfigError extends Error {
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Resolve the shared workspace repository root from the daemon's agent root.
|
|
33
|
+
*
|
|
34
|
+
* Standard NowWork storage keeps `agent-items`, `daemon`, `repos`, and the QA
|
|
35
|
+
* report hub as siblings. Legacy/standalone profiles may still point at an
|
|
36
|
+
* arbitrary agents directory, for which the historical `<agentsRoot>/repos`
|
|
37
|
+
* layout remains the compatible fallback.
|
|
38
|
+
*/
|
|
39
|
+
export function resolveReposRoot(agentsRoot, configured) {
|
|
40
|
+
const explicit = configured?.trim();
|
|
41
|
+
if (explicit)
|
|
42
|
+
return resolve(explicit);
|
|
43
|
+
const resolvedAgentsRoot = resolve(agentsRoot);
|
|
44
|
+
return basename(resolvedAgentsRoot) === "agent-items"
|
|
45
|
+
? join(dirname(resolvedAgentsRoot), "repos")
|
|
46
|
+
: join(resolvedAgentsRoot, "repos");
|
|
47
|
+
}
|
|
31
48
|
function positiveIntegerEnv(env, field, fallback) {
|
|
32
49
|
const raw = env[field];
|
|
33
50
|
const value = raw === undefined ? fallback : Number(raw);
|
|
@@ -136,16 +153,19 @@ export function loadConfig(env = process.env) {
|
|
|
136
153
|
});
|
|
137
154
|
const agentsRoot = resolveAgentsRoot(env.CREW_AGENTS_ROOT, homedir());
|
|
138
155
|
const configuredReposRoot = env.NOWWORK_REPOS_ROOT?.trim();
|
|
139
|
-
const reposRoot =
|
|
156
|
+
const reposRoot = resolveReposRoot(agentsRoot, configuredReposRoot);
|
|
140
157
|
const configuredQaReportRoot = env.QA_REPORT_HUB?.trim();
|
|
158
|
+
const defaultQaReportRoot = configuredReposRoot
|
|
159
|
+
? join(dirname(reposRoot), "qa-report-server-v2")
|
|
160
|
+
: join(dirname(agentsRoot), "qa-report-server-v2");
|
|
141
161
|
return {
|
|
142
162
|
serverUrl,
|
|
143
163
|
machineToken,
|
|
144
164
|
agentsRoot,
|
|
145
|
-
|
|
165
|
+
reposRoot,
|
|
146
166
|
qaReportRoot: configuredQaReportRoot
|
|
147
167
|
? resolve(configuredQaReportRoot)
|
|
148
|
-
:
|
|
168
|
+
: defaultQaReportRoot,
|
|
149
169
|
cliPath: env.CREW_CLI_PATH ?? defaultCliPath(),
|
|
150
170
|
runtimeBin: env.CREW_RUNTIME ?? "claude",
|
|
151
171
|
codexHomeMode: codexHomeModeEnv(env),
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { posix, win32 } from "node:path";
|
|
3
|
+
const PROFILE = /^[a-z0-9][a-z0-9_-]{0,47}$/u;
|
|
4
|
+
const CONTROL = /[\0\r\n]/u;
|
|
5
|
+
const pathsFor = (platform) => platform === "win32" ? win32 : posix;
|
|
6
|
+
export function parseDaemonLocalHomeMode(value) {
|
|
7
|
+
if (value === undefined || value.trim() === "")
|
|
8
|
+
return "auto";
|
|
9
|
+
const normalized = value.trim().toLowerCase();
|
|
10
|
+
if (normalized === "auto" || normalized === "nowwork" || normalized === "legacy")
|
|
11
|
+
return normalized;
|
|
12
|
+
throw new Error("NOWWORK_LOCAL_HOME_MODE must be auto, nowwork or legacy");
|
|
13
|
+
}
|
|
14
|
+
function absoluteRoot(value, userHome, platform) {
|
|
15
|
+
const paths = pathsFor(platform);
|
|
16
|
+
if (value.length === 0 || CONTROL.test(value))
|
|
17
|
+
throw new Error("NOWWORK_LOCAL_HOME must be a non-empty single-line path");
|
|
18
|
+
const expanded = value === "~"
|
|
19
|
+
? userHome
|
|
20
|
+
: value.startsWith("~/") || (platform === "win32" && value.startsWith("~\\"))
|
|
21
|
+
? paths.join(userHome, value.slice(2))
|
|
22
|
+
: value;
|
|
23
|
+
if (!paths.isAbsolute(expanded))
|
|
24
|
+
throw new Error("NOWWORK_LOCAL_HOME must be absolute or start with '~/'");
|
|
25
|
+
return paths.normalize(expanded);
|
|
26
|
+
}
|
|
27
|
+
function legacyDaemonDirectory(profile) {
|
|
28
|
+
if (profile === "in" || profile === "production")
|
|
29
|
+
return "daemon";
|
|
30
|
+
if (profile === "dev")
|
|
31
|
+
return "daemon-dev";
|
|
32
|
+
if (profile.startsWith("nw-"))
|
|
33
|
+
return `daemon-${profile.slice(3)}`;
|
|
34
|
+
return `daemon-${profile}`;
|
|
35
|
+
}
|
|
36
|
+
function resolution(mode, root, profile, platform, source) {
|
|
37
|
+
const paths = pathsFor(platform);
|
|
38
|
+
return Object.freeze({
|
|
39
|
+
mode,
|
|
40
|
+
root,
|
|
41
|
+
daemonHome: mode === "nowwork" ? paths.join(root, "config") : paths.join(root, legacyDaemonDirectory(profile)),
|
|
42
|
+
runtimePrefix: paths.join(root, "daemon-runtimes", profile),
|
|
43
|
+
source,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
export function resolveDaemonLocalHome(input) {
|
|
47
|
+
if (!PROFILE.test(input.profile))
|
|
48
|
+
throw new Error("daemon profile is invalid");
|
|
49
|
+
const mode = parseDaemonLocalHomeMode(input.mode);
|
|
50
|
+
const paths = pathsFor(input.platform);
|
|
51
|
+
const nowworkRoot = absoluteRoot(input.explicit ?? paths.join(input.userHome, ".nowwork"), input.userHome, input.platform);
|
|
52
|
+
const legacyRoot = absoluteRoot(paths.join(input.userHome, ".crew"), input.userHome, input.platform);
|
|
53
|
+
if (input.explicit !== undefined && mode !== "legacy") {
|
|
54
|
+
return resolution("nowwork", nowworkRoot, input.profile, input.platform, "explicit");
|
|
55
|
+
}
|
|
56
|
+
if (mode === "nowwork")
|
|
57
|
+
return resolution("nowwork", nowworkRoot, input.profile, input.platform, "nowwork_forced");
|
|
58
|
+
if (mode === "legacy")
|
|
59
|
+
return resolution("legacy", legacyRoot, input.profile, input.platform, "legacy_forced");
|
|
60
|
+
const exists = input.exists ?? existsSync;
|
|
61
|
+
const nowwork = resolution("nowwork", nowworkRoot, input.profile, input.platform, "auto_nowwork_existing");
|
|
62
|
+
const legacy = resolution("legacy", legacyRoot, input.profile, input.platform, "auto_legacy_existing");
|
|
63
|
+
const hasNowwork = exists(nowwork.runtimePrefix) || exists(paths.join(nowwork.daemonHome, "profiles", `${input.profile}.json`));
|
|
64
|
+
const hasLegacy = exists(legacy.runtimePrefix) || exists(paths.join(legacy.daemonHome, "profiles", `${input.profile}.json`));
|
|
65
|
+
if (hasNowwork && hasLegacy) {
|
|
66
|
+
throw new Error(`Daemon local home conflict: profile '${input.profile}' exists under both .crew and .nowwork`);
|
|
67
|
+
}
|
|
68
|
+
if (hasNowwork)
|
|
69
|
+
return nowwork;
|
|
70
|
+
if (hasLegacy)
|
|
71
|
+
return legacy;
|
|
72
|
+
return resolution("nowwork", nowworkRoot, input.profile, input.platform, "auto_new");
|
|
73
|
+
}
|
|
@@ -125,7 +125,15 @@ export function createDaemonMigrationController(input) {
|
|
|
125
125
|
if (currentProfile.machineToken !== input.machineToken)
|
|
126
126
|
throw new Error("active profile token does not match this daemon");
|
|
127
127
|
currentRegistryUrl = currentProfile.registryUrl;
|
|
128
|
-
const target = resolveAutomaticInstallTarget({
|
|
128
|
+
const target = resolveAutomaticInstallTarget({
|
|
129
|
+
serverUrl: input.serverUrl,
|
|
130
|
+
requestedProfile: message.targetProfile,
|
|
131
|
+
userHome,
|
|
132
|
+
platform,
|
|
133
|
+
// This controller migrates the legacy service layout; the new local-home
|
|
134
|
+
// standard is handled by the storage migration controller.
|
|
135
|
+
requestedLocalHomeMode: "legacy",
|
|
136
|
+
});
|
|
129
137
|
if (!isAbsolute(message.targetDaemonHome) || !isAbsolute(message.targetAgentsRoot)
|
|
130
138
|
|| resolve(message.targetDaemonHome) !== resolve(target.daemonHome)
|
|
131
139
|
|| resolve(message.targetAgentsRoot) !== resolve(target.agentsRoot))
|
package/dist/execution-runner.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { resolveReposRoot } from "./config.js";
|
|
3
4
|
import { DaemonToServerExecutionFrameSchema, ExecutionCompletedSchema, ExecutionRejectedSchema, ExecutionStartSchema, } from "./execution-protocol.js";
|
|
4
5
|
import { JournalConflictError } from "./execution-journal.js";
|
|
5
6
|
import { boundExecutionFrame } from "./execution-event-limit.js";
|
|
@@ -524,7 +525,7 @@ export async function runExecution(config, input, dependencies) {
|
|
|
524
525
|
if (dependencies.abilityRelease !== undefined) {
|
|
525
526
|
await cancellable(dependencies.abilityRelease.apply(config.agentsRoot, spec.agent.handle, spec.agent.abilityRelease), dependencies.cancellation);
|
|
526
527
|
}
|
|
527
|
-
const reposRoot =
|
|
528
|
+
const reposRoot = resolveReposRoot(config.agentsRoot, config.reposRoot);
|
|
528
529
|
const preparedProjectDependencies = await cancellable(dependencies.projectDependencies.prepare({
|
|
529
530
|
trainingRoot: join(config.agentsRoot, spec.agent.handle, "training"),
|
|
530
531
|
reposRoot,
|
|
@@ -863,7 +864,7 @@ export async function runExecution(config, input, dependencies) {
|
|
|
863
864
|
token: credential.token,
|
|
864
865
|
agentId: credential.agentId,
|
|
865
866
|
agentsRoot: config.agentsRoot,
|
|
866
|
-
|
|
867
|
+
reposRoot: resolveReposRoot(config.agentsRoot, config.reposRoot),
|
|
867
868
|
...(config.qaReportRoot === undefined ? {} : { qaReportRoot: config.qaReportRoot }),
|
|
868
869
|
cliPath: config.cliPath,
|
|
869
870
|
providerConfig,
|
package/dist/local-executor.js
CHANGED
|
@@ -466,6 +466,11 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
466
466
|
CREW_TOKEN: input.launch.token,
|
|
467
467
|
CREW_CHANNEL: input.channelId,
|
|
468
468
|
CREW_HOME: workspace.dir,
|
|
469
|
+
// Keep the daemon-selected storage root authoritative for runtimes.
|
|
470
|
+
// In profile-backed NowWork sessions the daemon config may differ from
|
|
471
|
+
// the daemon process environment, so inheriting CREW_AGENTS_ROOT is not
|
|
472
|
+
// sufficient for QA/project diagnostics.
|
|
473
|
+
CREW_AGENTS_ROOT: input.launch.agentsRoot,
|
|
469
474
|
...(input.launch.reposRoot === undefined ? {} : { NOWWORK_REPOS_ROOT: input.launch.reposRoot }),
|
|
470
475
|
...(input.launch.qaReportRoot === undefined ? {} : { QA_REPORT_HUB: input.launch.qaReportRoot }),
|
|
471
476
|
// Expose only the training root; ability-specific workspace layout stays
|
package/dist/main.js
CHANGED
|
@@ -74,7 +74,7 @@ async function main() {
|
|
|
74
74
|
process.env.CREW_DAEMON_HOME = values["daemon-home"];
|
|
75
75
|
let activeDaemonHome;
|
|
76
76
|
if (values.profile) {
|
|
77
|
-
const home = daemonHome();
|
|
77
|
+
const home = daemonHome(process.env, values.profile);
|
|
78
78
|
activeDaemonHome = home;
|
|
79
79
|
const profile = await loadProfile(values.profile, home);
|
|
80
80
|
if (cmd === "serve") {
|
|
@@ -16,12 +16,18 @@ export async function initializeAfterProjectSkillLeaseProtection(input) {
|
|
|
16
16
|
/**
|
|
17
17
|
* Runs only after execution-journal restart reconciliation has settled. A lease without a terminal
|
|
18
18
|
* journal fact remains durable and prevents the caller from marking Project Skill v2 ready.
|
|
19
|
+
*
|
|
20
|
+
* 重连时本进程仍在跑的 execution 合法持有 lease(`liveExecutionIds`):既不释放,也不算 unresolved。
|
|
21
|
+
* 否则一次网络抖动就会让 daemon 在整条连接上摘掉 Project Skills 能力(incident: 2026-09-02,
|
|
22
|
+
* 绑定项目的 Agent 全部 execution_spec_incompatible)。
|
|
19
23
|
*/
|
|
20
24
|
export async function recoverProjectSkillRuntimeRoots(input) {
|
|
21
25
|
for (const agentRoot of input.agentRoots) {
|
|
22
26
|
await input.roots.recover(agentRoot);
|
|
23
27
|
let unresolvedLease = false;
|
|
24
28
|
await input.leases.recover(agentRoot, async (executionId) => {
|
|
29
|
+
if (input.liveExecutionIds?.has(executionId))
|
|
30
|
+
return false;
|
|
25
31
|
const entry = await input.journal.get(executionId);
|
|
26
32
|
const releasable = entry?.state === "completed" || entry?.state === "interrupted";
|
|
27
33
|
if (!releasable)
|
|
@@ -22,13 +22,14 @@ export function createProjectSkillRuntimeRootStartup() {
|
|
|
22
22
|
leases,
|
|
23
23
|
initialize,
|
|
24
24
|
}),
|
|
25
|
-
recover: async (agentsRoot, journal, protectedExecutionIds) => {
|
|
25
|
+
recover: async (agentsRoot, journal, protectedExecutionIds, liveExecutionIds) => {
|
|
26
26
|
await recoverProjectSkillRuntimeRoots({
|
|
27
27
|
agentRoots: await agentRoots(agentsRoot),
|
|
28
28
|
journal,
|
|
29
29
|
roots,
|
|
30
30
|
leases,
|
|
31
31
|
gc,
|
|
32
|
+
...(liveExecutionIds === undefined ? {} : { liveExecutionIds }),
|
|
32
33
|
currentRootId: async (agentRoot) => (await readAppliedProjectSkillManifest(agentRoot))?.rootId ?? null,
|
|
33
34
|
afterLeaseRecovery: async () => {
|
|
34
35
|
const remaining = await initializeAfterProjectSkillLeaseProtection({
|
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
import { createExecutionJournal } from "../execution-journal.js";
|
|
2
2
|
import { reconcileExecutionJournal } from "../execution-recovery.js";
|
|
3
3
|
import { dslog, flushSlog } from "../slog.js";
|
|
4
|
+
/**
|
|
5
|
+
* 每次连接 preflight 的 Runtime root / lease 恢复。重连时本进程仍在跑的 execution 合法持有 lease,
|
|
6
|
+
* 必须当作存活而非孤儿;失败也必须留痕,否则整条连接静默摘掉 Project Skills 能力
|
|
7
|
+
* (incident: 2026-09-02,绑定项目的 Agent 全部 execution_spec_incompatible)。
|
|
8
|
+
*/
|
|
9
|
+
export function createProjectSkillReconnectRecovery(startup, agentsRoot, journal, protectedExecutionIds, liveExecutionIds) {
|
|
10
|
+
return async () => {
|
|
11
|
+
const live = liveExecutionIds();
|
|
12
|
+
try {
|
|
13
|
+
await startup.recover(agentsRoot, journal(), protectedExecutionIds, live);
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
dslog("project_skills.preflight_recovery_failed", "Project Skill runtime root 恢复失败,本连接不宣告 Project Skills 能力", {
|
|
18
|
+
level: "ERROR",
|
|
19
|
+
error_code: error.code ?? "unknown",
|
|
20
|
+
error_message: error.message,
|
|
21
|
+
live_execution_count: live.size,
|
|
22
|
+
});
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
4
27
|
export async function initializeProjectSkillProtectedExecutionJournal(input) {
|
|
5
28
|
return input.startup.initializeAfterLeaseProtection(input.config.agentsRoot, async (snapshot) => {
|
|
6
29
|
input.protectedExecutionIds.clear();
|
package/dist/runner.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** Legacy agent orchestration around the business-neutral local executor. */
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { resolveReposRoot } from "./config.js";
|
|
4
5
|
import { mintAgentToken } from "./token.js";
|
|
5
6
|
import { buildSystemPrompt, buildWakePrompt, capMemoryForInject, capWorkLogForInject } from "./prompt.js";
|
|
6
7
|
import { deliverScheduledReport, } from "./scheduled-report.js";
|
|
@@ -84,7 +85,7 @@ export async function runAgent(config, input, onActivity = defaultPrint, onConso
|
|
|
84
85
|
token: credential.token,
|
|
85
86
|
agentId: credential.agentId,
|
|
86
87
|
agentsRoot: config.agentsRoot,
|
|
87
|
-
|
|
88
|
+
reposRoot: resolveReposRoot(config.agentsRoot, config.reposRoot),
|
|
88
89
|
...(config.qaReportRoot === undefined ? {} : { qaReportRoot: config.qaReportRoot }),
|
|
89
90
|
cliPath: config.cliPath,
|
|
90
91
|
providerConfig,
|
package/dist/serve.js
CHANGED
|
@@ -42,7 +42,7 @@ import { PROJECT_SKILLS_CAPABILITY } from "./project-skills/types.js";
|
|
|
42
42
|
import { createAgentProjectionCoordinator } from "./project-skills/agent-projection-coordinator.js";
|
|
43
43
|
import { createProjectSkillRuntimeRootStartup, } from "./project-skills/runtime-root-startup.js";
|
|
44
44
|
import { createProjectSkillControllerReadiness, withProjectSkillCapabilityStatus } from "./project-skills/capability.js";
|
|
45
|
-
import { initializeProjectSkillProtectedExecutionJournal } from "./project-skills/serve-startup.js";
|
|
45
|
+
import { createProjectSkillReconnectRecovery, initializeProjectSkillProtectedExecutionJournal } from "./project-skills/serve-startup.js";
|
|
46
46
|
import { createProjectSkillConnectionCapabilityProvider, createProjectSkillMachineHelloPublisher } from "./project-skills/serve-capability.js";
|
|
47
47
|
import { createAgentAbilityRuntime } from "./agent-ability/runtime.js";
|
|
48
48
|
import { createProjectSkillsReconciler, } from "./project-skills/reconciler.js";
|
|
@@ -215,9 +215,7 @@ export function serve(config, opts = {}) {
|
|
|
215
215
|
additionalCapabilities: () => managedDaemonCapabilities(updateEligibility),
|
|
216
216
|
probeRuntimeRootStorePrimitive: projectSkillRuntimeRootStartup.probeNativePublicationPrimitiveReadiness,
|
|
217
217
|
initializeController: projectSkillControllerReadiness.ensure,
|
|
218
|
-
recoverRuntimeRootsAndLeases:
|
|
219
|
-
.recover(config.agentsRoot, executionJournal, protectedExecutionIds)
|
|
220
|
-
.then(() => true, () => false),
|
|
218
|
+
recoverRuntimeRootsAndLeases: createProjectSkillReconnectRecovery(projectSkillRuntimeRootStartup, config.agentsRoot, () => executionJournal, protectedExecutionIds, () => new Set(executionRuns.keys())),
|
|
221
219
|
applyReadiness: projectSkillControllerReadiness.applyPreflight,
|
|
222
220
|
});
|
|
223
221
|
const safeExecutionSend = (frame) => {
|
|
@@ -827,9 +825,9 @@ export function serve(config, opts = {}) {
|
|
|
827
825
|
// ready 帧带 server 视角的 machineId/workspaceId → 作为后续所有日志的默认关联键
|
|
828
826
|
const r = msg;
|
|
829
827
|
setSlogDefaults({ machine_id: r.machineId, workspace_id: r.workspaceId });
|
|
830
|
-
if (opts.storageMigration?.ready === true && typeof r.workspaceId === "string") {
|
|
828
|
+
if (opts.storageMigration?.ready === true && typeof r.workspaceId === "string" && typeof r.machineId === "string") {
|
|
831
829
|
try {
|
|
832
|
-
opts.storageMigration.bindServerIdentity?.({ workspaceId: r.workspaceId });
|
|
830
|
+
opts.storageMigration.bindServerIdentity?.({ workspaceId: r.workspaceId, machineId: r.machineId });
|
|
833
831
|
machineHelloPublisher.republish();
|
|
834
832
|
}
|
|
835
833
|
catch (error) {
|
package/dist/storage-control.js
CHANGED
|
@@ -5,6 +5,8 @@ const IdentitySchema = z.object({
|
|
|
5
5
|
migrationId: z.string().uuid(),
|
|
6
6
|
environment: z.string().regex(/^[a-z0-9][a-z0-9_-]{0,31}$/u),
|
|
7
7
|
workspaceId: z.string().uuid(),
|
|
8
|
+
ownerEpoch: z.number().int().positive().safe().optional(),
|
|
9
|
+
ownershipClaimFingerprint: z.string().regex(/^sha256:[0-9a-f]{64}$/u).optional(),
|
|
8
10
|
});
|
|
9
11
|
const DoctorRequestSchema = IdentitySchema.extend({ type: z.literal("daemon:storage-doctor") }).strict();
|
|
10
12
|
const MigrateRequestSchema = IdentitySchema.extend({
|
|
@@ -26,6 +28,11 @@ export function createStorageControlController(input) {
|
|
|
26
28
|
const profile = z.string().regex(/^[a-z0-9][a-z0-9_-]{0,47}$/u).parse(input.profile);
|
|
27
29
|
const sourceStorageFingerprint = z.string().regex(/^sha256:[0-9a-f]{64}$/u).parse(input.sourceStorageFingerprint);
|
|
28
30
|
const targetStorageFingerprint = z.string().regex(/^sha256:[0-9a-f]{64}$/u).parse(input.targetStorageFingerprint);
|
|
31
|
+
const ownership = input.ownership === undefined ? undefined : Object.freeze({
|
|
32
|
+
ownerEpoch: z.number().int().positive().safe().parse(input.ownership.ownerEpoch),
|
|
33
|
+
ownershipClaimFingerprint: z.string().regex(/^sha256:[0-9a-f]{64}$/u)
|
|
34
|
+
.parse(input.ownership.ownershipClaimFingerprint),
|
|
35
|
+
});
|
|
29
36
|
const preflights = new Map();
|
|
30
37
|
const preflightRuns = new Map();
|
|
31
38
|
const appliedMigrationIds = new Set();
|
|
@@ -39,6 +46,7 @@ export function createStorageControlController(input) {
|
|
|
39
46
|
environment,
|
|
40
47
|
workspaceId,
|
|
41
48
|
profile,
|
|
49
|
+
...(ownership === undefined ? {} : ownership),
|
|
42
50
|
...extra,
|
|
43
51
|
});
|
|
44
52
|
const failure = (identity, errorCode, error, reopenAdmission = false, extra = {}) => {
|
|
@@ -59,7 +67,10 @@ export function createStorageControlController(input) {
|
|
|
59
67
|
input.onStatusError?.(error, statusFrame);
|
|
60
68
|
}
|
|
61
69
|
};
|
|
62
|
-
const matchesIdentity = (identity) => identity.environment === environment && identity.workspaceId === workspaceId
|
|
70
|
+
const matchesIdentity = (identity) => identity.environment === environment && identity.workspaceId === workspaceId
|
|
71
|
+
&& (ownership === undefined
|
|
72
|
+
|| (identity.ownerEpoch === ownership.ownerEpoch
|
|
73
|
+
&& identity.ownershipClaimFingerprint === ownership.ownershipClaimFingerprint));
|
|
63
74
|
const preflightKey = (identity) => `${identity.migrationId}\0${identity.environment}\0${identity.workspaceId}`;
|
|
64
75
|
const runPreflight = (identity, failureFallback = "doctor_failed") => {
|
|
65
76
|
const key = preflightKey(identity);
|
package/dist/storage-identity.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { STORAGE_LAYOUT_VERSION } from "./storage-layout.js";
|
|
3
|
+
import { createStorageOwnershipClaim } from "./storage-ownership.js";
|
|
3
4
|
function fingerprint(namespace, value) {
|
|
4
5
|
return `sha256:${createHash("sha256").update(`${namespace}\0${value}`).digest("hex")}`;
|
|
5
6
|
}
|
|
@@ -14,14 +15,27 @@ export function storageHelloIdentity(input) {
|
|
|
14
15
|
|| installation.runtimeRoot !== layout.runtimeRoot) {
|
|
15
16
|
throw new Error("installation storage identity does not match resolved layout");
|
|
16
17
|
}
|
|
18
|
+
const workspaceRootFingerprint = fingerprint("workspace-root", layout.workspaceRoot);
|
|
19
|
+
let ownership;
|
|
20
|
+
if (input.ownership !== undefined) {
|
|
21
|
+
const { claimFingerprint: reportedFingerprint, ...claimIdentity } = input.ownership;
|
|
22
|
+
ownership = createStorageOwnershipClaim(claimIdentity);
|
|
23
|
+
if (ownership.claimFingerprint !== reportedFingerprint
|
|
24
|
+
|| ownership.workspaceId !== layout.workspaceId
|
|
25
|
+
|| ownership.environment !== layout.environment
|
|
26
|
+
|| ownership.workspaceRootFingerprint !== workspaceRootFingerprint) {
|
|
27
|
+
throw new Error("storage ownership identity does not match resolved layout");
|
|
28
|
+
}
|
|
29
|
+
}
|
|
17
30
|
return Object.freeze({
|
|
18
31
|
storageLayoutVersion: STORAGE_LAYOUT_VERSION,
|
|
19
32
|
storageEnvironment: layout.environment,
|
|
20
33
|
storageWorkspaceId: layout.workspaceId,
|
|
21
34
|
storageIdFingerprint: storageIdFingerprint(installation.storageId),
|
|
22
|
-
workspaceRootFingerprint
|
|
35
|
+
workspaceRootFingerprint,
|
|
23
36
|
daemonRootFingerprint: fingerprint("daemon-root", layout.daemonRoot),
|
|
24
37
|
agentRootFingerprint: fingerprint("agent-root", layout.agentItemsRoot),
|
|
38
|
+
...(ownership === undefined ? {} : { storageOwnership: ownership }),
|
|
25
39
|
...(installation.storageMigrationId === undefined
|
|
26
40
|
? {}
|
|
27
41
|
: { storageMigrationId: installation.storageMigrationId }),
|
|
@@ -27,6 +27,8 @@ const ReceiptSchema = z.object({
|
|
|
27
27
|
environment: z.string().min(1),
|
|
28
28
|
workspaceId: z.string().uuid(),
|
|
29
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(),
|
|
30
32
|
manifestDigest: z.string().regex(/^[0-9a-f]{64}$/u),
|
|
31
33
|
targetFingerprint: z.string().regex(/^sha256:[0-9a-f]{64}$/u),
|
|
32
34
|
status: ReceiptStatusSchema,
|
|
@@ -37,13 +39,23 @@ const ReceiptSchema = z.object({
|
|
|
37
39
|
sourceStopAttempted: z.boolean().optional(),
|
|
38
40
|
sourceStopped: z.boolean().optional(),
|
|
39
41
|
targetTouched: z.boolean().optional(),
|
|
40
|
-
}).strict()
|
|
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
|
+
});
|
|
41
47
|
const MigrationLockSchema = z.object({
|
|
42
48
|
version: z.literal(1),
|
|
43
49
|
migrationId: z.string().uuid(),
|
|
44
50
|
pid: z.number().int().positive(),
|
|
45
51
|
createdAt: z.string().datetime({ offset: true }),
|
|
46
52
|
}).strict();
|
|
53
|
+
const REQUIRED_BACKUP_ARTIFACTS = Object.freeze([
|
|
54
|
+
"profile.json",
|
|
55
|
+
"service-descriptor",
|
|
56
|
+
"managed-service-registry.json",
|
|
57
|
+
"installations.json",
|
|
58
|
+
]);
|
|
47
59
|
export class StorageMigrationFailure extends Error {
|
|
48
60
|
code;
|
|
49
61
|
receipt;
|
|
@@ -204,6 +216,16 @@ function assertOwnedStaging(input) {
|
|
|
204
216
|
}
|
|
205
217
|
}
|
|
206
218
|
async function backupArtifacts(input, receipt) {
|
|
219
|
+
const providedNames = new Set(input.backupArtifacts.map((artifact) => artifact.name));
|
|
220
|
+
const missing = REQUIRED_BACKUP_ARTIFACTS.filter((name) => !providedNames.has(name));
|
|
221
|
+
if (missing.length > 0) {
|
|
222
|
+
throw new StorageMigrationFailure("backup_artifact_missing", `required rollback artifacts are missing: ${missing.join(", ")}`);
|
|
223
|
+
}
|
|
224
|
+
for (const artifact of input.backupArtifacts) {
|
|
225
|
+
if (!(await lstat(artifact.path)).isFile()) {
|
|
226
|
+
throw new StorageMigrationFailure("backup_artifact_invalid", "backup artifact must be a file");
|
|
227
|
+
}
|
|
228
|
+
}
|
|
207
229
|
const directory = join(input.backupsRoot, receipt.migrationId);
|
|
208
230
|
const artifactsDirectory = join(directory, "artifacts");
|
|
209
231
|
await mkdir(artifactsDirectory, { recursive: true, mode: 0o700 });
|
|
@@ -217,8 +239,6 @@ async function backupArtifacts(input, receipt) {
|
|
|
217
239
|
throw new StorageMigrationFailure("backup_artifact_invalid", "backup artifact names must be unique safe file names");
|
|
218
240
|
}
|
|
219
241
|
names.add(artifact.name);
|
|
220
|
-
if (!(await lstat(artifact.path)).isFile())
|
|
221
|
-
throw new StorageMigrationFailure("backup_artifact_invalid", "backup artifact must be a file");
|
|
222
242
|
const destination = join(artifactsDirectory, artifact.name);
|
|
223
243
|
await copyFile(artifact.path, destination);
|
|
224
244
|
await chmod(destination, 0o600);
|
|
@@ -271,6 +291,7 @@ export async function runStorageMigration(input, dependencies) {
|
|
|
271
291
|
environment: input.doctor.environment,
|
|
272
292
|
workspaceId: input.doctor.workspaceId,
|
|
273
293
|
profile: input.doctor.profile,
|
|
294
|
+
...(input.ownership === undefined ? {} : input.ownership),
|
|
274
295
|
manifestDigest: input.doctor.manifestDigest,
|
|
275
296
|
targetFingerprint: targetFingerprint(input.targetWorkspaceRoot),
|
|
276
297
|
status: "preflight_passed",
|
|
@@ -301,7 +322,7 @@ export async function runStorageMigration(input, dependencies) {
|
|
|
301
322
|
input.assertHealthy?.();
|
|
302
323
|
await transition("backing_up");
|
|
303
324
|
receipt = await backupArtifacts(input, receipt).catch((error) => {
|
|
304
|
-
failureCode = "backup_failed";
|
|
325
|
+
failureCode = error instanceof StorageMigrationFailure ? error.code : "backup_failed";
|
|
305
326
|
throw error;
|
|
306
327
|
});
|
|
307
328
|
await writeReceipt(path, receipt);
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { lstat, readFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
const FingerprintSchema = z.string().regex(/^sha256:[0-9a-f]{64}$/u);
|
|
6
|
+
const StorageOwnershipClaimSchema = z.object({
|
|
7
|
+
version: z.literal(1),
|
|
8
|
+
machineId: z.string().uuid(),
|
|
9
|
+
workspaceId: z.string().uuid(),
|
|
10
|
+
environment: z.string().regex(/^[a-z0-9][a-z0-9_-]{0,31}$/u),
|
|
11
|
+
volumeFingerprint: FingerprintSchema,
|
|
12
|
+
workspaceRootFingerprint: FingerprintSchema,
|
|
13
|
+
ownerEpoch: z.number().int().positive().safe(),
|
|
14
|
+
claimFingerprint: FingerprintSchema,
|
|
15
|
+
}).strict();
|
|
16
|
+
function claimFingerprint(identity) {
|
|
17
|
+
const digest = createHash("sha256").update([
|
|
18
|
+
"nowwork-storage-ownership-v1",
|
|
19
|
+
identity.machineId,
|
|
20
|
+
identity.workspaceId,
|
|
21
|
+
identity.environment,
|
|
22
|
+
identity.volumeFingerprint,
|
|
23
|
+
identity.workspaceRootFingerprint,
|
|
24
|
+
String(identity.ownerEpoch),
|
|
25
|
+
].join("\0")).digest("hex");
|
|
26
|
+
return `sha256:${digest}`;
|
|
27
|
+
}
|
|
28
|
+
export function createStorageOwnershipClaim(input) {
|
|
29
|
+
const parsed = StorageOwnershipClaimSchema.omit({ version: true, claimFingerprint: true }).parse(input);
|
|
30
|
+
const identity = Object.freeze({
|
|
31
|
+
...parsed,
|
|
32
|
+
machineId: parsed.machineId.toLowerCase(),
|
|
33
|
+
workspaceId: parsed.workspaceId.toLowerCase(),
|
|
34
|
+
environment: parsed.environment.toLowerCase(),
|
|
35
|
+
});
|
|
36
|
+
return Object.freeze({ ...identity, claimFingerprint: claimFingerprint(identity) });
|
|
37
|
+
}
|
|
38
|
+
export function storageOwnershipClaimPath(nowworkHome) {
|
|
39
|
+
return join(nowworkHome, "storage-layout.json");
|
|
40
|
+
}
|
|
41
|
+
export async function readStorageOwnershipClaim(nowworkHome) {
|
|
42
|
+
const path = storageOwnershipClaimPath(nowworkHome);
|
|
43
|
+
const metadata = await lstat(path);
|
|
44
|
+
if (!metadata.isFile())
|
|
45
|
+
throw new Error("storage ownership claim must be a file");
|
|
46
|
+
if (metadata.size > 16 * 1024)
|
|
47
|
+
throw new Error("storage ownership claim file is too large");
|
|
48
|
+
const parsed = StorageOwnershipClaimSchema.parse(JSON.parse(await readFile(path, "utf8")));
|
|
49
|
+
const { version: _version, claimFingerprint: reportedFingerprint, ...identity } = parsed;
|
|
50
|
+
const expected = createStorageOwnershipClaim(identity);
|
|
51
|
+
if (reportedFingerprint !== expected.claimFingerprint) {
|
|
52
|
+
throw new Error("storage ownership claim fingerprint is invalid");
|
|
53
|
+
}
|
|
54
|
+
return expected;
|
|
55
|
+
}
|
|
@@ -23,9 +23,10 @@ function deterministicUuid(value) {
|
|
|
23
23
|
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
24
24
|
}
|
|
25
25
|
function storageId(input, workspaceId) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
if (input.ownership.workspaceId !== workspaceId || input.ownership.environment !== input.environment) {
|
|
27
|
+
throw new Error("Storage ownership claim does not match the migration identity");
|
|
28
|
+
}
|
|
29
|
+
return `storage_${input.ownership.claimFingerprint.slice("sha256:".length)}`;
|
|
29
30
|
}
|
|
30
31
|
function installationBase(input, workspaceId, migrationId, createdAt) {
|
|
31
32
|
const layout = storageLayout({
|
|
@@ -102,6 +103,7 @@ export function createStorageMigrationRuntimeController(rawInput, injected = {})
|
|
|
102
103
|
...injected,
|
|
103
104
|
};
|
|
104
105
|
let workspaceId = null;
|
|
106
|
+
let machineId = null;
|
|
105
107
|
let identity;
|
|
106
108
|
let gate = null;
|
|
107
109
|
let sendStatus = null;
|
|
@@ -194,6 +196,10 @@ export function createStorageMigrationRuntimeController(rawInput, injected = {})
|
|
|
194
196
|
targetWorkspaceRoot: target.workspaceRoot,
|
|
195
197
|
backupsRoot: input.local.backupsRoot,
|
|
196
198
|
backupArtifacts: input.backupArtifacts.map((artifact) => ({ ...artifact })),
|
|
199
|
+
ownership: {
|
|
200
|
+
ownerEpoch: input.ownership.ownerEpoch,
|
|
201
|
+
ownershipClaimFingerprint: input.ownership.claimFingerprint,
|
|
202
|
+
},
|
|
197
203
|
acceptedWarnings: [...input.policy.acceptedWarnings],
|
|
198
204
|
},
|
|
199
205
|
service: {
|
|
@@ -229,6 +235,10 @@ export function createStorageMigrationRuntimeController(rawInput, injected = {})
|
|
|
229
235
|
profile: input.profile,
|
|
230
236
|
sourceStorageFingerprint,
|
|
231
237
|
targetStorageFingerprint: identity.storageIdFingerprint,
|
|
238
|
+
ownership: {
|
|
239
|
+
ownerEpoch: input.ownership.ownerEpoch,
|
|
240
|
+
ownershipClaimFingerprint: input.ownership.claimFingerprint,
|
|
241
|
+
},
|
|
232
242
|
runDoctor: async (migrationId) => dependencies.runDoctor(await doctorInput(migrationId)),
|
|
233
243
|
runMigration: async (doctor) => {
|
|
234
244
|
await gate.closeAndFlush();
|
|
@@ -264,6 +274,7 @@ export function createStorageMigrationRuntimeController(rawInput, injected = {})
|
|
|
264
274
|
environment: operation.environment,
|
|
265
275
|
workspaceId: operation.workspaceId,
|
|
266
276
|
profile: operation.profile,
|
|
277
|
+
...(operation.migration.ownership === undefined ? {} : operation.migration.ownership),
|
|
267
278
|
}, sendStatus);
|
|
268
279
|
if (outcome.reopenAdmission)
|
|
269
280
|
gate.reopen();
|
|
@@ -285,20 +296,31 @@ export function createStorageMigrationRuntimeController(rawInput, injected = {})
|
|
|
285
296
|
};
|
|
286
297
|
const bindServerIdentity = (server) => {
|
|
287
298
|
const nextWorkspaceId = UUID.parse(server.workspaceId).toLowerCase();
|
|
299
|
+
const nextMachineId = UUID.parse(server.machineId).toLowerCase();
|
|
288
300
|
if (workspaceId !== null && workspaceId !== nextWorkspaceId) {
|
|
289
301
|
throw new Error("Storage migration server workspace identity changed");
|
|
290
302
|
}
|
|
291
|
-
|
|
303
|
+
if (machineId !== null && machineId !== nextMachineId) {
|
|
304
|
+
throw new Error("Storage migration server machine identity changed");
|
|
305
|
+
}
|
|
306
|
+
if (input.ownership.workspaceId !== nextWorkspaceId
|
|
307
|
+
|| input.ownership.machineId !== nextMachineId
|
|
308
|
+
|| input.ownership.environment !== input.environment) {
|
|
309
|
+
throw new Error("Storage ownership claim does not match authenticated server identity");
|
|
310
|
+
}
|
|
292
311
|
const createdAt = dependencies.now().toISOString();
|
|
293
|
-
const installation = installationBase(input,
|
|
312
|
+
const installation = installationBase(input, nextWorkspaceId, undefined, createdAt);
|
|
294
313
|
const layout = storageLayout({
|
|
295
314
|
nowworkHome: input.nowworkHome,
|
|
296
315
|
environment: input.environment,
|
|
297
|
-
workspaceId,
|
|
316
|
+
workspaceId: nextWorkspaceId,
|
|
298
317
|
runtimeBase: input.local.runtimeBase,
|
|
299
318
|
platform: input.platform,
|
|
300
319
|
});
|
|
301
|
-
|
|
320
|
+
const nextIdentity = storageHelloIdentity({ installation, layout, ownership: input.ownership });
|
|
321
|
+
workspaceId = nextWorkspaceId;
|
|
322
|
+
machineId = nextMachineId;
|
|
323
|
+
identity = nextIdentity;
|
|
302
324
|
ensureControl();
|
|
303
325
|
drainPendingControlMessages();
|
|
304
326
|
};
|
|
@@ -5,10 +5,11 @@ import { buildServiceSpec } from "./computer-service.js";
|
|
|
5
5
|
import { profilePath } from "./computer-profile.js";
|
|
6
6
|
import { assertManagedServiceIdentity } from "./managed-service-lifecycle.js";
|
|
7
7
|
import { managedServiceRegistryPath, readManagedServiceRegistry, } from "./managed-service-registry.js";
|
|
8
|
-
import { resolveLocalStorageRoots } from "./storage-config.js";
|
|
8
|
+
import { installationsPath, resolveLocalStorageRoots } from "./storage-config.js";
|
|
9
9
|
import { readStorageMigrationPolicy } from "./storage-migration-policy.js";
|
|
10
10
|
import { createStorageMigrationRuntimeController, } from "./storage-runtime-controller.js";
|
|
11
11
|
import { resolveEnvTag, resolveNowworkHome } from "./storage-layout.js";
|
|
12
|
+
import { readStorageOwnershipClaim } from "./storage-ownership.js";
|
|
12
13
|
const notReady = (reason) => Object.freeze({ ready: false, reason });
|
|
13
14
|
function canonicalPath(value) {
|
|
14
15
|
const absolute = resolve(value);
|
|
@@ -106,6 +107,7 @@ export async function probeStorageMigrationRuntime(rawInput, injected = {}) {
|
|
|
106
107
|
assertManagedServiceIdentity,
|
|
107
108
|
readPolicy: readStorageMigrationPolicy,
|
|
108
109
|
createController: createStorageMigrationRuntimeController,
|
|
110
|
+
readOwnership: readStorageOwnershipClaim,
|
|
109
111
|
...injected,
|
|
110
112
|
};
|
|
111
113
|
let sourceRecord;
|
|
@@ -153,6 +155,17 @@ export async function probeStorageMigrationRuntime(rawInput, injected = {}) {
|
|
|
153
155
|
catch {
|
|
154
156
|
return notReady("migration_policy_invalid");
|
|
155
157
|
}
|
|
158
|
+
let ownership;
|
|
159
|
+
try {
|
|
160
|
+
ownership = await dependencies.readOwnership(nowworkHome);
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
return notReady(error.code === "ENOENT"
|
|
164
|
+
? "storage_ownership_claim_missing"
|
|
165
|
+
: "storage_ownership_claim_invalid");
|
|
166
|
+
}
|
|
167
|
+
if (ownership.environment !== environment)
|
|
168
|
+
return notReady("storage_ownership_claim_invalid");
|
|
156
169
|
const local = resolveLocalStorageRoots({
|
|
157
170
|
platform: input.platform,
|
|
158
171
|
userHome: input.userHome,
|
|
@@ -169,6 +182,7 @@ export async function probeStorageMigrationRuntime(rawInput, injected = {}) {
|
|
|
169
182
|
sourceRecord,
|
|
170
183
|
local,
|
|
171
184
|
policy,
|
|
185
|
+
ownership,
|
|
172
186
|
serviceEnvironment: {
|
|
173
187
|
NOWWORK_HOME: nowworkHome,
|
|
174
188
|
NOWWORK_ENV_TAG: environment,
|
|
@@ -178,6 +192,7 @@ export async function probeStorageMigrationRuntime(rawInput, injected = {}) {
|
|
|
178
192
|
{ name: "profile.json", path: profilePath(input.profile, input.currentDaemonHome) },
|
|
179
193
|
{ name: "service-descriptor", path: sourceRecord.descriptorPath },
|
|
180
194
|
{ name: "managed-service-registry.json", path: managedServiceRegistryPath(input.userHome) },
|
|
195
|
+
{ name: "installations.json", path: installationsPath(local.configRoot) },
|
|
181
196
|
],
|
|
182
197
|
}, { assertSourceIdentity });
|
|
183
198
|
return Object.freeze({ ready: true, runtime });
|
package/dist/storage-runtime.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { findInstallationForProfile, readInstallations, resolveLocalStorageRoots, } from "./storage-config.js";
|
|
2
2
|
import { storageHelloIdentity } from "./storage-identity.js";
|
|
3
3
|
import { storageLayout } from "./storage-layout.js";
|
|
4
|
+
import { readStorageOwnershipClaim } from "./storage-ownership.js";
|
|
4
5
|
const canonicalServerUrl = (value) => value.replace(/\/+$/u, "");
|
|
5
6
|
export function resolveStorageRuntime(input) {
|
|
6
7
|
const installation = findInstallationForProfile(input.records, input.profile);
|
|
@@ -24,7 +25,11 @@ export function resolveStorageRuntime(input) {
|
|
|
24
25
|
return Object.freeze({
|
|
25
26
|
installation,
|
|
26
27
|
layout,
|
|
27
|
-
identity: storageHelloIdentity({
|
|
28
|
+
identity: storageHelloIdentity({
|
|
29
|
+
installation,
|
|
30
|
+
layout,
|
|
31
|
+
...(input.ownership === undefined ? {} : { ownership: input.ownership }),
|
|
32
|
+
}),
|
|
28
33
|
local,
|
|
29
34
|
});
|
|
30
35
|
}
|
|
@@ -35,5 +40,16 @@ export async function loadStorageRuntime(input) {
|
|
|
35
40
|
environment: input.environment,
|
|
36
41
|
});
|
|
37
42
|
const records = await readInstallations({ configRoot: local.configRoot });
|
|
38
|
-
|
|
43
|
+
const baseline = resolveStorageRuntime({ ...input, records });
|
|
44
|
+
if (baseline === null)
|
|
45
|
+
return null;
|
|
46
|
+
let ownership;
|
|
47
|
+
try {
|
|
48
|
+
ownership = await readStorageOwnershipClaim(baseline.layout.persistentRoot);
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (error.code !== "ENOENT")
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
return ownership === undefined ? baseline : resolveStorageRuntime({ ...input, records, ownership });
|
|
39
55
|
}
|
|
@@ -18,6 +18,8 @@ const DeliveredFrameSchema = z.object({
|
|
|
18
18
|
environment: z.string().regex(/^[a-z0-9][a-z0-9_-]{0,31}$/u),
|
|
19
19
|
workspaceId: z.string().uuid(),
|
|
20
20
|
profile: z.string().regex(/^[a-z0-9][a-z0-9_-]{0,47}$/u),
|
|
21
|
+
ownerEpoch: z.number().int().positive().safe().optional(),
|
|
22
|
+
ownershipClaimFingerprint: z.string().regex(/^sha256:[0-9a-f]{64}$/u).optional(),
|
|
21
23
|
sourceStorageFingerprint: z.string().optional(),
|
|
22
24
|
targetStorageFingerprint: z.string().optional(),
|
|
23
25
|
manifestDigest: z.string().optional(),
|
|
@@ -45,6 +47,8 @@ export function storageStatusEventId(frame) {
|
|
|
45
47
|
frame.environment,
|
|
46
48
|
frame.workspaceId,
|
|
47
49
|
frame.profile,
|
|
50
|
+
frame.ownerEpoch ?? null,
|
|
51
|
+
frame.ownershipClaimFingerprint ?? null,
|
|
48
52
|
frame.sourceStorageFingerprint ?? null,
|
|
49
53
|
frame.targetStorageFingerprint ?? null,
|
|
50
54
|
frame.manifestDigest ?? null,
|
|
@@ -35,7 +35,10 @@ export function storageMigrationStatusReplay(receipt, expected) {
|
|
|
35
35
|
if (expected !== undefined && (receipt.migrationId !== expected.migrationId
|
|
36
36
|
|| receipt.environment !== expected.environment
|
|
37
37
|
|| receipt.workspaceId !== expected.workspaceId
|
|
38
|
-
|| receipt.profile !== expected.profile
|
|
38
|
+
|| receipt.profile !== expected.profile
|
|
39
|
+
|| (expected.ownerEpoch !== undefined && receipt.ownerEpoch !== expected.ownerEpoch)
|
|
40
|
+
|| (expected.ownershipClaimFingerprint !== undefined
|
|
41
|
+
&& receipt.ownershipClaimFingerprint !== expected.ownershipClaimFingerprint))) {
|
|
39
42
|
throw new Error("storage migration receipt identity mismatch");
|
|
40
43
|
}
|
|
41
44
|
return Object.freeze(storageMigrationReplayStatuses(receipt).map((status) => Object.freeze({
|
|
@@ -45,6 +48,10 @@ export function storageMigrationStatusReplay(receipt, expected) {
|
|
|
45
48
|
environment: receipt.environment,
|
|
46
49
|
workspaceId: receipt.workspaceId,
|
|
47
50
|
profile: receipt.profile,
|
|
51
|
+
...(receipt.ownerEpoch === undefined ? {} : { ownerEpoch: receipt.ownerEpoch }),
|
|
52
|
+
...(receipt.ownershipClaimFingerprint === undefined ? {} : {
|
|
53
|
+
ownershipClaimFingerprint: receipt.ownershipClaimFingerprint,
|
|
54
|
+
}),
|
|
48
55
|
...(status === "verified" ? { finalManifestDigest: receipt.manifestDigest } : {}),
|
|
49
56
|
...(status === "failed"
|
|
50
57
|
? {
|
|
@@ -98,6 +105,10 @@ export async function monitorStorageMigrationStatus(input, send, injected = {})
|
|
|
98
105
|
environment: input.environment,
|
|
99
106
|
workspaceId: input.workspaceId,
|
|
100
107
|
profile: input.profile,
|
|
108
|
+
...(input.ownerEpoch === undefined ? {} : { ownerEpoch: input.ownerEpoch }),
|
|
109
|
+
...(input.ownershipClaimFingerprint === undefined ? {} : {
|
|
110
|
+
ownershipClaimFingerprint: input.ownershipClaimFingerprint,
|
|
111
|
+
}),
|
|
101
112
|
errorCode: workerFailure.errorCode,
|
|
102
113
|
errorMessage: workerFailure.errorMessage,
|
|
103
114
|
...(receipt?.sourceStopAttempted === false || receipt === null ? { reopenAdmission: true } : {}),
|
|
@@ -114,6 +125,10 @@ export async function monitorStorageMigrationStatus(input, send, injected = {})
|
|
|
114
125
|
environment: input.environment,
|
|
115
126
|
workspaceId: input.workspaceId,
|
|
116
127
|
profile: input.profile,
|
|
128
|
+
...(input.ownerEpoch === undefined ? {} : { ownerEpoch: input.ownerEpoch }),
|
|
129
|
+
...(input.ownershipClaimFingerprint === undefined ? {} : {
|
|
130
|
+
ownershipClaimFingerprint: input.ownershipClaimFingerprint,
|
|
131
|
+
}),
|
|
117
132
|
errorCode: "worker_heartbeat_stale_recovery_required",
|
|
118
133
|
errorMessage: "storage migration worker heartbeat expired; explicit recovery is required",
|
|
119
134
|
...(receipt?.sourceStopAttempted === false || receipt === null ? { reopenAdmission: true } : {}),
|
|
@@ -367,6 +367,7 @@ export async function executeStorageMigrationWorker(handoffPath, injected = {})
|
|
|
367
367
|
targetWorkspaceRoot: operation.migration.targetWorkspaceRoot,
|
|
368
368
|
backupsRoot: operation.migration.backupsRoot,
|
|
369
369
|
backupArtifacts: operation.migration.backupArtifacts,
|
|
370
|
+
...(operation.migration.ownership === undefined ? {} : { ownership: operation.migration.ownership }),
|
|
370
371
|
acceptedWarnings: operation.migration.acceptedWarnings,
|
|
371
372
|
onStatus: () => assertHeartbeatHealthy(),
|
|
372
373
|
assertHealthy: assertHeartbeatHealthy,
|
|
@@ -118,6 +118,10 @@ const OperationSchema = z.object({
|
|
|
118
118
|
name: z.string().regex(/^[a-z0-9][a-z0-9_.-]{0,127}$/u),
|
|
119
119
|
path: AbsolutePathSchema,
|
|
120
120
|
}).strict()).max(32),
|
|
121
|
+
ownership: z.object({
|
|
122
|
+
ownerEpoch: z.number().int().positive().safe(),
|
|
123
|
+
ownershipClaimFingerprint: z.string().regex(/^sha256:[0-9a-f]{64}$/u),
|
|
124
|
+
}).strict().optional(),
|
|
121
125
|
acceptedWarnings: z.array(StorageMigrationWarningCodeSchema)
|
|
122
126
|
.max(StorageMigrationWarningCodeSchema.options.length),
|
|
123
127
|
}).strict(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nowcrew/daemon",
|
|
3
|
-
"version": "0.6.77-beta.
|
|
3
|
+
"version": "0.6.77-beta.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -16,6 +16,14 @@
|
|
|
16
16
|
"publishConfig": {
|
|
17
17
|
"access": "public"
|
|
18
18
|
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"daemon": "pnpm --filter @nowcrew/cli build && tsx src/main.ts",
|
|
21
|
+
"build": "tsc -p tsconfig.json",
|
|
22
|
+
"codex-home:migrate": "tsx src/runtimes/codex-home-migration-cli.ts",
|
|
23
|
+
"prepublishOnly": "pnpm build && node ../scripts/daemon-release-artifact.mjs --strict-registry",
|
|
24
|
+
"test": "vitest run",
|
|
25
|
+
"typecheck": "tsc --noEmit"
|
|
26
|
+
},
|
|
19
27
|
"dependencies": {
|
|
20
28
|
"@agentclientprotocol/sdk": "1.2.1",
|
|
21
29
|
"@nowcrew/cli": "0.4.13",
|
|
@@ -36,12 +44,5 @@
|
|
|
36
44
|
"tsx": "^4.19.0",
|
|
37
45
|
"typescript": "^5.6.0",
|
|
38
46
|
"vitest": "^2.1.0"
|
|
39
|
-
},
|
|
40
|
-
"scripts": {
|
|
41
|
-
"daemon": "pnpm --filter @nowcrew/cli build && tsx src/main.ts",
|
|
42
|
-
"build": "tsc -p tsconfig.json",
|
|
43
|
-
"codex-home:migrate": "tsx src/runtimes/codex-home-migration-cli.ts",
|
|
44
|
-
"test": "vitest run",
|
|
45
|
-
"typecheck": "tsc --noEmit"
|
|
46
47
|
}
|
|
47
|
-
}
|
|
48
|
+
}
|