@nowcrew/daemon 0.6.74 → 0.6.75
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 +22 -11
- package/dist/automatic-daemon-installer.js +19 -2
- package/dist/automatic-install-target.js +31 -1
- package/dist/computer-cli.js +59 -7
- package/dist/computer-profile.js +40 -4
- package/dist/computer-service.js +9 -4
- package/dist/daemon-migration-controller.js +3 -0
- package/dist/daemon-release-version.js +22 -0
- package/dist/daemon-update-controller.js +3 -2
- package/dist/daemon-updater.js +62 -2
- package/dist/i18n.js +3 -1
- package/dist/machine-info.js +10 -0
- package/dist/main.js +12 -3
- package/dist/npm-registry.js +32 -0
- package/dist/release-channel-guard.js +60 -0
- package/dist/release-provenance.js +87 -0
- package/dist/serve.js +7 -7
- package/dist/slog.js +4 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -11,6 +11,13 @@ For a temporary foreground connection, run:
|
|
|
11
11
|
npx @nowcrew/daemon@latest --server-url https://nowwork.example --api-key sk_machine_...
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
+
The package has two release channels. The `in` environment consumes stable `x.y.z` releases through
|
|
15
|
+
the `latest` tag; the `dev` environment consumes `x.y.z-beta.n` prereleases through the `beta` tag.
|
|
16
|
+
The parser also accepts the historical `x.y.z-beta-n` spelling and normalizes it for ordering; new
|
|
17
|
+
published versions should use the dotted SemVer form.
|
|
18
|
+
Never use `latest` for a dev installation or `beta` for an in installation. See the
|
|
19
|
+
[Daemon Release Runbook](../docs/releases/daemon-release-runbook.md) for release and rollback procedures.
|
|
20
|
+
|
|
14
21
|
This compatibility form stays resident in the current terminal and reconnects with backoff. New
|
|
15
22
|
**Add Computer** commands use the managed automatic installation described below. For local development, run
|
|
16
23
|
`pnpm --filter @nowcrew/daemon daemon`. `crew-daemon run --agent <handle> --channel <id>` is the legacy
|
|
@@ -22,7 +29,7 @@ Persist a named connection without putting its machine token in shell history or
|
|
|
22
29
|
|
|
23
30
|
```bash
|
|
24
31
|
printf '%s' "$CREW_MACHINE_TOKEN" | crew-daemon profile save work \
|
|
25
|
-
--server-url https://nowwork.example --token-stdin
|
|
32
|
+
--server-url https://nowwork.example --release-tag latest --token-stdin
|
|
26
33
|
crew-daemon profile list
|
|
27
34
|
crew-daemon profile show work # reports configured=true; never prints the token
|
|
28
35
|
crew-daemon serve --profile work
|
|
@@ -36,8 +43,9 @@ The connection command generated by **Add Computer** installs the package into a
|
|
|
36
43
|
saves the private profile, installs the native user service, starts it, and verifies that it is running:
|
|
37
44
|
|
|
38
45
|
```bash
|
|
39
|
-
npx -y --registry https://registry.npmjs.org @nowcrew/daemon@
|
|
40
|
-
--server-url https://nowwork-dev.nowcoder.com --api-key sk_machine_...
|
|
46
|
+
npx -y --registry https://registry.npmjs.org @nowcrew/daemon@beta install \
|
|
47
|
+
--server-url https://nowwork-dev.nowcoder.com --api-key sk_machine_... \
|
|
48
|
+
--release-tag beta --registry https://registry.npmjs.org
|
|
41
49
|
```
|
|
42
50
|
|
|
43
51
|
The installer selects the fixed `in` layout for `nowwork-in.nowcoder.com` and the fixed `dev` layout for
|
|
@@ -45,8 +53,8 @@ The installer selects the fixed `in` layout for `nowwork-in.nowcoder.com` and th
|
|
|
45
53
|
An unknown hostname is rejected before any write and reports how to retry with an explicit custom profile:
|
|
46
54
|
|
|
47
55
|
```bash
|
|
48
|
-
npx -y --registry https://registry.npmjs.org @nowcrew/daemon@
|
|
49
|
-
--server-url https://lab.example.com --api-key sk_machine_... --profile lab
|
|
56
|
+
npx -y --registry https://registry.npmjs.org @nowcrew/daemon@beta install \
|
|
57
|
+
--server-url https://lab.example.com --api-key sk_machine_... --profile lab --release-tag beta
|
|
50
58
|
```
|
|
51
59
|
|
|
52
60
|
Custom profiles use `~/.crew/daemon-<profile>`, `~/.crew/agents-<profile>`, and
|
|
@@ -95,13 +103,16 @@ Upgrade the global daemon package, optionally restarting one installed profile a
|
|
|
95
103
|
```bash
|
|
96
104
|
crew-daemon upgrade
|
|
97
105
|
crew-daemon upgrade --profile work
|
|
106
|
+
crew-daemon upgrade --profile work --release-tag beta
|
|
98
107
|
```
|
|
99
108
|
|
|
100
109
|
With `--profile`, upgrade validates isolation, stops the native service, acquires the installation lease,
|
|
101
110
|
replaces only the entrypoint-derived npm prefix, releases the lease, and starts the service. If package
|
|
102
111
|
replacement fails, it still attempts to start the previous installation. Without `--profile`, upgrade
|
|
103
|
-
refuses to replace a prefix currently leased by a running daemon;
|
|
104
|
-
|
|
112
|
+
refuses to replace a prefix currently leased by a running daemon; it also permits only the currently
|
|
113
|
+
installed daemon's release channel. Cross-channel upgrades (for example installing `beta` from a stable
|
|
114
|
+
global package) require `--profile`, so the target server environment and profile isolation are verified.
|
|
115
|
+
Use the profile form for any managed service.
|
|
105
116
|
|
|
106
117
|
## Managed Self-Update
|
|
107
118
|
|
|
@@ -171,7 +182,7 @@ daemon home also contributes a stable hash to the native service ID, so equal pr
|
|
|
171
182
|
|
|
172
183
|
Before replacing files, the daemon refuses new work, requires zero local active or queued work, and
|
|
173
184
|
non-blockingly acquires every host execution slot. It then installs only the server-selected exact
|
|
174
|
-
`x.y.z` version with npm using the prefix derived from the running global entrypoint, verifies the installed
|
|
185
|
+
exact `x.y.z` or `x.y.z-beta.n` version with npm using the prefix derived from the running global entrypoint, verifies the installed
|
|
175
186
|
package version, and schedules the existing native service restart from a detached helper. macOS uses
|
|
176
187
|
`launchctl kickstart -k` to avoid the `bootout`/`bootstrap` race; Linux uses `systemctl --user --no-block
|
|
177
188
|
restart`. The helper reports manager failure over IPC. A failed handoff releases both execution barriers and
|
|
@@ -195,11 +206,11 @@ Failures are recoverable and keep a bounded code in the machine record:
|
|
|
195
206
|
| `update_in_progress` | wait for the active request to reach a terminal state |
|
|
196
207
|
| `update_timeout` | inspect `crew-daemon status --profile work`, restart if needed, then retry |
|
|
197
208
|
|
|
198
|
-
|
|
199
|
-
the service manually:
|
|
209
|
+
The Web UI supports owner/admin rollback to an exact release in the configured channel. When the managed
|
|
210
|
+
control plane is unavailable, install a known exact release and restart the service manually:
|
|
200
211
|
|
|
201
212
|
```bash
|
|
202
|
-
npm install --global --prefix <environment-prefix> --ignore-scripts --no-audit --no-fund @nowcrew/daemon@<previous-
|
|
213
|
+
npm install --global --prefix <environment-prefix> --ignore-scripts --no-audit --no-fund @nowcrew/daemon@<previous-release>
|
|
203
214
|
crew-daemon restart --profile work
|
|
204
215
|
crew-daemon status --profile work
|
|
205
216
|
```
|
|
@@ -8,6 +8,9 @@ import { ManagedServiceConflictError, managedServiceConflict, managedServiceIden
|
|
|
8
8
|
import { resolveAutomaticInstallTarget, } from "./automatic-install-target.js";
|
|
9
9
|
import { acquireDaemonInstallationLease, } from "./daemon-installation-lease.js";
|
|
10
10
|
import { daemonVersion } from "./machine-info.js";
|
|
11
|
+
import { daemonReleaseProvenance } from "./release-provenance.js";
|
|
12
|
+
import { daemonReleaseChannel, isDaemonReleaseVersion } from "./daemon-release-version.js";
|
|
13
|
+
import { normalizeDaemonNpmRegistry } from "./npm-registry.js";
|
|
11
14
|
import { discoverExactLegacyService, } from "./automatic-service-migration.js";
|
|
12
15
|
function profileStorage(platform, userHome, runner) {
|
|
13
16
|
return {
|
|
@@ -175,7 +178,7 @@ export function stableDaemonEntry(target, platform) {
|
|
|
175
178
|
? paths.join(target.npmPrefix, "node_modules", "@nowcrew", "daemon", "dist", "main.js")
|
|
176
179
|
: paths.join(target.npmPrefix, "lib", "node_modules", "@nowcrew", "daemon", "dist", "main.js");
|
|
177
180
|
}
|
|
178
|
-
export async function installExactDaemonPackage(npmPrefix, version, platform, runner) {
|
|
181
|
+
export async function installExactDaemonPackage(npmPrefix, version, platform, runner, registryUrl) {
|
|
179
182
|
const command = platform === "win32" ? "npm.cmd" : "npm";
|
|
180
183
|
const result = await runner(command, [
|
|
181
184
|
"install",
|
|
@@ -185,6 +188,9 @@ export async function installExactDaemonPackage(npmPrefix, version, platform, ru
|
|
|
185
188
|
"--ignore-scripts",
|
|
186
189
|
"--no-audit",
|
|
187
190
|
"--no-fund",
|
|
191
|
+
...(registryUrl === undefined ? [] : ["--registry", normalizeDaemonNpmRegistry(registryUrl, {
|
|
192
|
+
allowLoopbackHttp: daemonReleaseChannel(version) === "beta",
|
|
193
|
+
})]),
|
|
188
194
|
`@nowcrew/daemon@${version}`,
|
|
189
195
|
]);
|
|
190
196
|
if (result.exitCode !== 0) {
|
|
@@ -217,9 +223,18 @@ export async function installDaemonAutomatically(input, dependencies = defaultDe
|
|
|
217
223
|
const target = resolveAutomaticInstallTarget({
|
|
218
224
|
serverUrl: input.serverUrl,
|
|
219
225
|
...(input.requestedProfile === undefined ? {} : { requestedProfile: input.requestedProfile }),
|
|
226
|
+
...(input.requestedReleaseTag === undefined ? {} : { requestedReleaseTag: input.requestedReleaseTag }),
|
|
227
|
+
...(input.requestedRegistryUrl === undefined ? {} : { requestedRegistryUrl: input.requestedRegistryUrl }),
|
|
220
228
|
userHome: dependencies.userHome,
|
|
221
229
|
platform: dependencies.platform,
|
|
222
230
|
});
|
|
231
|
+
if (!isDaemonReleaseVersion(dependencies.version)) {
|
|
232
|
+
throw new Error(`Invoking daemon package has an invalid release version '${dependencies.version}'`);
|
|
233
|
+
}
|
|
234
|
+
const invokingTag = daemonReleaseProvenance(dependencies.version).releaseTag;
|
|
235
|
+
if (invokingTag !== target.releaseTag) {
|
|
236
|
+
throw new Error(`Invoking daemon release tag '${invokingTag}' does not match target '${target.releaseTag}'`);
|
|
237
|
+
}
|
|
223
238
|
const spec = buildServiceSpec({
|
|
224
239
|
platform: dependencies.platform,
|
|
225
240
|
profile: target.profile,
|
|
@@ -251,7 +266,7 @@ export async function installDaemonAutomatically(input, dependencies = defaultDe
|
|
|
251
266
|
const lease = await dependencies.acquireInstallationLease(target.npmPrefix);
|
|
252
267
|
let installFailure;
|
|
253
268
|
try {
|
|
254
|
-
await dependencies.installPackage(target.npmPrefix, dependencies.version, dependencies.platform, dependencies.runner);
|
|
269
|
+
await dependencies.installPackage(target.npmPrefix, dependencies.version, dependencies.platform, dependencies.runner, target.registryUrl);
|
|
255
270
|
}
|
|
256
271
|
catch (error) {
|
|
257
272
|
installFailure = error;
|
|
@@ -280,6 +295,8 @@ export async function installDaemonAutomatically(input, dependencies = defaultDe
|
|
|
280
295
|
serverUrl: target.serverUrl,
|
|
281
296
|
machineToken,
|
|
282
297
|
agentsRoot: target.agentsRoot,
|
|
298
|
+
releaseTag: target.releaseTag,
|
|
299
|
+
...(target.registryUrl === undefined ? {} : { registryUrl: target.registryUrl }),
|
|
283
300
|
...(dependencies.runtimePath === undefined ? {} : { runtimePath: dependencies.runtimePath }),
|
|
284
301
|
}, target.daemonHome);
|
|
285
302
|
serviceReplacementStarted = true;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { posix, win32 } from "node:path";
|
|
2
2
|
import { profileLayout, validateLocalInstanceName } from "./profile-layout.js";
|
|
3
|
+
import { normalizeDaemonNpmRegistry } from "./npm-registry.js";
|
|
3
4
|
const PROFILE_NAME = /^[a-z0-9][a-z0-9_-]{0,47}$/;
|
|
4
5
|
const KNOWN_TARGETS = {
|
|
5
6
|
"nowwork-in.nowcoder.com": {
|
|
@@ -13,6 +14,23 @@ const KNOWN_TARGETS = {
|
|
|
13
14
|
agentsDirectory: "agents-dev",
|
|
14
15
|
},
|
|
15
16
|
};
|
|
17
|
+
export function releaseTagForServerUrl(serverUrl) {
|
|
18
|
+
const parsed = parseServerUrl(serverUrl);
|
|
19
|
+
const known = KNOWN_TARGETS[parsed.hostname.toLowerCase()];
|
|
20
|
+
return known?.environment === "dev" ? "beta" : "latest";
|
|
21
|
+
}
|
|
22
|
+
export function assertReleaseTagForServerUrl(serverUrl, requested) {
|
|
23
|
+
if (requested === undefined)
|
|
24
|
+
return;
|
|
25
|
+
const parsed = parseServerUrl(serverUrl);
|
|
26
|
+
const known = KNOWN_TARGETS[parsed.hostname.toLowerCase()];
|
|
27
|
+
if (known === undefined)
|
|
28
|
+
return;
|
|
29
|
+
const expected = known.environment === "dev" ? "beta" : "latest";
|
|
30
|
+
if (requested !== expected) {
|
|
31
|
+
throw new Error(`Server hostname '${parsed.hostname}' requires release tag '${expected}'`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
16
34
|
function parseServerUrl(value) {
|
|
17
35
|
let parsed;
|
|
18
36
|
try {
|
|
@@ -52,6 +70,7 @@ function knownTargetForProfile(environment, requestedProfile, input, parsed) {
|
|
|
52
70
|
// Keep the no-flag target readable by old daemons; any workspace-scoped profile below
|
|
53
71
|
// uses the canonical environment token (production, never in).
|
|
54
72
|
const legacyProfile = environment === "production" ? "in" : environment;
|
|
73
|
+
assertReleaseTagForServerUrl(input.serverUrl, input.requestedReleaseTag);
|
|
55
74
|
if (requestedProfile === undefined || requestedProfile === legacyProfile) {
|
|
56
75
|
return Object.freeze({
|
|
57
76
|
kind: "known",
|
|
@@ -60,6 +79,7 @@ function knownTargetForProfile(environment, requestedProfile, input, parsed) {
|
|
|
60
79
|
daemonHome: paths.join(input.userHome, ".crew", environment === "production" ? "daemon" : "daemon-dev"),
|
|
61
80
|
agentsRoot: paths.join(input.userHome, ".crew", environment === "production" ? "agents" : "agents-dev"),
|
|
62
81
|
npmPrefix: paths.join(input.userHome, ".crew", "daemon-runtimes", legacyProfile),
|
|
82
|
+
releaseTag: environment === "dev" ? "beta" : "latest",
|
|
63
83
|
});
|
|
64
84
|
}
|
|
65
85
|
if (requestedProfile === "in" || requestedProfile === "dev") {
|
|
@@ -88,6 +108,7 @@ function knownTargetForProfile(environment, requestedProfile, input, parsed) {
|
|
|
88
108
|
daemonHome: layout.daemonHome,
|
|
89
109
|
agentsRoot: layout.agentsRoot,
|
|
90
110
|
npmPrefix: layout.npmPrefix,
|
|
111
|
+
releaseTag: environment === "dev" ? "beta" : "latest",
|
|
91
112
|
});
|
|
92
113
|
}
|
|
93
114
|
export function resolveAutomaticInstallTarget(input) {
|
|
@@ -95,8 +116,15 @@ export function resolveAutomaticInstallTarget(input) {
|
|
|
95
116
|
const hostname = parsed.hostname.toLowerCase();
|
|
96
117
|
const paths = input.platform === "win32" ? win32 : posix;
|
|
97
118
|
const known = KNOWN_TARGETS[hostname];
|
|
119
|
+
const targetReleaseTag = known?.environment === "dev"
|
|
120
|
+
? "beta"
|
|
121
|
+
: input.requestedReleaseTag ?? "latest";
|
|
122
|
+
const registryUrl = input.requestedRegistryUrl === undefined
|
|
123
|
+
? undefined
|
|
124
|
+
: normalizeDaemonNpmRegistry(input.requestedRegistryUrl, { allowLoopbackHttp: targetReleaseTag === "beta" });
|
|
98
125
|
if (known !== undefined) {
|
|
99
|
-
|
|
126
|
+
const target = knownTargetForProfile(known.environment, input.requestedProfile, input, parsed);
|
|
127
|
+
return registryUrl === undefined ? target : Object.freeze({ ...target, registryUrl });
|
|
100
128
|
}
|
|
101
129
|
const profile = requireCustomProfile(hostname, input.requestedProfile);
|
|
102
130
|
return Object.freeze({
|
|
@@ -106,5 +134,7 @@ export function resolveAutomaticInstallTarget(input) {
|
|
|
106
134
|
daemonHome: paths.join(input.userHome, ".crew", `daemon-${profile}`),
|
|
107
135
|
agentsRoot: paths.join(input.userHome, ".crew", `agents-${profile}`),
|
|
108
136
|
npmPrefix: paths.join(input.userHome, ".crew", "daemon-runtimes", profile),
|
|
137
|
+
releaseTag: input.requestedReleaseTag ?? "latest",
|
|
138
|
+
...(registryUrl === undefined ? {} : { registryUrl }),
|
|
109
139
|
});
|
|
110
140
|
}
|
package/dist/computer-cli.js
CHANGED
|
@@ -4,10 +4,14 @@ import { resolve } 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";
|
|
7
|
+
import { releaseTagForServerUrl, assertReleaseTagForServerUrl } from "./automatic-install-target.js";
|
|
8
|
+
import { normalizeDaemonNpmRegistry } from "./npm-registry.js";
|
|
7
9
|
import { defaultProcessController } from "./execution-journal.js";
|
|
8
10
|
import { inspectJournalLock } from "./execution-journal-lock.js";
|
|
9
11
|
import { detectDaemonLang, formatDaemonText } from "./i18n.js";
|
|
10
12
|
import { daemonGlobalInstallation } from "./daemon-installation.js";
|
|
13
|
+
import { daemonVersion } from "./machine-info.js";
|
|
14
|
+
import { daemonReleaseProvenance } from "./release-provenance.js";
|
|
11
15
|
import { acquireDaemonInstallationLease, } from "./daemon-installation-lease.js";
|
|
12
16
|
import { assertManagedServiceIdentity, buildManagedServiceRecord, installManagedService, uninstallUnregisteredManagedService, uninstallManagedService, } from "./managed-service-lifecycle.js";
|
|
13
17
|
import { inspectManagedServiceDiagnostics } from "./managed-service-diagnostics.js";
|
|
@@ -57,11 +61,11 @@ export function isGlobalDaemonEntry(entryPath, globalNodeModules) {
|
|
|
57
61
|
function usage(td) {
|
|
58
62
|
return [
|
|
59
63
|
td("Computer lifecycle:"),
|
|
60
|
-
" crew-daemon profile save <name> --server-url <url> [--agents-root <path>] [--token-stdin]",
|
|
64
|
+
" crew-daemon profile save <name> --server-url <url> [--release-tag <latest|beta>] [--registry <https-url>] [--agents-root <path>] [--token-stdin]",
|
|
61
65
|
" crew-daemon profile list|show|remove [<name>]",
|
|
62
|
-
" crew-daemon install --server-url <url> --api-key <sk_machine_*> [--profile <custom-name>]",
|
|
66
|
+
" crew-daemon install --server-url <url> --api-key <sk_machine_*> [--profile <custom-name>] [--release-tag <latest|beta>] [--registry <https-url>]",
|
|
63
67
|
" crew-daemon doctor|install|uninstall|start|stop|restart|status --profile <name>",
|
|
64
|
-
" crew-daemon upgrade [--profile <name>]",
|
|
68
|
+
" crew-daemon upgrade [--profile <name>] [--release-tag <latest|beta>] [--registry <https-url>]",
|
|
65
69
|
"",
|
|
66
70
|
td("profile save reads the machine token from CREW_MACHINE_TOKEN, or stdin with --token-stdin."),
|
|
67
71
|
].join("\n") + "\n";
|
|
@@ -105,6 +109,8 @@ export async function runComputerCommand(argv, overrides = {}) {
|
|
|
105
109
|
options: {
|
|
106
110
|
profile: { type: "string" },
|
|
107
111
|
"server-url": { type: "string" },
|
|
112
|
+
"release-tag": { type: "string" },
|
|
113
|
+
registry: { type: "string" },
|
|
108
114
|
"agents-root": { type: "string" },
|
|
109
115
|
"token-stdin": { type: "boolean", default: false },
|
|
110
116
|
"api-key": { type: "string" },
|
|
@@ -118,6 +124,14 @@ export async function runComputerCommand(argv, overrides = {}) {
|
|
|
118
124
|
}
|
|
119
125
|
const [, action, positionalName] = parsed.positionals;
|
|
120
126
|
const profileName = parsed.values.profile;
|
|
127
|
+
const requestedReleaseTag = parsed.values["release-tag"];
|
|
128
|
+
const requestedRegistryUrl = parsed.values.registry;
|
|
129
|
+
const configuredRegistryUrl = requestedRegistryUrl === undefined
|
|
130
|
+
? deps.env.CREW_NPM_REGISTRY?.trim() || undefined
|
|
131
|
+
: requestedRegistryUrl;
|
|
132
|
+
if (requestedReleaseTag !== undefined && requestedReleaseTag !== "latest" && requestedReleaseTag !== "beta") {
|
|
133
|
+
throw new Error(td("Invalid release tag; expected latest or beta"));
|
|
134
|
+
}
|
|
121
135
|
const automaticInstall = command === "install" && parsed.values["server-url"] !== undefined;
|
|
122
136
|
const tokenOption = parsed.values["api-key"] !== undefined
|
|
123
137
|
? "--api-key"
|
|
@@ -143,6 +157,10 @@ export async function runComputerCommand(argv, overrides = {}) {
|
|
|
143
157
|
serverUrl: parsed.values["server-url"],
|
|
144
158
|
machineToken,
|
|
145
159
|
...(profileName === undefined ? {} : { requestedProfile: profileName }),
|
|
160
|
+
...(requestedReleaseTag === undefined ? {} : { requestedReleaseTag }),
|
|
161
|
+
...(configuredRegistryUrl === undefined
|
|
162
|
+
? {}
|
|
163
|
+
: { requestedRegistryUrl: configuredRegistryUrl }),
|
|
146
164
|
});
|
|
147
165
|
deps.stdout(`${td("Installed daemon {{version}} for profile '{{profile}}'; service '{{service}}' is running.", { version: result.version, profile: result.profile, service: result.serviceId })}\n`);
|
|
148
166
|
return 0;
|
|
@@ -187,10 +205,19 @@ export async function runComputerCommand(argv, overrides = {}) {
|
|
|
187
205
|
const machineToken = (fromStdin ? await deps.readStdin() : deps.env.CREW_MACHINE_TOKEN)?.trim();
|
|
188
206
|
if (!machineToken)
|
|
189
207
|
throw new Error(td("Missing machine token; use CREW_MACHINE_TOKEN or --token-stdin"));
|
|
208
|
+
const serverUrl = requireValue(parsed.values["server-url"], "--server-url", td);
|
|
209
|
+
assertReleaseTagForServerUrl(serverUrl, requestedReleaseTag);
|
|
210
|
+
const profileReleaseTag = requestedReleaseTag ?? releaseTagForServerUrl(serverUrl);
|
|
190
211
|
const profile = {
|
|
191
212
|
name,
|
|
192
|
-
serverUrl
|
|
213
|
+
serverUrl,
|
|
193
214
|
machineToken,
|
|
215
|
+
releaseTag: profileReleaseTag,
|
|
216
|
+
...(configuredRegistryUrl === undefined ? {} : {
|
|
217
|
+
registryUrl: normalizeDaemonNpmRegistry(configuredRegistryUrl, {
|
|
218
|
+
allowLoopbackHttp: profileReleaseTag === "beta",
|
|
219
|
+
}),
|
|
220
|
+
}),
|
|
194
221
|
...(parsed.values["agents-root"]
|
|
195
222
|
? { agentsRoot: resolveAgentsRoot(parsed.values["agents-root"], deps.userHome, deps.platform) }
|
|
196
223
|
: {}),
|
|
@@ -205,6 +232,19 @@ export async function runComputerCommand(argv, overrides = {}) {
|
|
|
205
232
|
const restartProfile = profileName
|
|
206
233
|
? await loadProfile(profileName, home, profileStorage)
|
|
207
234
|
: null;
|
|
235
|
+
if (!restartProfile && requestedReleaseTag !== undefined) {
|
|
236
|
+
const installedReleaseTag = daemonReleaseProvenance(daemonVersion(), deps.env).releaseTag;
|
|
237
|
+
if (requestedReleaseTag !== installedReleaseTag) {
|
|
238
|
+
throw new Error(td("Cross-channel daemon upgrade requires --profile so the server environment can be verified"));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const releaseTag = requestedReleaseTag
|
|
242
|
+
?? restartProfile?.releaseTag
|
|
243
|
+
?? (restartProfile === null
|
|
244
|
+
? daemonReleaseProvenance(daemonVersion(), deps.env).releaseTag
|
|
245
|
+
: releaseTagForServerUrl(restartProfile.serverUrl));
|
|
246
|
+
if (restartProfile)
|
|
247
|
+
assertReleaseTagForServerUrl(restartProfile.serverUrl, releaseTag);
|
|
208
248
|
const spec = restartProfile
|
|
209
249
|
? buildServiceSpec({
|
|
210
250
|
...deps,
|
|
@@ -215,7 +255,7 @@ export async function runComputerCommand(argv, overrides = {}) {
|
|
|
215
255
|
: null;
|
|
216
256
|
if (spec && restartProfile) {
|
|
217
257
|
if (installation === null && !isGlobalDaemonEntry(deps.entryPath, await deps.resolveGlobalNodeModules())) {
|
|
218
|
-
throw new Error(td("Service lifecycle requires a global @nowcrew/daemon install; run npm install --global @nowcrew/daemon@
|
|
258
|
+
throw new Error(td("Service lifecycle requires a global @nowcrew/daemon install; run npm install --global @nowcrew/daemon@{{tag}}", { tag: releaseTag }));
|
|
219
259
|
}
|
|
220
260
|
const before = await serviceStatus(spec, deps.runner);
|
|
221
261
|
if (!before.installed)
|
|
@@ -236,7 +276,10 @@ export async function runComputerCommand(argv, overrides = {}) {
|
|
|
236
276
|
installationLease = installation === null
|
|
237
277
|
? null
|
|
238
278
|
: await deps.acquireInstallationLease(installation.npmPrefix, { userHome: deps.userHome });
|
|
239
|
-
await upgradeDaemon(deps.platform, deps.runner, installation?.npmPrefix
|
|
279
|
+
await upgradeDaemon(deps.platform, deps.runner, installation?.npmPrefix, releaseTag, configuredRegistryUrl === undefined
|
|
280
|
+
? restartProfile?.registryUrl
|
|
281
|
+
?? configuredRegistryUrl
|
|
282
|
+
: normalizeDaemonNpmRegistry(configuredRegistryUrl, { allowLoopbackHttp: releaseTag === "beta" }));
|
|
240
283
|
}
|
|
241
284
|
catch (error) {
|
|
242
285
|
upgradeError = error;
|
|
@@ -274,6 +317,14 @@ export async function runComputerCommand(argv, overrides = {}) {
|
|
|
274
317
|
}
|
|
275
318
|
const name = requireValue(profileName, "--profile", td);
|
|
276
319
|
const profile = await loadProfile(name, home, profileStorage);
|
|
320
|
+
const profileReleaseTag = requestedReleaseTag ?? profile.releaseTag ?? releaseTagForServerUrl(profile.serverUrl);
|
|
321
|
+
if (command === "install") {
|
|
322
|
+
assertReleaseTagForServerUrl(profile.serverUrl, profileReleaseTag);
|
|
323
|
+
const installedReleaseTag = daemonReleaseProvenance(daemonVersion(), deps.env).releaseTag;
|
|
324
|
+
if (installedReleaseTag !== profileReleaseTag) {
|
|
325
|
+
throw new Error(td("Installed daemon release tag '{{installed}}' does not match profile '{{profile}}' release tag '{{target}}'", { installed: installedReleaseTag, profile: name, target: profileReleaseTag }));
|
|
326
|
+
}
|
|
327
|
+
}
|
|
277
328
|
if (command === "install" || command === "start" || command === "restart") {
|
|
278
329
|
await assertProfileAgentsRootUnique(profile, home, deps.userHome, profileStorage);
|
|
279
330
|
}
|
|
@@ -296,7 +347,8 @@ export async function runComputerCommand(argv, overrides = {}) {
|
|
|
296
347
|
: null;
|
|
297
348
|
if (command === "install") {
|
|
298
349
|
if (installation === null && !isGlobalDaemonEntry(deps.entryPath, await deps.resolveGlobalNodeModules())) {
|
|
299
|
-
|
|
350
|
+
const releaseTag = requestedReleaseTag ?? profile.releaseTag ?? releaseTagForServerUrl(profile.serverUrl);
|
|
351
|
+
throw new Error(td("Service lifecycle requires a global @nowcrew/daemon install; run npm install --global @nowcrew/daemon@{{tag}}", { tag: releaseTag }));
|
|
300
352
|
}
|
|
301
353
|
if (managedRecord === null)
|
|
302
354
|
await installService(spec, deps.runner);
|
package/dist/computer-profile.js
CHANGED
|
@@ -8,33 +8,57 @@ import { z } from "zod";
|
|
|
8
8
|
import { acquireProfileSaveLock, } from "./computer-profile-lock.js";
|
|
9
9
|
import { formatDaemonText } from "./i18n.js";
|
|
10
10
|
import { atomicPrivateWrite } from "./atomic-private-write.js";
|
|
11
|
+
import { normalizeDaemonNpmRegistry } from "./npm-registry.js";
|
|
11
12
|
const PROFILE_NAME = /^[a-z0-9][a-z0-9_-]{0,47}$/;
|
|
12
13
|
export const MachineTokenSchema = z.string().startsWith("sk_machine_").max(4096);
|
|
13
14
|
const ServerUrlSchema = z.string().url().max(2048).refine((value) => {
|
|
14
15
|
const url = new URL(value);
|
|
15
16
|
return (url.protocol === "http:" || url.protocol === "https:") && !url.username && !url.password;
|
|
16
17
|
}, "serverUrl must be an HTTP(S) URL without embedded credentials");
|
|
17
|
-
const
|
|
18
|
+
const ProfileFields = z.object({
|
|
18
19
|
version: z.literal(1),
|
|
19
20
|
name: z.string().regex(PROFILE_NAME),
|
|
20
21
|
serverUrl: ServerUrlSchema,
|
|
21
22
|
machineToken: MachineTokenSchema,
|
|
22
23
|
agentsRoot: z.string().min(1).optional(),
|
|
23
24
|
runtimePath: z.string().min(1).max(32768).refine((value) => !value.includes("\0") && !value.includes("\n") && !value.includes("\r"), "runtimePath must be a single line").optional(),
|
|
25
|
+
releaseTag: z.enum(["latest", "beta"]).optional(),
|
|
26
|
+
registryUrl: z.string().url().optional(),
|
|
24
27
|
codexHomeMode: z.enum(["shared", "private"]).optional(),
|
|
25
28
|
}).strict();
|
|
26
|
-
|
|
29
|
+
function validateProfileRegistry(value, context) {
|
|
30
|
+
const fields = value;
|
|
31
|
+
if (fields.registryUrl === undefined)
|
|
32
|
+
return;
|
|
33
|
+
try {
|
|
34
|
+
const normalized = normalizeDaemonNpmRegistry(fields.registryUrl, {
|
|
35
|
+
allowLoopbackHttp: fields.releaseTag === "beta",
|
|
36
|
+
});
|
|
37
|
+
if (normalized !== fields.registryUrl) {
|
|
38
|
+
context.addIssue({ code: z.ZodIssueCode.custom, path: ["registryUrl"], message: "registryUrl must be normalized" });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
context.addIssue({
|
|
43
|
+
code: z.ZodIssueCode.custom,
|
|
44
|
+
path: ["registryUrl"],
|
|
45
|
+
message: error instanceof Error ? error.message : "registryUrl must be HTTPS",
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const ProfileSchema = ProfileFields.superRefine(validateProfileRegistry);
|
|
50
|
+
const ProfileAgentsRootCandidateSchema = ProfileFields.pick({
|
|
27
51
|
name: true,
|
|
28
52
|
serverUrl: true,
|
|
29
53
|
agentsRoot: true,
|
|
30
54
|
}).strict();
|
|
31
55
|
const StoredPlainProfileSchema = ProfileSchema;
|
|
32
|
-
const StoredProtectedProfileSchema =
|
|
56
|
+
const StoredProtectedProfileSchema = ProfileFields.omit({ machineToken: true }).extend({
|
|
33
57
|
machineTokenProtected: z.object({
|
|
34
58
|
scheme: z.literal("dpapi-current-user"),
|
|
35
59
|
ciphertext: z.string().min(1).max(16384),
|
|
36
60
|
}).strict(),
|
|
37
|
-
}).strict();
|
|
61
|
+
}).strict().superRefine(validateProfileRegistry);
|
|
38
62
|
const StoredProfileSchema = z.union([StoredPlainProfileSchema, StoredProtectedProfileSchema]);
|
|
39
63
|
export const PROFILE_AGENTS_ROOT_CONFLICT_MESSAGE = "Profile '{{profile}}' conflicts with profile '{{conflict}}': both resolve to agents root '{{agentsRoot}}'. Save it with a unique root, for example: {{command}}";
|
|
40
64
|
export class ProfileAgentsRootConflictError extends Error {
|
|
@@ -218,6 +242,8 @@ export async function saveProfile(input, home = daemonHome(), options = {}) {
|
|
|
218
242
|
serverUrl: profile.serverUrl,
|
|
219
243
|
...(profile.agentsRoot ? { agentsRoot: profile.agentsRoot } : {}),
|
|
220
244
|
...(profile.runtimePath ? { runtimePath: profile.runtimePath } : {}),
|
|
245
|
+
...(profile.releaseTag === undefined ? {} : { releaseTag: profile.releaseTag }),
|
|
246
|
+
...(profile.registryUrl === undefined ? {} : { registryUrl: profile.registryUrl }),
|
|
221
247
|
...(profile.codexHomeMode === undefined ? {} : { codexHomeMode: profile.codexHomeMode }),
|
|
222
248
|
machineTokenProtected: { scheme: "dpapi-current-user", ciphertext },
|
|
223
249
|
});
|
|
@@ -261,6 +287,8 @@ export async function loadProfile(name, home = daemonHome(), options = {}) {
|
|
|
261
287
|
serverUrl: stored.serverUrl,
|
|
262
288
|
...(stored.agentsRoot ? { agentsRoot: stored.agentsRoot } : {}),
|
|
263
289
|
...(stored.runtimePath ? { runtimePath: stored.runtimePath } : {}),
|
|
290
|
+
...(stored.releaseTag === undefined ? {} : { releaseTag: stored.releaseTag }),
|
|
291
|
+
...(stored.registryUrl === undefined ? {} : { registryUrl: stored.registryUrl }),
|
|
264
292
|
machineToken: await configured.protector.unprotect(stored.machineTokenProtected.ciphertext),
|
|
265
293
|
});
|
|
266
294
|
}
|
|
@@ -355,5 +383,13 @@ export function applyProfileToEnv(profile, env) {
|
|
|
355
383
|
delete env.CREW_AGENTS_ROOT;
|
|
356
384
|
if (profile.runtimePath)
|
|
357
385
|
env.PATH = profile.runtimePath;
|
|
386
|
+
if (profile.releaseTag)
|
|
387
|
+
env.CREW_DAEMON_RELEASE_TAG = profile.releaseTag;
|
|
388
|
+
else
|
|
389
|
+
delete env.CREW_DAEMON_RELEASE_TAG;
|
|
390
|
+
if (profile.registryUrl)
|
|
391
|
+
env.CREW_NPM_REGISTRY = profile.registryUrl;
|
|
392
|
+
else
|
|
393
|
+
delete env.CREW_NPM_REGISTRY;
|
|
358
394
|
env.CREW_CODEX_HOME_MODE = profile.codexHomeMode ?? "shared";
|
|
359
395
|
}
|
package/dist/computer-service.js
CHANGED
|
@@ -4,6 +4,7 @@ import { execFile, spawn } from "node:child_process";
|
|
|
4
4
|
import { createHash, randomUUID } from "node:crypto";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
6
|
import { dirname, resolve } from "node:path";
|
|
7
|
+
import { normalizeDaemonNpmRegistry } from "./npm-registry.js";
|
|
7
8
|
const execFileAsync = promisify(execFile);
|
|
8
9
|
const WINDOWS_STOP_TIMEOUT_MS = 5_000;
|
|
9
10
|
const WINDOWS_STOP_POLL_MS = 100;
|
|
@@ -541,19 +542,23 @@ export async function doctorService(spec, profilePrivate, runner = systemCommand
|
|
|
541
542
|
{ name: "service-running", ok: status.running === true, detail: status.running ? "running" : "not running" },
|
|
542
543
|
];
|
|
543
544
|
}
|
|
544
|
-
export function buildUpgradeCommand(platform, npmPrefix) {
|
|
545
|
+
export function buildUpgradeCommand(platform, npmPrefix, releaseTag = "latest", registryUrl) {
|
|
545
546
|
return [
|
|
546
547
|
platform === "win32" ? "npm.cmd" : "npm",
|
|
547
548
|
[
|
|
548
549
|
"install",
|
|
549
550
|
"--global",
|
|
551
|
+
"--ignore-scripts",
|
|
552
|
+
"--no-audit",
|
|
553
|
+
"--no-fund",
|
|
550
554
|
...(npmPrefix === undefined ? [] : ["--prefix", npmPrefix]),
|
|
551
|
-
"
|
|
555
|
+
...(registryUrl === undefined ? [] : ["--registry", normalizeDaemonNpmRegistry(registryUrl, { allowLoopbackHttp: releaseTag === "beta" })]),
|
|
556
|
+
`@nowcrew/daemon@${releaseTag}`,
|
|
552
557
|
],
|
|
553
558
|
];
|
|
554
559
|
}
|
|
555
|
-
export async function upgradeDaemon(platform, runner = systemCommandRunner, npmPrefix) {
|
|
556
|
-
const [command, args] = buildUpgradeCommand(platform, npmPrefix);
|
|
560
|
+
export async function upgradeDaemon(platform, runner = systemCommandRunner, npmPrefix, releaseTag = "latest", registryUrl) {
|
|
561
|
+
const [command, args] = buildUpgradeCommand(platform, npmPrefix, releaseTag, registryUrl);
|
|
557
562
|
await checked(runner, command, args, "Daemon upgrade");
|
|
558
563
|
}
|
|
559
564
|
const WINDOWS_SID_SCRIPT = "[System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value";
|
|
@@ -119,10 +119,12 @@ export function createDaemonMigrationController(input) {
|
|
|
119
119
|
let currentSpec;
|
|
120
120
|
let currentService;
|
|
121
121
|
let managedRecord = null;
|
|
122
|
+
let currentRegistryUrl;
|
|
122
123
|
try {
|
|
123
124
|
const currentProfile = await (input.loadCurrentProfile ?? loadProfile)(input.profileName, input.currentDaemonHome);
|
|
124
125
|
if (currentProfile.machineToken !== input.machineToken)
|
|
125
126
|
throw new Error("active profile token does not match this daemon");
|
|
127
|
+
currentRegistryUrl = currentProfile.registryUrl;
|
|
126
128
|
const target = resolveAutomaticInstallTarget({ serverUrl: input.serverUrl, requestedProfile: message.targetProfile, userHome, platform });
|
|
127
129
|
if (!isAbsolute(message.targetDaemonHome) || !isAbsolute(message.targetAgentsRoot)
|
|
128
130
|
|| resolve(message.targetDaemonHome) !== resolve(target.daemonHome)
|
|
@@ -165,6 +167,7 @@ export function createDaemonMigrationController(input) {
|
|
|
165
167
|
serverUrl: input.serverUrl,
|
|
166
168
|
machineToken: input.machineToken,
|
|
167
169
|
requestedProfile: message.targetProfile,
|
|
170
|
+
...(currentRegistryUrl === undefined ? {} : { requestedRegistryUrl: currentRegistryUrl }),
|
|
168
171
|
});
|
|
169
172
|
if (result.profile !== message.targetProfile || result.serviceId.length === 0) {
|
|
170
173
|
throw new Error("target service identity did not match the requested profile");
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { compare, prerelease, valid } from "semver";
|
|
2
|
+
const CANONICAL_VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-beta(?:\.|-)(0|[1-9]\d*))?$/u;
|
|
3
|
+
function normalizedVersion(input) {
|
|
4
|
+
return input.replace(/-beta-(0|[1-9]\d*)$/u, "-beta.$1");
|
|
5
|
+
}
|
|
6
|
+
export function isDaemonReleaseVersion(input) {
|
|
7
|
+
const raw = input.trim();
|
|
8
|
+
if (raw !== input || !CANONICAL_VERSION.test(raw) || valid(raw) !== raw)
|
|
9
|
+
return false;
|
|
10
|
+
const parts = prerelease(raw);
|
|
11
|
+
return parts === null || normalizedVersion(raw).includes("-beta.");
|
|
12
|
+
}
|
|
13
|
+
export function daemonReleaseChannel(version) {
|
|
14
|
+
if (!isDaemonReleaseVersion(version))
|
|
15
|
+
return null;
|
|
16
|
+
return prerelease(version) === null ? "stable" : "beta";
|
|
17
|
+
}
|
|
18
|
+
export function compareDaemonReleaseVersions(left, right) {
|
|
19
|
+
return isDaemonReleaseVersion(left) && isDaemonReleaseVersion(right)
|
|
20
|
+
? compare(normalizedVersion(left), normalizedVersion(right))
|
|
21
|
+
: 0;
|
|
22
|
+
}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { isDaemonReleaseVersion } from "./daemon-release-version.js";
|
|
2
3
|
const DaemonUpdateMessageSchema = z.object({
|
|
3
4
|
type: z.literal("daemon:update"),
|
|
4
5
|
updateId: z.string().uuid(),
|
|
5
|
-
targetVersion: z.string().
|
|
6
|
+
targetVersion: z.string().refine(isDaemonReleaseVersion, "invalid daemon release version"),
|
|
6
7
|
}).strict();
|
|
7
8
|
const DaemonRestartMessageSchema = z.object({
|
|
8
9
|
type: z.literal("daemon:restart"),
|
|
9
10
|
updateId: z.string().uuid(),
|
|
10
|
-
targetVersion: z.string().
|
|
11
|
+
targetVersion: z.string().refine(isDaemonReleaseVersion, "invalid daemon release version"),
|
|
11
12
|
}).strict();
|
|
12
13
|
const DaemonControlMessageSchema = z.union([DaemonUpdateMessageSchema, DaemonRestartMessageSchema]);
|
|
13
14
|
export function createDaemonUpdateController(deps) {
|
package/dist/daemon-updater.js
CHANGED
|
@@ -2,7 +2,19 @@ import { readFile } from "node:fs/promises";
|
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { systemCommandRunner } from "./computer-service.js";
|
|
4
4
|
import { daemonGlobalInstallation } from "./daemon-installation.js";
|
|
5
|
-
|
|
5
|
+
import { daemonReleaseChannel, isDaemonReleaseVersion } from "./daemon-release-version.js";
|
|
6
|
+
import { normalizeDaemonNpmRegistry } from "./npm-registry.js";
|
|
7
|
+
function configuredRegistryArgs(allowLoopbackHttp) {
|
|
8
|
+
try {
|
|
9
|
+
return process.env.CREW_NPM_REGISTRY === undefined ? [] : [
|
|
10
|
+
"--registry",
|
|
11
|
+
normalizeDaemonNpmRegistry(process.env.CREW_NPM_REGISTRY, { allowLoopbackHttp }),
|
|
12
|
+
];
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
6
18
|
export async function prepareDaemonRestart(input) {
|
|
7
19
|
const local = input.localSlots.tryAcquireExclusive();
|
|
8
20
|
if (local === null)
|
|
@@ -28,8 +40,32 @@ async function readPackageVersion(packageRoot) {
|
|
|
28
40
|
const body = JSON.parse(await readFile(resolve(packageRoot, "package.json"), "utf8"));
|
|
29
41
|
return typeof body.version === "string" ? body.version : "";
|
|
30
42
|
}
|
|
43
|
+
async function restoreInstalledVersion(input, runner, version, registryArgs) {
|
|
44
|
+
if (!isDaemonReleaseVersion(version))
|
|
45
|
+
return false;
|
|
46
|
+
const result = await runner((input.platform ?? process.platform) === "win32" ? "npm.cmd" : "npm", [
|
|
47
|
+
"install",
|
|
48
|
+
"--global",
|
|
49
|
+
"--prefix",
|
|
50
|
+
input.npmPrefix,
|
|
51
|
+
"--ignore-scripts",
|
|
52
|
+
"--no-audit",
|
|
53
|
+
"--no-fund",
|
|
54
|
+
...registryArgs,
|
|
55
|
+
`@nowcrew/daemon@${version}`,
|
|
56
|
+
]).catch(() => null);
|
|
57
|
+
if (result?.exitCode !== 0)
|
|
58
|
+
return false;
|
|
59
|
+
try {
|
|
60
|
+
const installedVersion = await (input.readInstalledVersion ?? readPackageVersion)(input.packageRoot);
|
|
61
|
+
return installedVersion === version;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
31
67
|
export async function installExactDaemonUpdate(input) {
|
|
32
|
-
if (!
|
|
68
|
+
if (!isDaemonReleaseVersion(input.targetVersion)) {
|
|
33
69
|
return { ok: false, errorCode: "ineligible" };
|
|
34
70
|
}
|
|
35
71
|
const installation = daemonGlobalInstallation(input.currentEntryPath ?? process.argv[1] ?? "", input.platform ?? process.platform);
|
|
@@ -62,6 +98,25 @@ export async function installExactDaemonUpdate(input) {
|
|
|
62
98
|
return { ok: false, errorCode: "ineligible" };
|
|
63
99
|
}
|
|
64
100
|
const runner = input.runner ?? systemCommandRunner;
|
|
101
|
+
let previousVersion = "";
|
|
102
|
+
try {
|
|
103
|
+
previousVersion = await (input.readInstalledVersion ?? readPackageVersion)(input.packageRoot);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// Do not replace a package when there is no verified version to restore.
|
|
107
|
+
await release();
|
|
108
|
+
return { ok: false, errorCode: "install_failed" };
|
|
109
|
+
}
|
|
110
|
+
if (!isDaemonReleaseVersion(previousVersion)) {
|
|
111
|
+
await release();
|
|
112
|
+
return { ok: false, errorCode: "install_failed" };
|
|
113
|
+
}
|
|
114
|
+
const registryArgs = configuredRegistryArgs(daemonReleaseChannel(previousVersion) === "beta"
|
|
115
|
+
|| daemonReleaseChannel(input.targetVersion) === "beta");
|
|
116
|
+
if (registryArgs === null) {
|
|
117
|
+
await release();
|
|
118
|
+
return { ok: false, errorCode: "install_failed" };
|
|
119
|
+
}
|
|
65
120
|
try {
|
|
66
121
|
await input.onInstalling?.();
|
|
67
122
|
}
|
|
@@ -77,6 +132,7 @@ export async function installExactDaemonUpdate(input) {
|
|
|
77
132
|
"--ignore-scripts",
|
|
78
133
|
"--no-audit",
|
|
79
134
|
"--no-fund",
|
|
135
|
+
...registryArgs,
|
|
80
136
|
`@nowcrew/daemon@${input.targetVersion}`,
|
|
81
137
|
]).catch(() => null);
|
|
82
138
|
if (result === null || result.exitCode !== 0) {
|
|
@@ -91,6 +147,10 @@ export async function installExactDaemonUpdate(input) {
|
|
|
91
147
|
// Version verification is authoritative; unreadable package metadata is a mismatch.
|
|
92
148
|
}
|
|
93
149
|
if (installedVersion !== input.targetVersion) {
|
|
150
|
+
const restored = await restoreInstalledVersion(input, runner, previousVersion, registryArgs);
|
|
151
|
+
if (!restored) {
|
|
152
|
+
console.error(`[daemon-update] version verification failed; restore of ${previousVersion} was unsuccessful`);
|
|
153
|
+
}
|
|
94
154
|
await release();
|
|
95
155
|
return { ok: false, errorCode: "version_mismatch" };
|
|
96
156
|
}
|
package/dist/i18n.js
CHANGED
|
@@ -51,12 +51,14 @@ const zh = {
|
|
|
51
51
|
"Upgraded daemon and restart request accepted for '{{name}}'. Verify with status.": "daemon 已升级,并已请求重启 '{{name}}';请用 status 确认。",
|
|
52
52
|
"Refused daemon upgrade for '{{name}}': {{reason}}": "已拒绝升级 daemon '{{name}}':{{reason}}",
|
|
53
53
|
"Upgraded daemon. Installed services were not restarted; pass --profile to restart one.": "daemon 已升级;已安装服务尚未重启,可传入 --profile 重启指定服务。",
|
|
54
|
+
"Cross-channel daemon upgrade requires --profile so the server environment can be verified": "跨发布渠道升级 daemon 需要传入 --profile,以便校验服务端环境。",
|
|
54
55
|
"Service lifecycle requires the built daemon entry (.js), not a TypeScript development entry": "服务生命周期必须使用已构建的 daemon 入口(.js),不能使用 TypeScript 开发入口",
|
|
56
|
+
"Installed daemon release tag '{{installed}}' does not match profile '{{profile}}' release tag '{{target}}'": "已安装 daemon 的发布标签 '{{installed}}' 与配置 '{{profile}}' 要求的发布标签 '{{target}}' 不一致",
|
|
55
57
|
"Installed '{{id}}'. Use status to confirm runtime state.": "已安装 '{{id}}';请用 status 确认运行状态。",
|
|
56
58
|
"Uninstalled '{{id}}'.": "已卸载 '{{id}}'。",
|
|
57
59
|
"{{action}} request accepted for '{{id}}'. Verify with status.": "已接受对 '{{id}}' 的 {{action}} 请求;请用 status 确认。",
|
|
58
60
|
"--token-stdin requires a token on standard input": "--token-stdin 需要从标准输入读取令牌",
|
|
59
|
-
"Service lifecycle requires a global @nowcrew/daemon install; run npm install --global @nowcrew/daemon@
|
|
61
|
+
"Service lifecycle requires a global @nowcrew/daemon install; run npm install --global @nowcrew/daemon@{{tag}}": "服务生命周期需要全局安装 @nowcrew/daemon;请运行 npm install --global @nowcrew/daemon@{{tag}}",
|
|
60
62
|
"Profile '{{profile}}' conflicts with profile '{{conflict}}': both resolve to agents root '{{agentsRoot}}'. Save it with a unique root, for example: {{command}}": "配置 '{{profile}}' 与配置 '{{conflict}}' 解析到了同一个 agents root '{{agentsRoot}}'。请保存为唯一目录,例如:{{command}}",
|
|
61
63
|
"Daemon is already running (PID={{ownerPid}}) for agents root '{{agentsRoot}}'. Journal: '{{journalPath}}'. Do not delete the active journal lock. Stop the existing daemon before retrying. If this profile is managed as a service, use 'crew-daemon stop --profile {{profileName}}' or 'crew-daemon restart --profile {{profileName}}'.": "daemon 已在运行(PID={{ownerPid}}),agents root 为 '{{agentsRoot}}'。日志目录:'{{journalPath}}'。不要删除活跃进程持有的 journal 锁。请先停止现有 daemon 再重试;如果此 profile 由系统服务管理,请使用 'crew-daemon stop --profile {{profileName}}' 或 'crew-daemon restart --profile {{profileName}}'。",
|
|
62
64
|
"Daemon is already running (PID={{ownerPid}}) for agents root '{{agentsRoot}}'. Journal: '{{journalPath}}'. Do not delete the active journal lock. Stop the existing daemon before retrying. If it is managed as a service, use 'crew-daemon stop --profile <name>' or 'crew-daemon restart --profile <name>'.": "daemon 已在运行(PID={{ownerPid}}),agents root 为 '{{agentsRoot}}'。日志目录:'{{journalPath}}'。不要删除活跃进程持有的 journal 锁。请先停止现有 daemon 再重试;如果它由系统服务管理,请使用 'crew-daemon stop --profile <name>' 或 'crew-daemon restart --profile <name>'。",
|
package/dist/machine-info.js
CHANGED
|
@@ -20,6 +20,7 @@ import { probeOpenCodeRun } from "./runtimes/opencode.js";
|
|
|
20
20
|
import { RUNTIME_HEALTH_PROBE_CAPABILITY } from "./runtime-health.js";
|
|
21
21
|
import { probeDeepSeekHarnessAcp } from "./runtimes/deepseek-harness.js";
|
|
22
22
|
import { daemonHome } from "./computer-profile.js";
|
|
23
|
+
import { daemonReleaseProvenance } from "./release-provenance.js";
|
|
23
24
|
export { executableRuntimes } from "./runtime-capabilities.js";
|
|
24
25
|
const execFileP = promisify(execFile);
|
|
25
26
|
export const PROJECT_WORKSPACES_CAPABILITY = "project_workspaces_v1";
|
|
@@ -182,6 +183,15 @@ export async function collectMachineHello(agentsRoot, executionLimits, runtimePl
|
|
|
182
183
|
hostname: hostname(),
|
|
183
184
|
os: `${osPlatform()} ${arch()}`,
|
|
184
185
|
daemonVersion: daemonVersion(),
|
|
186
|
+
...(() => {
|
|
187
|
+
const provenance = daemonReleaseProvenance(daemonVersion());
|
|
188
|
+
return {
|
|
189
|
+
releaseChannel: provenance.releaseChannel,
|
|
190
|
+
releaseTag: provenance.releaseTag,
|
|
191
|
+
...(provenance.gitSha === null ? {} : { gitSha: provenance.gitSha }),
|
|
192
|
+
...(provenance.artifactDigest === null ? {} : { artifactDigest: provenance.artifactDigest }),
|
|
193
|
+
};
|
|
194
|
+
})(),
|
|
185
195
|
runtimes,
|
|
186
196
|
executionRuntimes,
|
|
187
197
|
capabilities,
|
package/dist/main.js
CHANGED
|
@@ -11,6 +11,8 @@ import { join } from "node:path";
|
|
|
11
11
|
import { loadConfig, ConfigError } from "./config.js";
|
|
12
12
|
import { detectDaemonLang, formatDaemonText, translateDaemon } from "./i18n.js";
|
|
13
13
|
import { cliVersion, daemonVersion } from "./machine-info.js";
|
|
14
|
+
import { daemonReleaseProvenance } from "./release-provenance.js";
|
|
15
|
+
import { assertReleaseTagForServerUrl } from "./automatic-install-target.js";
|
|
14
16
|
import { runAgent } from "./runner.js";
|
|
15
17
|
import { serve } from "./serve.js";
|
|
16
18
|
import { initSlog, flushSlog } from "./slog.js";
|
|
@@ -57,11 +59,12 @@ async function main() {
|
|
|
57
59
|
"daemon-home": { type: "string" },
|
|
58
60
|
},
|
|
59
61
|
});
|
|
60
|
-
// 无子命令时默认 serve(
|
|
62
|
+
// 无子命令时默认 serve(对齐当前发布 channel 的 npx 入口)
|
|
61
63
|
const cmd = positionals[0] ?? "serve";
|
|
62
64
|
if (cmd !== "run" && cmd !== "serve") {
|
|
65
|
+
const releaseTag = daemonReleaseProvenance(daemonVersion()).releaseTag;
|
|
63
66
|
process.stderr.write(td("Usage:") + "\n" +
|
|
64
|
-
|
|
67
|
+
` npx @nowcrew/daemon@${releaseTag} --server-url <url> --api-key <sk_machine_*> # ` + td("connect and stay resident") + "\n" +
|
|
65
68
|
" crew-daemon run --agent <h> --channel <id> [--wake ...] # " + td("run once manually") + "\n");
|
|
66
69
|
process.exit(2);
|
|
67
70
|
}
|
|
@@ -112,8 +115,10 @@ async function main() {
|
|
|
112
115
|
}
|
|
113
116
|
throw e;
|
|
114
117
|
}
|
|
118
|
+
const provenance = daemonReleaseProvenance(daemonVersion());
|
|
119
|
+
assertReleaseTagForServerUrl(config.serverUrl, provenance.releaseTag);
|
|
115
120
|
if (cmd === "serve") {
|
|
116
|
-
process.stdout.write(formatDaemonLogLine(`🛰️ crew-daemon v${daemonVersion()} (cli v${cliVersion()}) ${td("resident, connecting to")} ${config.serverUrl} ${td("control plane")}...`) + "\n");
|
|
121
|
+
process.stdout.write(formatDaemonLogLine(`🛰️ crew-daemon v${daemonVersion()} (cli v${cliVersion()}, ${provenance.releaseChannel}/${provenance.releaseTag}) ${td("resident, connecting to")} ${config.serverUrl} ${td("control plane")}...`) + "\n");
|
|
117
122
|
const installation = values.profile === undefined
|
|
118
123
|
? null
|
|
119
124
|
: daemonGlobalInstallation(builtDaemonEntry(), process.platform);
|
|
@@ -199,6 +204,10 @@ async function main() {
|
|
|
199
204
|
initSlog(config.serverUrl, config.machineToken, {
|
|
200
205
|
daemonVersion: daemonVersion(),
|
|
201
206
|
cliVersion: cliVersion(),
|
|
207
|
+
releaseChannel: provenance.releaseChannel,
|
|
208
|
+
releaseTag: provenance.releaseTag,
|
|
209
|
+
gitSha: provenance.gitSha,
|
|
210
|
+
artifactDigest: provenance.artifactDigest,
|
|
202
211
|
...(values.profile === undefined ? {} : { profileName: values.profile }),
|
|
203
212
|
agentsRoot: config.agentsRoot,
|
|
204
213
|
}); // 一次性 run 模式也上报 SLS(runner 里的埋点生效)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org";
|
|
2
|
+
function isLoopbackHostname(hostname) {
|
|
3
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
|
|
4
|
+
}
|
|
5
|
+
export function normalizeDaemonNpmRegistry(value, options = {}) {
|
|
6
|
+
const raw = value?.trim();
|
|
7
|
+
if (!raw)
|
|
8
|
+
return DEFAULT_NPM_REGISTRY;
|
|
9
|
+
let parsed;
|
|
10
|
+
try {
|
|
11
|
+
parsed = new URL(raw);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
throw new Error("npm registry must be an HTTPS URL without credentials");
|
|
15
|
+
}
|
|
16
|
+
if (!/^[A-Za-z0-9._~/%-]*$/u.test(parsed.pathname)) {
|
|
17
|
+
throw new Error("npm registry must be an HTTPS URL without credentials or unsafe path characters");
|
|
18
|
+
}
|
|
19
|
+
if ((parsed.protocol !== "https:" && !(parsed.protocol === "http:"
|
|
20
|
+
&& isLoopbackHostname(parsed.hostname)
|
|
21
|
+
&& options.allowLoopbackHttp === true))
|
|
22
|
+
|| parsed.username !== ""
|
|
23
|
+
|| parsed.password !== ""
|
|
24
|
+
|| parsed.search !== ""
|
|
25
|
+
|| parsed.hash !== "") {
|
|
26
|
+
throw new Error("npm registry must be an HTTPS URL without credentials");
|
|
27
|
+
}
|
|
28
|
+
return parsed.toString().replace(/\/+$/u, "");
|
|
29
|
+
}
|
|
30
|
+
export function npmRegistryArgs(value) {
|
|
31
|
+
return ["--registry", normalizeDaemonNpmRegistry(value)];
|
|
32
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { cliVersion, daemonVersion } from "./machine-info.js";
|
|
2
|
+
import { daemonReleaseProvenance } from "./release-provenance.js";
|
|
3
|
+
import { dslog, initSlog } from "./slog.js";
|
|
4
|
+
export function initReleaseSlog(input) {
|
|
5
|
+
const provenance = daemonReleaseProvenance(daemonVersion());
|
|
6
|
+
initSlog(input.serverUrl, input.machineToken, {
|
|
7
|
+
daemonVersion: daemonVersion(),
|
|
8
|
+
cliVersion: cliVersion(),
|
|
9
|
+
releaseChannel: provenance.releaseChannel,
|
|
10
|
+
releaseTag: provenance.releaseTag,
|
|
11
|
+
gitSha: provenance.gitSha,
|
|
12
|
+
artifactDigest: provenance.artifactDigest,
|
|
13
|
+
...(input.profileName === undefined ? {} : { profileName: input.profileName }),
|
|
14
|
+
agentsRoot: input.agentsRoot,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
export function reportReleaseChannelError(log) {
|
|
18
|
+
const provenance = daemonReleaseProvenance(daemonVersion());
|
|
19
|
+
log(`🛑 线上环境只允许正式版 daemon,当前 ${provenance.releaseTag} 包不可连接。`);
|
|
20
|
+
dslog("daemon.ws_release_rejected", "线上控制面拒绝非正式 daemon 包", {
|
|
21
|
+
level: "ERROR",
|
|
22
|
+
release_channel: provenance.releaseChannel,
|
|
23
|
+
release_tag: provenance.releaseTag,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
export function reportReleaseArtifactError(log) {
|
|
27
|
+
log("🛑 daemon artifact digest 未通过 server allowlist,当前安装包不能连接控制面。");
|
|
28
|
+
dslog("daemon.ws_artifact_rejected", "控制面拒绝未获批准的 daemon artifact", {
|
|
29
|
+
level: "ERROR",
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
export function reportReleaseChannelClose(code, log) {
|
|
33
|
+
if (code !== 4002)
|
|
34
|
+
return false;
|
|
35
|
+
log("🛑 线上环境只允许正式版 daemon,beta 包不能连接控制面 (code 4002)。");
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
export function reportReleaseError(code, log) {
|
|
39
|
+
if (code === "DAEMON_RELEASE_CHANNEL_MISMATCH") {
|
|
40
|
+
reportReleaseChannelError(log);
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
if (code === "DAEMON_RELEASE_ARTIFACT_MISMATCH") {
|
|
44
|
+
reportReleaseArtifactError(log);
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
export function reportDaemonConnectionClose(code, log) {
|
|
50
|
+
if (code === 4001) {
|
|
51
|
+
log("🛑 控制面以鉴权失败关闭连接 (code 4001):机器凭证无效或已吊销。");
|
|
52
|
+
log(" 请在 NowWork 重新 \"Add Computer\" 获取新连接命令再重跑(当前 --api-key 已失效)。");
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
if (code === 4003) {
|
|
56
|
+
reportReleaseArtifactError(log);
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
return reportReleaseChannelClose(code, log);
|
|
60
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { lstatSync, readdirSync, readFileSync } from "node:fs";
|
|
3
|
+
import { basename, dirname, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { daemonReleaseChannel } from "./daemon-release-version.js";
|
|
6
|
+
function validSha(value) {
|
|
7
|
+
return value !== undefined && /^[a-f0-9]{40}$/u.test(value) ? value : null;
|
|
8
|
+
}
|
|
9
|
+
function validDigest(value) {
|
|
10
|
+
return value !== undefined && /^[a-f0-9]{64}$/u.test(value) ? value : null;
|
|
11
|
+
}
|
|
12
|
+
function collectArtifactFiles(root, directory, files) {
|
|
13
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
14
|
+
const absolutePath = resolve(directory, entry.name);
|
|
15
|
+
if (entry.isDirectory()) {
|
|
16
|
+
if (!collectArtifactFiles(root, absolutePath, files))
|
|
17
|
+
return false;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
// Reject links and special files so local and packed artifacts share one
|
|
21
|
+
// regular-file-only digest contract.
|
|
22
|
+
if (!entry.isFile())
|
|
23
|
+
return false;
|
|
24
|
+
files.push({
|
|
25
|
+
relativePath: relative(root, absolutePath).split(sep).join("/"),
|
|
26
|
+
bytes: readFileSync(absolutePath),
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
function artifactDigestState(distDirectory) {
|
|
32
|
+
let distStat;
|
|
33
|
+
try {
|
|
34
|
+
distStat = lstatSync(distDirectory);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
return error.code === "ENOENT"
|
|
38
|
+
? { exists: false, digest: null }
|
|
39
|
+
: { exists: true, digest: null };
|
|
40
|
+
}
|
|
41
|
+
if (!distStat.isDirectory())
|
|
42
|
+
return { exists: true, digest: null };
|
|
43
|
+
try {
|
|
44
|
+
const files = [];
|
|
45
|
+
if (!collectArtifactFiles(distDirectory, distDirectory, files))
|
|
46
|
+
return { exists: true, digest: null };
|
|
47
|
+
files.sort((a, b) => a.relativePath < b.relativePath ? -1 : a.relativePath > b.relativePath ? 1 : 0);
|
|
48
|
+
const hash = createHash("sha256");
|
|
49
|
+
for (const file of files) {
|
|
50
|
+
hash.update(file.relativePath, "utf8");
|
|
51
|
+
hash.update(Buffer.from([0]));
|
|
52
|
+
hash.update(file.bytes);
|
|
53
|
+
hash.update(Buffer.from([0]));
|
|
54
|
+
}
|
|
55
|
+
return { exists: true, digest: hash.digest("hex") };
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return { exists: true, digest: null };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** Compute the canonical content digest for a daemon dist directory. */
|
|
62
|
+
export function computeArtifactContentSha256(distDirectory) {
|
|
63
|
+
return artifactDigestState(distDirectory).digest;
|
|
64
|
+
}
|
|
65
|
+
function defaultDistDirectory() {
|
|
66
|
+
const moduleDirectory = dirname(fileURLToPath(import.meta.url));
|
|
67
|
+
return basename(moduleDirectory) === "dist" ? moduleDirectory : resolve(moduleDirectory, "../dist");
|
|
68
|
+
}
|
|
69
|
+
export function daemonReleaseProvenance(version, env = process.env, options = {}) {
|
|
70
|
+
const channel = daemonReleaseChannel(version) ?? "stable";
|
|
71
|
+
const canonicalTag = channel === "beta" ? "beta" : "latest";
|
|
72
|
+
// The package version is the artifact identity. An inherited environment tag
|
|
73
|
+
// is advisory and must not create a stable/beta mismatch in hello logs.
|
|
74
|
+
const configuredTag = env.CREW_DAEMON_RELEASE_TAG;
|
|
75
|
+
const releaseTag = configuredTag === canonicalTag ? configuredTag : canonicalTag;
|
|
76
|
+
const localArtifact = artifactDigestState(options.distDirectory ?? defaultDistDirectory());
|
|
77
|
+
return Object.freeze({
|
|
78
|
+
packageName: "@nowcrew/daemon",
|
|
79
|
+
packageVersion: version,
|
|
80
|
+
releaseChannel: channel,
|
|
81
|
+
releaseTag,
|
|
82
|
+
gitSha: validSha(env.CREW_DAEMON_GIT_SHA),
|
|
83
|
+
artifactDigest: localArtifact.exists
|
|
84
|
+
? localArtifact.digest
|
|
85
|
+
: validDigest(env.CREW_DAEMON_ARTIFACT_DIGEST),
|
|
86
|
+
});
|
|
87
|
+
}
|
package/dist/serve.js
CHANGED
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
import { WebSocket } from "ws";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { randomUUID } from "node:crypto";
|
|
8
|
-
import {
|
|
8
|
+
import { dslog, setSlogDefaults, drainSpool, flushSlog } from "./slog.js";
|
|
9
9
|
import { mergeRunAgentResults, reportScheduledStartFailure, runAgent } from "./runner.js";
|
|
10
10
|
import { buildOriginDecisionRetryPrompt, buildScheduledPrompt } from "./prompt.js";
|
|
11
|
-
import { collectMachineHello,
|
|
11
|
+
import { collectMachineHello, daemonCapabilities, detectExecutionRuntimesWithSignal, } from "./machine-info.js";
|
|
12
|
+
import { initReleaseSlog, reportDaemonConnectionClose, reportReleaseError } from "./release-channel-guard.js";
|
|
12
13
|
import { conservativeExecutionRuntimes, createRuntimeProbeCoordinator, } from "./runtime-probe.js";
|
|
13
14
|
import { listWorkspace, readWorkspaceFile } from "./workspace-fs.js";
|
|
14
15
|
import { listSkills } from "./skills.js";
|
|
@@ -311,7 +312,7 @@ export function serve(config, opts = {}) {
|
|
|
311
312
|
reservation.release();
|
|
312
313
|
};
|
|
313
314
|
let connectedAt = 0; // 本次 WS 连接建立时刻(断开日志算在线时长用)
|
|
314
|
-
|
|
315
|
+
initReleaseSlog({ serverUrl: config.serverUrl, machineToken: config.machineToken, ...(opts.profileName === undefined ? {} : { profileName: opts.profileName }), agentsRoot: config.agentsRoot });
|
|
315
316
|
dslog("daemon.start", "daemon 常驻模式启动", { server_url: config.serverUrl, runtime: config.runtimeBin });
|
|
316
317
|
// 并行调度:同一 agent 可并行处理多个【不同任务】(线程/频道),每任务隔离 cwd+work-log。
|
|
317
318
|
// scheduled 重复 run 仍由 running 去重;普通同线程 wake 用 legacyTaskTails 串成 FIFO。
|
|
@@ -854,6 +855,8 @@ export function serve(config, opts = {}) {
|
|
|
854
855
|
dslog("daemon.ws_auth_rejected", "控制面拒绝鉴权:机器凭证无效或已吊销", { level: "ERROR" });
|
|
855
856
|
backoff = maxBackoff; // 退避到最大,停止每秒重连刷屏
|
|
856
857
|
}
|
|
858
|
+
if (reportReleaseError(msg.code, log))
|
|
859
|
+
backoff = maxBackoff;
|
|
857
860
|
return;
|
|
858
861
|
}
|
|
859
862
|
if (msg.type === "project:add"
|
|
@@ -1309,11 +1312,8 @@ export function serve(config, opts = {}) {
|
|
|
1309
1312
|
completionRetransmitter.pause();
|
|
1310
1313
|
// 4001 = 控制面应用级「鉴权失败」关闭码(见 server control-plane.ts)。即使上面的
|
|
1311
1314
|
// error 帧因 close 抢先而丢失,也能据关闭码识别这是凭证失效——退避拉满,不再每秒热循环。
|
|
1312
|
-
if (code
|
|
1315
|
+
if (reportDaemonConnectionClose(code, log))
|
|
1313
1316
|
backoff = maxBackoff;
|
|
1314
|
-
log(`🛑 控制面以鉴权失败关闭连接 (code 4001):机器凭证无效或已吊销。`);
|
|
1315
|
-
log(` 请在 NowWork 重新 "Add Computer" 获取新连接命令再重跑(当前 --api-key 已失效)。`);
|
|
1316
|
-
}
|
|
1317
1317
|
log(`🔁 控制面断开,${Math.round(backoff / 1000)}s 后重连`);
|
|
1318
1318
|
// 此刻 server 大概率不可达 → 这条会落 spool,重连 drainSpool 时补传;
|
|
1319
1319
|
// ts 是现在(断开时刻),排查「daemon 为什么断」以它对齐 server 侧 machine_disconnected。
|
package/dist/slog.js
CHANGED
|
@@ -59,6 +59,10 @@ export function initSlog(serverUrl, machineToken, identity = {}) {
|
|
|
59
59
|
pid: process.pid,
|
|
60
60
|
daemon_version: identity.daemonVersion,
|
|
61
61
|
cli_version: identity.cliVersion,
|
|
62
|
+
release_channel: identity.releaseChannel,
|
|
63
|
+
release_tag: identity.releaseTag,
|
|
64
|
+
daemon_git_sha: identity.gitSha,
|
|
65
|
+
daemon_artifact_digest: identity.artifactDigest,
|
|
62
66
|
daemon_profile: identity.profileName,
|
|
63
67
|
server_host: serverHost,
|
|
64
68
|
env: deploymentEnvironment(serverHost),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nowcrew/daemon",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.75",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -18,8 +18,9 @@
|
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@agentclientprotocol/sdk": "1.2.1",
|
|
21
|
-
"@nowcrew/cli": "
|
|
21
|
+
"@nowcrew/cli": "0.4.13",
|
|
22
22
|
"cross-spawn": "^7.0.6",
|
|
23
|
+
"semver": "7.8.5",
|
|
23
24
|
"ws": "^8",
|
|
24
25
|
"yaml": "^2.8.1",
|
|
25
26
|
"zod": "^3.23.0"
|
|
@@ -30,6 +31,7 @@
|
|
|
30
31
|
"devDependencies": {
|
|
31
32
|
"@types/cross-spawn": "^6.0.6",
|
|
32
33
|
"@types/node": "^22.0.0",
|
|
34
|
+
"@types/semver": "7.7.1",
|
|
33
35
|
"@types/ws": "^8",
|
|
34
36
|
"tsx": "^4.19.0",
|
|
35
37
|
"typescript": "^5.6.0",
|