@nowcrew/daemon 0.6.98-beta.5 → 0.6.98-beta.6

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 CHANGED
@@ -110,6 +110,15 @@ to confirm Workspace, Machine, environment, profile, and storage identity. It do
110
110
  existing `.crew` data. Existing `crew-daemon install --server-url ... --api-key ...` and profile lifecycle
111
111
  commands remain available as compatibility and recovery entry points.
112
112
 
113
+ Profiles created by older installers may omit `workspaceId` and `machineId`. During setup, the installer
114
+ uses the newly authorized machine credential to call `POST /daemon/computer-setup/legacy-profile` with
115
+ the SHA-256 of the previous profile token. The server checks historical ownership within that exact
116
+ Workspace and Machine, including revoked credentials, without reactivating them. After the server,
117
+ profile name, Agent root and any existing identity fields match, normal installation persists both UUIDs.
118
+ A mismatch or failed verification stops before changing the local profile or service; do not delete a
119
+ profile or fill in UUIDs by guessing. Deploy the server endpoint before publishing the compatible daemon;
120
+ an older server cannot verify legacy profiles, and already published daemon packages remain unchanged.
121
+
113
122
  ## Machine Environment
114
123
 
115
124
  The daemon loads exported variables from its OS account’s bash/zsh configuration on macOS/Linux,
@@ -1,7 +1,8 @@
1
1
  import { homedir } from "node:os";
2
+ import { createHash } from "node:crypto";
2
3
  import { posix, win32 } from "node:path";
3
4
  import { z } from "zod";
4
- import { assertProfileAgentsRootUnique, listProfiles, loadProfile, MachineTokenSchema, removeProfile, saveProfile, windowsDpapiProtector, } from "./computer-profile.js";
5
+ import { assertProfileAgentsRootUnique, listProfiles, loadProfile, MachineTokenSchema, removeProfile, resolveAgentsRoot, saveProfile, windowsDpapiProtector, } from "./computer-profile.js";
5
6
  import { buildServiceSpec, captureExactService, hardenWindowsProfile, replaceExactService, restoreExactService, serviceAction, serviceStatus, systemCommandRunner, uninstallService, } from "./computer-service.js";
6
7
  import { daemonGlobalInstallation } from "./daemon-installation.js";
7
8
  import { buildManagedServiceRecord, installManagedService, uninstallManagedService, } from "./managed-service-lifecycle.js";
@@ -15,8 +16,8 @@ import { daemonRegistryCandidates } from "./daemon-registry.js";
15
16
  import { npmInvocation } from "./npm-command.js";
16
17
  import { discoverExactLegacyService, } from "./automatic-service-migration.js";
17
18
  import { resolveDaemonLocalStateRootFromEnv } from "./daemon-local-home.js";
18
- import { serverHostIdentity } from "./server-url.js";
19
- import { computerSetupRecoveryGuidance } from "./computer-setup-client.js";
19
+ import { serverHostIdentity, serverUrlsMatch } from "./server-url.js";
20
+ import { computerSetupRecoveryGuidance, createComputerSetupClient, } from "./computer-setup-client.js";
20
21
  export const automaticDaemonInstallStageMessage = {
21
22
  downloading: "Downloading the Daemon package...",
22
23
  configuring: "Configuring the Daemon service...",
@@ -63,6 +64,7 @@ function defaultDependencies() {
63
64
  version: daemonVersion(),
64
65
  ...(process.env.PATH ? { runtimePath: process.env.PATH } : {}),
65
66
  runner,
67
+ verifyLegacyProfileIdentity: (serverUrl, input) => createComputerSetupClient({ serverUrl }).verifyLegacyProfileIdentity(input),
66
68
  captureProfile: async (target) => (await listProfiles(target.daemonHome)).includes(target.profile)
67
69
  ? loadProfile(target.profile, target.daemonHome, storage)
68
70
  : null,
@@ -266,18 +268,37 @@ function setupIdentity(input) {
266
268
  machineId: z.string().uuid().parse(input.machineId),
267
269
  };
268
270
  }
269
- function assertExistingProfileIdentity(previous, identity) {
271
+ async function assertExistingProfileIdentity(previous, identity, target, machineToken, dependencies) {
270
272
  if (previous === null || identity === null)
271
273
  return;
272
- if (previous.workspaceId === undefined || previous.machineId === undefined) {
273
- throw new Error(`Profile '${previous.name}' does not record its Workspace and Machine identity`);
274
- }
275
- if (previous.workspaceId !== identity.workspaceId) {
274
+ if (previous.workspaceId !== undefined && previous.workspaceId !== identity.workspaceId) {
276
275
  throw new Error(`Profile '${previous.name}' belongs to a different Workspace identity`);
277
276
  }
278
- if (previous.machineId !== identity.machineId) {
277
+ if (previous.machineId !== undefined && previous.machineId !== identity.machineId) {
279
278
  throw new Error(`Profile '${previous.name}' belongs to a different Machine identity`);
280
279
  }
280
+ if (previous.workspaceId !== undefined && previous.machineId !== undefined)
281
+ return;
282
+ const rootIdentity = (root) => {
283
+ const resolved = resolveAgentsRoot(root, dependencies.userHome, dependencies.platform);
284
+ return dependencies.platform === "win32" ? resolved.toLowerCase() : resolved;
285
+ };
286
+ if (previous.name !== target.profile || !serverUrlsMatch(previous.serverUrl, target.serverUrl)
287
+ || rootIdentity(previous.agentsRoot) !== rootIdentity(target.agentsRoot)) {
288
+ throw new Error(`Legacy profile '${previous.name}' does not match the requested server, profile, or Agent root`);
289
+ }
290
+ if (dependencies.verifyLegacyProfileIdentity === undefined) {
291
+ throw new Error(`Cannot verify legacy profile '${previous.name}' Workspace and Machine identity`);
292
+ }
293
+ // The old credential may already have been revoked by setup. Only the current credential
294
+ // authenticates this read-only request; the old hash proves historical ownership.
295
+ const verified = await dependencies.verifyLegacyProfileIdentity(target.serverUrl, {
296
+ machineToken,
297
+ previousTokenHash: createHash("sha256").update(previous.machineToken).digest("hex"),
298
+ });
299
+ if (verified.workspaceId !== identity.workspaceId || verified.machineId !== identity.machineId) {
300
+ throw new Error(`Legacy profile '${previous.name}' belongs to a different Workspace or Machine identity`);
301
+ }
281
302
  }
282
303
  export async function installDaemonAutomatically(input, dependencies = defaultDependencies()) {
283
304
  const machineToken = MachineTokenSchema.parse(input.machineToken);
@@ -314,7 +335,7 @@ export async function installDaemonAutomatically(input, dependencies = defaultDe
314
335
  });
315
336
  const previousProfile = await dependencies.captureProfile(target);
316
337
  await dependencies.assertProfileTarget(target, previousProfile);
317
- assertExistingProfileIdentity(previousProfile, identity);
338
+ await assertExistingProfileIdentity(previousProfile, identity, target, machineToken, dependencies);
318
339
  const previousService = await dependencies.captureService(spec, target);
319
340
  if (previousService.installed && !previousService.exact) {
320
341
  throw new Error(`Installed service '${spec.id}' does not match the requested identity`);
File without changes
@@ -299,6 +299,11 @@ export function createComputerSetupClient(options) {
299
299
  return receipt.data;
300
300
  },
301
301
  tokenTarget: async (machineToken) => SetupTokenTargetSchema.parse(await request("GET", "/daemon/computer-setup/target", undefined, machineToken)),
302
+ verifyLegacyProfileIdentity: async (input) => z.object({
303
+ workspaceId: z.string().uuid(), machineId: z.string().uuid(),
304
+ }).strict().parse(await request("POST", "/daemon/computer-setup/legacy-profile", {
305
+ previousTokenHash: input.previousTokenHash,
306
+ }, input.machineToken)),
302
307
  start: async (input) => {
303
308
  return parseStart(await request("POST", "/api/auth/computer-setup/start", {
304
309
  ...(input.workspace.kind === "id"
package/dist/main.js CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowcrew/daemon",
3
- "version": "0.6.98-beta.5",
3
+ "version": "0.6.98-beta.6",
4
4
  "type": "module",
5
5
  "description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
6
6
  "license": "Apache-2.0",
@@ -18,6 +18,13 @@
18
18
  "publishConfig": {
19
19
  "access": "public"
20
20
  },
21
+ "scripts": {
22
+ "daemon": "pnpm --filter @nowcrew/cli build && tsx src/main.ts",
23
+ "build": "tsc -p tsconfig.json",
24
+ "prepublishOnly": "pnpm build && node ../scripts/daemon-release-artifact.mjs --strict-registry",
25
+ "test": "vitest run",
26
+ "typecheck": "tsc --noEmit"
27
+ },
21
28
  "dependencies": {
22
29
  "@agentclientprotocol/sdk": "1.2.1",
23
30
  "@nowcrew/cli": "0.4.17",
@@ -39,11 +46,5 @@
39
46
  "tsx": "^4.19.0",
40
47
  "typescript": "^5.6.0",
41
48
  "vitest": "^2.1.0"
42
- },
43
- "scripts": {
44
- "daemon": "pnpm --filter @nowcrew/cli build && tsx src/main.ts",
45
- "build": "tsc -p tsconfig.json",
46
- "test": "vitest run",
47
- "typecheck": "tsc --noEmit"
48
49
  }
49
- }
50
+ }