@nowcrew/daemon 0.6.77 → 0.6.78

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +27 -0
  2. package/dist/automatic-daemon-installer.js +44 -24
  3. package/dist/automatic-install-target.js +34 -9
  4. package/dist/computer-cli.js +43 -14
  5. package/dist/computer-profile.js +15 -2
  6. package/dist/computer-service.js +26 -5
  7. package/dist/config.js +28 -8
  8. package/dist/control-plane-url.js +3 -9
  9. package/dist/daemon-installation-lease.js +7 -4
  10. package/dist/daemon-local-home.js +108 -0
  11. package/dist/daemon-migration-controller.js +15 -4
  12. package/dist/daemon-registry.js +20 -0
  13. package/dist/daemon-update-controller.js +1 -0
  14. package/dist/daemon-update-eligibility.js +17 -7
  15. package/dist/daemon-updater.js +26 -26
  16. package/dist/execution-runner.js +7 -7
  17. package/dist/host-execution-coordinator.js +4 -3
  18. package/dist/local-executor.js +23 -17
  19. package/dist/local-memory/runtime.js +5 -3
  20. package/dist/machine-info.js +3 -0
  21. package/dist/main.js +13 -2
  22. package/dist/managed-service-diagnostics.js +1 -0
  23. package/dist/managed-service-registry.js +12 -7
  24. package/dist/managed-service-startup.js +4 -1
  25. package/dist/npm-command.js +9 -0
  26. package/dist/project-skills/controller.js +6 -71
  27. package/dist/project-skills/execution-adapter.js +7 -0
  28. package/dist/project-skills/execution-projection.js +163 -0
  29. package/dist/project-skills/initialized-reconciler.js +10 -8
  30. package/dist/project-skills/legacy-cleanup.js +224 -0
  31. package/dist/project-skills/machine-hello-publisher.js +20 -0
  32. package/dist/project-skills/reconciler.js +114 -500
  33. package/dist/project-skills/resolution-records.js +63 -0
  34. package/dist/project-skills/runtime-launch.js +0 -42
  35. package/dist/project-skills/runtime-warning.js +1 -3
  36. package/dist/project-skills/scanner.js +1 -17
  37. package/dist/project-skills/startup-cleanup.js +24 -0
  38. package/dist/runner.js +2 -1
  39. package/dist/serve.js +49 -49
  40. package/dist/slog.js +14 -1
  41. package/dist/storage-control.js +12 -1
  42. package/dist/storage-identity.js +15 -1
  43. package/dist/storage-migration-controller.js +25 -4
  44. package/dist/storage-ownership.js +55 -0
  45. package/dist/storage-runtime-controller.js +29 -7
  46. package/dist/storage-runtime-readiness.js +26 -4
  47. package/dist/storage-runtime.js +18 -2
  48. package/dist/storage-status-delivery.js +4 -0
  49. package/dist/storage-status-replay.js +16 -1
  50. package/dist/storage-worker-executor.js +1 -0
  51. package/dist/storage-worker-operation.js +4 -0
  52. package/package.json +1 -1
@@ -3,6 +3,7 @@ import { realpathSync } from "node:fs";
3
3
  import { mkdir, open } from "node:fs/promises";
4
4
  import { homedir } from "node:os";
5
5
  import { resolve } from "node:path";
6
+ import { resolveDaemonLocalStateRoot } from "./daemon-local-home.js";
6
7
  import { defaultProcessController } from "./execution-journal.js";
7
8
  import { createJournalLease, createJournalLeaseRegistry, inspectJournalLock, } from "./execution-journal-lock.js";
8
9
  const registry = createJournalLeaseRegistry();
@@ -18,9 +19,10 @@ function canonicalPrefix(npmPrefix) {
18
19
  return absolute;
19
20
  }
20
21
  }
21
- function leaseDirectory(npmPrefix, userHome) {
22
+ function leaseDirectory(npmPrefix, userHome, localHome) {
22
23
  const fingerprint = createHash("sha256").update(resolve(npmPrefix)).digest("hex");
23
- return resolve(userHome, ".crew", "daemon", "managed-services", "installation-leases", fingerprint);
24
+ const root = localHome ?? resolveDaemonLocalStateRoot({ userHome, platform: process.platform });
25
+ return resolve(root, "daemon", "managed-services", "installation-leases", fingerprint);
24
26
  }
25
27
  async function syncDirectory(path) {
26
28
  if (process.platform === "win32")
@@ -35,7 +37,8 @@ async function syncDirectory(path) {
35
37
  }
36
38
  export async function acquireDaemonInstallationLease(npmPrefix, options = {}) {
37
39
  const normalizedPrefix = canonicalPrefix(npmPrefix);
38
- const directory = leaseDirectory(normalizedPrefix, options.userHome ?? homedir());
40
+ const userHome = options.userHome ?? homedir();
41
+ const directory = leaseDirectory(normalizedPrefix, userHome, options.localHome);
39
42
  await mkdir(directory, { recursive: true, mode: 0o700 });
40
43
  const lease = createJournalLease({
41
44
  directory,
@@ -75,7 +78,7 @@ export function daemonInstallationLeaseHeld(npmPrefix) {
75
78
  export async function inspectDaemonInstallationLease(npmPrefix, options = {}) {
76
79
  const normalizedPrefix = canonicalPrefix(npmPrefix);
77
80
  return inspectJournalLock({
78
- directory: leaseDirectory(normalizedPrefix, options.userHome ?? homedir()),
81
+ directory: leaseDirectory(normalizedPrefix, options.userHome ?? homedir(), options.localHome),
79
82
  inspectIdentity: options.inspectIdentity ?? defaultProcessController.inspectIdentity,
80
83
  });
81
84
  }
@@ -0,0 +1,108 @@
1
+ import { existsSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { posix, win32 } from "node:path";
4
+ const PROFILE = /^[a-z0-9][a-z0-9_-]{0,47}$/u;
5
+ const CONTROL = /[\0\r\n]/u;
6
+ const pathsFor = (platform) => platform === "win32" ? win32 : posix;
7
+ export function parseDaemonLocalHomeMode(value) {
8
+ if (value === undefined || value.trim() === "")
9
+ return "auto";
10
+ const normalized = value.trim().toLowerCase();
11
+ if (normalized === "auto" || normalized === "nowwork" || normalized === "legacy")
12
+ return normalized;
13
+ throw new Error("NOWWORK_LOCAL_HOME_MODE must be auto, nowwork or legacy");
14
+ }
15
+ function absoluteRoot(value, userHome, platform) {
16
+ const paths = pathsFor(platform);
17
+ if (value.length === 0 || CONTROL.test(value))
18
+ throw new Error("NOWWORK_LOCAL_HOME must be a non-empty single-line path");
19
+ const expanded = value === "~"
20
+ ? userHome
21
+ : value.startsWith("~/") || (platform === "win32" && value.startsWith("~\\"))
22
+ ? paths.join(userHome, value.slice(2))
23
+ : value;
24
+ if (!paths.isAbsolute(expanded))
25
+ throw new Error("NOWWORK_LOCAL_HOME must be absolute or start with '~/'");
26
+ return paths.normalize(expanded);
27
+ }
28
+ function legacyDaemonDirectory(profile) {
29
+ if (profile === "in" || profile === "production")
30
+ return "daemon";
31
+ if (profile === "dev")
32
+ return "daemon-dev";
33
+ if (profile.startsWith("nw-"))
34
+ return `daemon-${profile.slice(3)}`;
35
+ return `daemon-${profile}`;
36
+ }
37
+ function resolution(mode, root, profile, platform, source) {
38
+ const paths = pathsFor(platform);
39
+ return Object.freeze({
40
+ mode,
41
+ root,
42
+ daemonHome: mode === "nowwork" ? paths.join(root, "config") : paths.join(root, legacyDaemonDirectory(profile)),
43
+ runtimePrefix: paths.join(root, "daemon-runtimes", profile),
44
+ source,
45
+ });
46
+ }
47
+ export function resolveDaemonLocalHome(input) {
48
+ if (!PROFILE.test(input.profile))
49
+ throw new Error("daemon profile is invalid");
50
+ const mode = parseDaemonLocalHomeMode(input.mode);
51
+ const paths = pathsFor(input.platform);
52
+ const nowworkRoot = absoluteRoot(input.explicit ?? paths.join(input.userHome, ".nowwork"), input.userHome, input.platform);
53
+ const legacyRoot = absoluteRoot(paths.join(input.userHome, ".crew"), input.userHome, input.platform);
54
+ if (input.explicit !== undefined && mode !== "legacy") {
55
+ return resolution("nowwork", nowworkRoot, input.profile, input.platform, "explicit");
56
+ }
57
+ if (mode === "nowwork")
58
+ return resolution("nowwork", nowworkRoot, input.profile, input.platform, "nowwork_forced");
59
+ if (mode === "legacy")
60
+ return resolution("legacy", legacyRoot, input.profile, input.platform, "legacy_forced");
61
+ const exists = input.exists ?? existsSync;
62
+ const nowwork = resolution("nowwork", nowworkRoot, input.profile, input.platform, "auto_nowwork_existing");
63
+ const legacy = resolution("legacy", legacyRoot, input.profile, input.platform, "auto_legacy_existing");
64
+ const hasNowwork = exists(nowwork.runtimePrefix) || exists(paths.join(nowwork.daemonHome, "profiles", `${input.profile}.json`));
65
+ const hasLegacy = exists(legacy.runtimePrefix) || exists(paths.join(legacy.daemonHome, "profiles", `${input.profile}.json`));
66
+ if (hasNowwork && hasLegacy) {
67
+ throw new Error(`Daemon local home conflict: profile '${input.profile}' exists under both .crew and .nowwork`);
68
+ }
69
+ if (hasNowwork)
70
+ return nowwork;
71
+ if (hasLegacy)
72
+ return legacy;
73
+ return resolution("nowwork", nowworkRoot, input.profile, input.platform, "auto_new");
74
+ }
75
+ /**
76
+ * Resolve the local machine state root shared by service metadata, leases,
77
+ * host coordination and offline logs. It intentionally stays separate from
78
+ * NOWWORK_HOME, which may point at a NAS persistent data root.
79
+ */
80
+ export function resolveDaemonLocalStateRoot(input) {
81
+ const paths = pathsFor(input.platform);
82
+ const mode = parseDaemonLocalHomeMode(input.mode);
83
+ const nowworkRoot = absoluteRoot(input.explicit ?? paths.join(input.userHome, ".nowwork"), input.userHome, input.platform);
84
+ const legacyRoot = absoluteRoot(paths.join(input.userHome, ".crew"), input.userHome, input.platform);
85
+ if (mode === "legacy")
86
+ return legacyRoot;
87
+ if (input.explicit !== undefined || mode === "nowwork")
88
+ return nowworkRoot;
89
+ const exists = input.exists ?? existsSync;
90
+ // Once the new root exists, shared state must follow it even while the
91
+ // legacy root is retained as a rollback source.
92
+ if (exists(nowworkRoot))
93
+ return nowworkRoot;
94
+ if (exists(legacyRoot))
95
+ return legacyRoot;
96
+ return nowworkRoot;
97
+ }
98
+ export function resolveDaemonLocalStateRootFromEnv(input = {}) {
99
+ const environment = input.environment ?? process.env;
100
+ return resolveDaemonLocalStateRoot({
101
+ userHome: input.userHome ?? homedir(),
102
+ platform: input.platform ?? process.platform,
103
+ ...(environment.NOWWORK_LOCAL_HOME === undefined ? {} : { explicit: environment.NOWWORK_LOCAL_HOME }),
104
+ ...(environment.NOWWORK_LOCAL_HOME_MODE === undefined && environment.NOWWORK_STORAGE_MODE === undefined
105
+ ? {}
106
+ : { mode: environment.NOWWORK_LOCAL_HOME_MODE ?? environment.NOWWORK_STORAGE_MODE }),
107
+ });
108
+ }
@@ -21,6 +21,9 @@ const MigrationMessageSchema = z.object({
21
21
  function migrationBackupRoot(input, id) {
22
22
  return resolve(input.backupRoot ?? resolve(input.userHome ?? homedir(), ".crew", "migration-backups"), id);
23
23
  }
24
+ function legacyLocalHome(input) {
25
+ return resolve(input.userHome ?? homedir(), ".crew");
26
+ }
24
27
  async function assertEmptyOrMissing(path) {
25
28
  try {
26
29
  const entries = await readdir(path);
@@ -77,10 +80,10 @@ async function backupAndCopy(input, id, targetHome, targetAgents, service) {
77
80
  }
78
81
  async function cleanupCurrentServiceDefault(spec, input) {
79
82
  const userHome = input.userHome ?? homedir();
80
- const records = await readManagedServiceRegistry({ userHome });
83
+ const records = await readManagedServiceRegistry({ userHome, localHome: legacyLocalHome(input) });
81
84
  const record = records.find((candidate) => candidate.profile === spec.profile && candidate.daemonHome === input.currentDaemonHome);
82
85
  if (record !== undefined && (spec.platform === "darwin" || spec.platform === "linux")) {
83
- await uninstallManagedService({ spec, record, runner: systemCommandRunner, registry: { userHome } });
86
+ await uninstallManagedService({ spec, record, runner: systemCommandRunner, registry: { userHome, localHome: legacyLocalHome(input) } });
84
87
  return;
85
88
  }
86
89
  // Windows and legacy descriptors are handled by the normal CLI/service lifecycle.
@@ -125,7 +128,15 @@ export function createDaemonMigrationController(input) {
125
128
  if (currentProfile.machineToken !== input.machineToken)
126
129
  throw new Error("active profile token does not match this daemon");
127
130
  currentRegistryUrl = currentProfile.registryUrl;
128
- const target = resolveAutomaticInstallTarget({ serverUrl: input.serverUrl, requestedProfile: message.targetProfile, userHome, platform });
131
+ const target = resolveAutomaticInstallTarget({
132
+ serverUrl: input.serverUrl,
133
+ requestedProfile: message.targetProfile,
134
+ userHome,
135
+ platform,
136
+ // This controller migrates the legacy service layout; the new local-home
137
+ // standard is handled by the storage migration controller.
138
+ requestedLocalHomeMode: "legacy",
139
+ });
129
140
  if (!isAbsolute(message.targetDaemonHome) || !isAbsolute(message.targetAgentsRoot)
130
141
  || resolve(message.targetDaemonHome) !== resolve(target.daemonHome)
131
142
  || resolve(message.targetAgentsRoot) !== resolve(target.agentsRoot))
@@ -140,7 +151,7 @@ export function createDaemonMigrationController(input) {
140
151
  throw new Error("current managed service identity is not exact and running");
141
152
  currentService = { installed: exact.installed, running: exact.running };
142
153
  if (platform === "darwin" || platform === "linux") {
143
- const records = await (input.readManagedRecords ?? (() => readManagedServiceRegistry({ userHome })))();
154
+ const records = await (input.readManagedRecords ?? (() => readManagedServiceRegistry({ userHome, localHome: legacyLocalHome(input) })))();
144
155
  managedRecord = records.find((record) => record.serviceId === currentSpec.id && record.profile === input.profileName && record.daemonHome === input.currentDaemonHome) ?? null;
145
156
  if (managedRecord === null)
146
157
  throw new Error("current managed service registry record is missing");
@@ -0,0 +1,20 @@
1
+ import { normalizeDaemonNpmRegistry } from "./npm-registry.js";
2
+ export const DAEMON_PUBLIC_NPM_REGISTRY = normalizeDaemonNpmRegistry();
3
+ export const DAEMON_PUBLIC_NPM_MIRROR = "https://registry.npmmirror.com";
4
+ /** Keep every daemon package operation on the same ordered, channel-safe sources. */
5
+ export function daemonRegistryCandidates(input) {
6
+ try {
7
+ const requested = input.registryUrl ?? input.env?.CREW_NPM_REGISTRY;
8
+ const configured = requested === undefined
9
+ ? DAEMON_PUBLIC_NPM_REGISTRY
10
+ : normalizeDaemonNpmRegistry(requested, { allowLoopbackHttp: input.betaUpdate });
11
+ return [...new Set([
12
+ configured,
13
+ DAEMON_PUBLIC_NPM_REGISTRY,
14
+ ...(input.betaUpdate ? [DAEMON_PUBLIC_NPM_MIRROR] : []),
15
+ ])];
16
+ }
17
+ catch {
18
+ return null;
19
+ }
20
+ }
@@ -30,6 +30,7 @@ export function createDaemonUpdateController(deps) {
30
30
  targetVersion: message.targetVersion,
31
31
  packageRoot: eligibility.packageRoot,
32
32
  npmPrefix: eligibility.npmPrefix,
33
+ ...(eligibility.registryUrl === undefined ? {} : { registryUrl: eligibility.registryUrl }),
33
34
  onInstalling: () => {
34
35
  deps.sendStatus({ type: "daemon:update-status", updateId: message.updateId, status: "installing" });
35
36
  },
@@ -9,6 +9,7 @@ import { daemonGlobalInstallation } from "./daemon-installation.js";
9
9
  import { daemonInstallationLeaseHeld } from "./daemon-installation-lease.js";
10
10
  import { inspectLegacyServiceInstallation } from "./legacy-service-installation.js";
11
11
  import { inspectWindowsServiceUpdateScope } from "./windows-scheduled-task.js";
12
+ import { resolveDaemonLocalStateRoot } from "./daemon-local-home.js";
12
13
  import { assertRegistryCoversDescriptors, listManagedServiceDescriptorPaths, managedServiceIdentityMatches, readManagedServiceRegistry, serviceDescriptorSha256, } from "./managed-service-registry.js";
13
14
  export async function managedDaemonCapabilities(eligibility) {
14
15
  try {
@@ -20,24 +21,31 @@ export async function managedDaemonCapabilities(eligibility) {
20
21
  return [];
21
22
  }
22
23
  }
23
- function defaults() {
24
+ function defaults(profileName) {
25
+ const userHome = homedir();
26
+ const localHome = resolveDaemonLocalStateRoot({
27
+ userHome,
28
+ platform: process.platform,
29
+ explicit: process.env.NOWWORK_LOCAL_HOME,
30
+ mode: process.env.NOWWORK_LOCAL_HOME_MODE ?? process.env.NOWWORK_STORAGE_MODE,
31
+ });
24
32
  return {
25
33
  platform: process.platform,
26
- profileHome: daemonHome(),
27
- userHome: homedir(),
34
+ profileHome: daemonHome(process.env, profileName),
35
+ userHome,
28
36
  uid: process.getuid?.(),
29
37
  nodePath: process.execPath,
30
38
  entryPath: builtDaemonEntry(),
31
39
  loadProfile,
32
- readManagedServices: () => readManagedServiceRegistry(),
33
- listManagedDescriptorPaths: () => listManagedServiceDescriptorPaths(process.platform, homedir()),
40
+ readManagedServices: () => readManagedServiceRegistry({ userHome, localHome }),
41
+ listManagedDescriptorPaths: () => listManagedServiceDescriptorPaths(process.platform, userHome),
34
42
  readServiceDescriptor,
35
43
  serviceStatus,
36
44
  assertWritable: (path) => access(path, constants.W_OK),
37
45
  installationLeaseHeld: daemonInstallationLeaseHeld,
38
46
  installationRealpath: realpathSync.native,
39
47
  inspectWindowsServiceUpdateScope,
40
- inspectLegacyDescriptorInstallation: (descriptorPath) => inspectLegacyServiceInstallation(descriptorPath, process.platform, homedir(), process.getuid?.()),
48
+ inspectLegacyDescriptorInstallation: (descriptorPath) => inspectLegacyServiceInstallation(descriptorPath, process.platform, userHome, process.getuid?.()),
41
49
  };
42
50
  }
43
51
  async function registryCoversUpdateScope(services, descriptorPaths, targetDescriptorPath, npmPrefix, inspectLegacyDescriptorInstallation) {
@@ -57,7 +65,7 @@ async function registryCoversUpdateScope(services, descriptorPaths, targetDescri
57
65
  return true;
58
66
  }
59
67
  export async function detectDaemonUpdateEligibility(profileName, overrides = {}) {
60
- const deps = { ...defaults(), ...overrides };
68
+ const deps = { ...defaults(profileName), ...overrides };
61
69
  if (deps.platform !== "darwin" && deps.platform !== "linux" && deps.platform !== "win32") {
62
70
  return { eligible: false, reason: "unsupported_platform" };
63
71
  }
@@ -109,6 +117,7 @@ export async function detectDaemonUpdateEligibility(profileName, overrides = {})
109
117
  eligible: true,
110
118
  profileName,
111
119
  ...installation,
120
+ ...(profile.registryUrl === undefined ? {} : { registryUrl: profile.registryUrl }),
112
121
  serviceSpec: { ...spec, runtimeProcessIdentity: scope.processIdentity },
113
122
  };
114
123
  }
@@ -188,6 +197,7 @@ export async function detectDaemonUpdateEligibility(profileName, overrides = {})
188
197
  eligible: true,
189
198
  profileName,
190
199
  ...installation,
200
+ ...(profile.registryUrl === undefined ? {} : { registryUrl: profile.registryUrl }),
191
201
  serviceSpec: selectedSpec,
192
202
  };
193
203
  }
@@ -1,25 +1,10 @@
1
1
  import { readFile } from "node:fs/promises";
2
- import { posix, resolve, win32 } from "node:path";
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
- const PUBLIC_NPM_REGISTRY = normalizeDaemonNpmRegistry();
8
- function configuredRegistries(betaUpdate) {
9
- try {
10
- const configured = process.env.CREW_NPM_REGISTRY === undefined
11
- ? PUBLIC_NPM_REGISTRY
12
- : normalizeDaemonNpmRegistry(process.env.CREW_NPM_REGISTRY, { allowLoopbackHttp: betaUpdate });
13
- return betaUpdate ? [...new Set([configured, PUBLIC_NPM_REGISTRY])] : [configured];
14
- }
15
- catch {
16
- return null;
17
- }
18
- }
19
- function npmCommand(platform, nodePath) {
20
- const paths = platform === "win32" ? win32 : posix;
21
- return paths.join(paths.dirname(nodePath), platform === "win32" ? "npm.cmd" : "npm");
22
- }
6
+ import { daemonRegistryCandidates } from "./daemon-registry.js";
7
+ import { npmInvocation } from "./npm-command.js";
23
8
  function registryArgs(registry) {
24
9
  return ["--registry", registry];
25
10
  }
@@ -51,10 +36,11 @@ async function readPackageVersion(packageRoot) {
51
36
  const body = JSON.parse(await readFile(resolve(packageRoot, "package.json"), "utf8"));
52
37
  return typeof body.version === "string" ? body.version : "";
53
38
  }
54
- async function restoreInstalledVersion(input, runner, command, version, registryArgs) {
39
+ async function restoreInstalledVersion(input, runner, command, commandPrefix, version, registryArgs) {
55
40
  if (!isDaemonReleaseVersion(version))
56
41
  return false;
57
42
  const result = await runner(command, [
43
+ ...commandPrefix,
58
44
  "install",
59
45
  "--global",
60
46
  "--prefix",
@@ -110,7 +96,9 @@ export async function installExactDaemonUpdate(input) {
110
96
  }
111
97
  const runner = input.runner ?? systemCommandRunner;
112
98
  const platform = input.platform ?? process.platform;
113
- const command = npmCommand(platform, input.nodePath ?? process.execPath);
99
+ const npm = npmInvocation(platform, input.nodePath ?? process.execPath);
100
+ const command = npm.command;
101
+ const commandPrefix = npm.argsPrefix;
114
102
  let previousVersion = "";
115
103
  try {
116
104
  previousVersion = await (input.readInstalledVersion ?? readPackageVersion)(input.packageRoot);
@@ -124,8 +112,12 @@ export async function installExactDaemonUpdate(input) {
124
112
  await release();
125
113
  return { ok: false, errorCode: "install_failed" };
126
114
  }
127
- const registries = configuredRegistries(daemonReleaseChannel(previousVersion) === "beta"
128
- || daemonReleaseChannel(input.targetVersion) === "beta");
115
+ const registries = daemonRegistryCandidates({
116
+ betaUpdate: daemonReleaseChannel(previousVersion) === "beta"
117
+ || daemonReleaseChannel(input.targetVersion) === "beta",
118
+ ...(input.registryUrl === undefined ? {} : { registryUrl: input.registryUrl }),
119
+ env: process.env,
120
+ });
129
121
  if (registries === null) {
130
122
  await release();
131
123
  return { ok: false, errorCode: "install_failed" };
@@ -139,10 +131,12 @@ export async function installExactDaemonUpdate(input) {
139
131
  }
140
132
  let selectedRegistryArgs = [];
141
133
  let installed = false;
142
- let authFailed = false;
134
+ let authFailures = 0;
135
+ let failedAttempts = 0;
143
136
  for (const registry of registries) {
144
137
  const args = registryArgs(registry);
145
138
  const result = await runner(command, [
139
+ ...commandPrefix,
146
140
  "install",
147
141
  "--global",
148
142
  "--prefix",
@@ -158,12 +152,18 @@ export async function installExactDaemonUpdate(input) {
158
152
  installed = true;
159
153
  break;
160
154
  }
155
+ failedAttempts += 1;
161
156
  if (result !== null && isRegistryAuthFailure(result.stderr))
162
- authFailed = true;
157
+ authFailures += 1;
163
158
  }
164
159
  if (!installed) {
165
160
  await release();
166
- return { ok: false, errorCode: authFailed ? "registry_auth_failed" : "install_failed" };
161
+ return {
162
+ ok: false,
163
+ errorCode: failedAttempts > 0 && authFailures === failedAttempts
164
+ ? "registry_auth_failed"
165
+ : "install_failed",
166
+ };
167
167
  }
168
168
  let installedVersion = "";
169
169
  try {
@@ -173,7 +173,7 @@ export async function installExactDaemonUpdate(input) {
173
173
  // Version verification is authoritative; unreadable package metadata is a mismatch.
174
174
  }
175
175
  if (installedVersion !== input.targetVersion) {
176
- const restored = await restoreInstalledVersion(input, runner, command, previousVersion, selectedRegistryArgs);
176
+ const restored = await restoreInstalledVersion(input, runner, command, commandPrefix, previousVersion, selectedRegistryArgs);
177
177
  if (!restored) {
178
178
  console.error(`[daemon-update] version verification failed; restore of ${previousVersion} was unsuccessful`);
179
179
  }
@@ -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";
@@ -16,8 +17,7 @@ import { HostReservationCancelledError } from "./host-execution-coordinator.js";
16
17
  import { supervisorLaunch } from "./supervised-runtime.js";
17
18
  import { appendAgentMemoryContext } from "./agent-memory/policy.js";
18
19
  import { ProjectSkillRuntimeOwnershipUnverifiedError, } from "./project-skills/reconciler.js";
19
- import { projectSkillExecutionProjection, projectSkillProjectionErrorCode } from "./project-skills/execution-adapter.js";
20
- import { redactProjectSkillRuntimeRootError, } from "./project-skills/runtime-launch.js";
20
+ import { projectSkillExecutionProjection, projectSkillProjectionApplies, projectSkillProjectionErrorCode, } from "./project-skills/execution-adapter.js";
21
21
  import { createProjectRegistry } from "./project-skills/registry.js";
22
22
  import { ProjectDependencyError, } from "./project-dependencies.js";
23
23
  import { ProjectContextUnavailableError, resolveProjectContext } from "./project-workspaces/resolver.js";
@@ -524,7 +524,7 @@ export async function runExecution(config, input, dependencies) {
524
524
  if (dependencies.abilityRelease !== undefined) {
525
525
  await cancellable(dependencies.abilityRelease.apply(config.agentsRoot, spec.agent.handle, spec.agent.abilityRelease), dependencies.cancellation);
526
526
  }
527
- const reposRoot = config.reposRoot ?? join(config.agentsRoot, "repos");
527
+ const reposRoot = resolveReposRoot(config.agentsRoot, config.reposRoot);
528
528
  const preparedProjectDependencies = await cancellable(dependencies.projectDependencies.prepare({
529
529
  trainingRoot: join(config.agentsRoot, spec.agent.handle, "training"),
530
530
  reposRoot,
@@ -736,7 +736,7 @@ export async function runExecution(config, input, dependencies) {
736
736
  }
737
737
  catch (error) {
738
738
  await proveActiveSupervisorStopped(error, true);
739
- throw redactProjectSkillRuntimeRootError(error, root);
739
+ throw error;
740
740
  }
741
741
  };
742
742
  return safeLaunch;
@@ -863,7 +863,7 @@ export async function runExecution(config, input, dependencies) {
863
863
  token: credential.token,
864
864
  agentId: credential.agentId,
865
865
  agentsRoot: config.agentsRoot,
866
- ...(config.reposRoot === undefined ? {} : { reposRoot: config.reposRoot }),
866
+ reposRoot: resolveReposRoot(config.agentsRoot, config.reposRoot),
867
867
  ...(config.qaReportRoot === undefined ? {} : { qaReportRoot: config.qaReportRoot }),
868
868
  cliPath: config.cliPath,
869
869
  providerConfig,
@@ -973,14 +973,14 @@ export async function runExecution(config, input, dependencies) {
973
973
  await dependencies.cancellation?.waitForStop();
974
974
  }
975
975
  catch (stopError) {
976
- if (spec.agent.projectSkillBindingGeneration !== undefined) {
976
+ if (projectSkillProjectionApplies(spec)) {
977
977
  throw new ProjectSkillRuntimeOwnershipUnverifiedError();
978
978
  }
979
979
  throw stopError;
980
980
  }
981
981
  }
982
982
  else {
983
- await proveActiveSupervisorStopped(error, spec.agent.projectSkillBindingGeneration !== undefined);
983
+ await proveActiveSupervisorStopped(error, projectSkillProjectionApplies(spec));
984
984
  }
985
985
  const failureCompletion = cancelled
986
986
  ? ExecutionCompletedSchema.parse({
@@ -5,6 +5,7 @@ import { randomUUID } from "node:crypto";
5
5
  import { defaultProcessController } from "./execution-journal.js";
6
6
  import { createJournalLease, createJournalLeaseRegistry, JournalLockedError, } from "./execution-journal-lock.js";
7
7
  import { dslog } from "./slog.js";
8
+ import { resolveDaemonLocalStateRoot } from "./daemon-local-home.js";
8
9
  const HOST_EXECUTION_SLOT_COUNT = 10;
9
10
  const HOST_STARTUP_SLOT_COUNT = 10;
10
11
  const HOST_STARTUP_GAP_MS = 500;
@@ -16,10 +17,10 @@ export class HostReservationCancelledError extends Error {
16
17
  super(message);
17
18
  }
18
19
  }
19
- export function defaultHostExecutionCoordinatorRoot(userHome = homedir()) {
20
+ export function defaultHostExecutionCoordinatorRoot(userHome = homedir(), localHome) {
20
21
  // Deliberately independent of CREW_DAEMON_HOME and agentsRoot: every profile
21
22
  // owned by this OS user must share the same physical-compute safety boundary.
22
- return resolve(userHome, ".crew", "daemon", "compute-execution");
23
+ return resolve(localHome ?? resolveDaemonLocalStateRoot({ userHome, platform: process.platform }), "daemon", "compute-execution");
23
24
  }
24
25
  async function syncDirectory(path) {
25
26
  if (process.platform === "win32")
@@ -132,7 +133,7 @@ async function writeLaunchTimestamp(path, value) {
132
133
  }
133
134
  }
134
135
  export function createHostExecutionCoordinator(options = {}) {
135
- const root = resolve(options.root ?? defaultHostExecutionCoordinatorRoot());
136
+ const root = resolve(options.root ?? defaultHostExecutionCoordinatorRoot(options.userHome, options.localHome));
136
137
  const executionSlots = options.executionSlots ?? HOST_EXECUTION_SLOT_COUNT;
137
138
  const startupSlots = options.startupSlots ?? HOST_STARTUP_SLOT_COUNT;
138
139
  const startupGapMs = options.startupGapMs ?? HOST_STARTUP_GAP_MS;
@@ -24,8 +24,9 @@ import { isRuntimeReadyEvent, isRuntimeRunningEvent, } from "./runtime-startup-g
24
24
  import { dslog } from "./slog.js";
25
25
  import { boundedDiagnosticJsonArray } from "./diagnostic-json.js";
26
26
  import { ProjectProjectionError, } from "./project-skills/reconciler.js";
27
+ import { projectSkillProjectionApplies } from "./project-skills/execution-adapter.js";
27
28
  import { formatProjectSkillRuntimeWarning } from "./project-skills/runtime-warning.js";
28
- import { projectSkillRuntimeDirectories, redactProjectSkillRuntimeRootText, redactProjectSkillRuntimeRootValue, } from "./project-skills/runtime-launch.js";
29
+ import { projectSkillRuntimeDirectories, } from "./project-skills/runtime-launch.js";
29
30
  import { logIdentityRosterRuntimePrepared } from "./identity-roster-telemetry.js";
30
31
  import { diffMemoryPruneNotes, evaluateMemoryPrunePostcondition, inspectMemoryPruneFilesWithinDeadline, logMemoryPruneDiagnosticsFailure, memoryPruneSnapshotFields, parseMemoryPruneTraceId, } from "./memory-prune-diagnostics.js";
31
32
  import { createLocalMemoryTelemetry, logLocalMemoryContextPrepareFailure, logLocalMemoryDiagnosticsFailure, } from "./local-memory-telemetry.js";
@@ -466,6 +467,11 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
466
467
  CREW_TOKEN: input.launch.token,
467
468
  CREW_CHANNEL: input.channelId,
468
469
  CREW_HOME: workspace.dir,
470
+ // Keep the daemon-selected storage root authoritative for runtimes.
471
+ // In profile-backed NowWork sessions the daemon config may differ from
472
+ // the daemon process environment, so inheriting CREW_AGENTS_ROOT is not
473
+ // sufficient for QA/project diagnostics.
474
+ CREW_AGENTS_ROOT: input.launch.agentsRoot,
469
475
  ...(input.launch.reposRoot === undefined ? {} : { NOWWORK_REPOS_ROOT: input.launch.reposRoot }),
470
476
  ...(input.launch.qaReportRoot === undefined ? {} : { QA_REPORT_HUB: input.launch.qaReportRoot }),
471
477
  // Expose only the training root; ability-specific workspace layout stays
@@ -520,12 +526,10 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
520
526
  });
521
527
  if (dependencies.cancellation?.isRequested())
522
528
  throw new RuntimeCancelledError();
523
- let activeProjectSkillRuntimeRoot;
524
529
  const abilityContext = input.abilityRelease !== undefined && dependencies.abilityRelease !== undefined
525
530
  ? await dependencies.abilityRelease.prepareAndLaunch(input.launch.agentsRoot, input.handle, input.abilityRelease, async (context) => context)
526
531
  : undefined;
527
532
  const prepareRuntimeLaunch = async (runtimeRoot) => {
528
- activeProjectSkillRuntimeRoot = runtimeRoot;
529
533
  const effectiveSystemPrompt = abilityContext === undefined
530
534
  ? systemPrompt
531
535
  : `${systemPrompt}\n\n${abilityContext.prompt}`;
@@ -676,11 +680,17 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
676
680
  return launchRuntime(launchRequest);
677
681
  };
678
682
  let child;
679
- if (input.projectSkills !== undefined && dependencies.projectSkills !== undefined) {
683
+ const projectionBindings = projectSkillProjectionApplies({
684
+ agent: { projectSkills: input.projectSkills },
685
+ runtime: input.runtime,
686
+ })
687
+ ? input.projectSkills
688
+ : undefined;
689
+ if (projectionBindings !== undefined && dependencies.projectSkills !== undefined) {
680
690
  const launchOwner = dependencies.projectSkillLeaseSafeLaunchOwner;
681
691
  if (launchOwner === undefined)
682
692
  throw new ProjectProjectionError("skill_projection_failed");
683
- child = await dependencies.projectSkills.prepareAndLaunch(input.launch.agentsRoot, input.executionId, input.handle, input.projectSkills, launchOwner.claim(launchPreparedRuntime), (warning) => callbacks.onConsole?.({
693
+ child = await dependencies.projectSkills.prepareAndLaunch(input.launch.agentsRoot, input.executionId, input.handle, projectionBindings, launchOwner.claim(launchPreparedRuntime), (warning) => callbacks.onConsole?.({
684
694
  stream: "system",
685
695
  text: formatProjectSkillRuntimeWarning(warning),
686
696
  }));
@@ -813,8 +823,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
813
823
  usage = meta.usage;
814
824
  if (meta.model)
815
825
  observedModel = meta.model;
816
- for (const rawActivity of normalizeEvent(event)) {
817
- const activity = redactProjectSkillRuntimeRootValue(rawActivity, activeProjectSkillRuntimeRoot);
826
+ for (const activity of normalizeEvent(event)) {
818
827
  if (activity.kind === "sending")
819
828
  sentViaCrew = true;
820
829
  activities.push(activity);
@@ -822,19 +831,18 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
822
831
  }
823
832
  const extracted = extractFinalText(event);
824
833
  if (extracted) {
825
- const safeExtracted = redactProjectSkillRuntimeRootText(extracted, activeProjectSkillRuntimeRoot);
826
834
  const incremental = typeof event === "object" && event !== null
827
835
  && "type" in event && (event.type === "kimi.acp.text_delta"
828
836
  || event.type === "hermes.acp.text_delta"
829
837
  || event.type === "deepseek-harness.acp.text_delta"
830
838
  || event.type === "opencode.text_delta");
831
- finalText = incremental ? `${finalText ?? ""}${safeExtracted}` : safeExtracted;
839
+ finalText = incremental ? `${finalText ?? ""}${extracted}` : extracted;
832
840
  }
833
841
  for (const text of decodeExternalOutputEvent(runtime.name, event, externalOutput)) {
834
- callbacks.onExternalOutput?.(redactProjectSkillRuntimeRootText(text, activeProjectSkillRuntimeRoot));
842
+ callbacks.onExternalOutput?.(text);
835
843
  }
836
844
  for (const chunk of consoleFormatter.format(event)) {
837
- callbacks.onConsole?.(redactProjectSkillRuntimeRootValue(chunk, activeProjectSkillRuntimeRoot));
845
+ callbacks.onConsole?.(chunk);
838
846
  }
839
847
  });
840
848
  let stderrTail = "";
@@ -856,7 +864,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
856
864
  })
857
865
  : null;
858
866
  child.stderr.on("data", (data) => {
859
- const text = redactProjectSkillRuntimeRootText(String(data), activeProjectSkillRuntimeRoot);
867
+ const text = String(data);
860
868
  process.stderr.write(text);
861
869
  stderrTail = (stderrTail + text).slice(-STDERR_TAIL_CAP);
862
870
  codexStartupStageParser?.push(text);
@@ -935,9 +943,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
935
943
  }
936
944
  const errorTail = [
937
945
  stderrTail.trim(),
938
- spawnError === undefined
939
- ? undefined
940
- : redactProjectSkillRuntimeRootText(spawnError, activeProjectSkillRuntimeRoot),
946
+ spawnError === undefined ? undefined : spawnError,
941
947
  terminationSignal ? `terminated by ${terminationSignal}` : undefined,
942
948
  ].filter(Boolean).join(" ").trim();
943
949
  const providerError = providerErrorFromExit(exitCode, errorTail);
@@ -989,11 +995,11 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
989
995
  : null,
990
996
  // 契约答案 = 整轮最后一个闭合的标签块(六个 runtime 语义一致的"后到者胜",半截块与
991
997
  // 叙述里的开标签不算数);从未闭合过时才回落到对 finalText 的宽容提取,保住"收尾被
992
- // 截断"这一老兜底。解码器存的是原文,这里补脱敏。注意这放宽了"externalAnswer 非空
998
+ // 截断"这一老兜底。注意这放宽了"externalAnswer 非空
993
999
  // ⇒ finalText 非空"的旧不变式;是否上报仍由 execution-runner 按 exitCode===0 把关。
994
1000
  externalAnswer: input.captureFinal
995
1001
  ? (externalOutput.lastClosedBlock !== null
996
- ? redactProjectSkillRuntimeRootText(externalOutput.lastClosedBlock, activeProjectSkillRuntimeRoot)
1002
+ ? externalOutput.lastClosedBlock
997
1003
  : finalText !== null ? extractExternalAnswer(finalText) : null)
998
1004
  : null,
999
1005
  sentViaCrew,
@@ -3,9 +3,11 @@ import { lstat, mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
  import { applyLocalMemoryResult, parseLocalMemoryResult } from "./write.js";
5
5
  export function isLocalMemoryV2Eligible(input) {
6
- return input.enabledForHandle
7
- && (input.localMemoryV2Initialized || (!input.agentHomePreexisted && input.memorySeedCreated))
8
- && input.memoryPrune;
6
+ // The allowlist is the rollout boundary. The marker and seed/pre-existing
7
+ // state are retained as diagnostics and for backward-compatible marker
8
+ // creation, but must not prevent an explicitly allowlisted Agent from using
9
+ // the structured memory-prune protocol.
10
+ return input.enabledForHandle && input.memoryPrune;
9
11
  }
10
12
  const LOCAL_MEMORY_V2_MARKER = "v2-enabled";
11
13
  export async function initializeLocalMemoryV2(agentDir) {