@nowcrew/daemon 0.6.76 → 0.6.77-beta.10

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 (50) hide show
  1. package/README.md +27 -0
  2. package/dist/agent-ability/resolver.js +1 -1
  3. package/dist/attachment-limits.js +4 -0
  4. package/dist/attachments.js +33 -27
  5. package/dist/automatic-daemon-installer.js +44 -24
  6. package/dist/automatic-install-target.js +34 -9
  7. package/dist/computer-cli.js +43 -14
  8. package/dist/computer-profile.js +15 -2
  9. package/dist/computer-service.js +26 -5
  10. package/dist/config.js +34 -6
  11. package/dist/daemon-installation-lease.js +7 -4
  12. package/dist/daemon-local-home.js +108 -0
  13. package/dist/daemon-migration-controller.js +15 -4
  14. package/dist/daemon-registry.js +20 -0
  15. package/dist/daemon-update-controller.js +1 -0
  16. package/dist/daemon-update-eligibility.js +17 -7
  17. package/dist/daemon-updater.js +56 -30
  18. package/dist/execution-protocol.js +5 -2
  19. package/dist/execution-runner.js +7 -3
  20. package/dist/host-execution-coordinator.js +4 -3
  21. package/dist/local-executor.js +7 -0
  22. package/dist/local-memory/runtime.js +5 -3
  23. package/dist/machine-info.js +2 -0
  24. package/dist/main.js +33 -14
  25. package/dist/managed-service-diagnostics.js +1 -0
  26. package/dist/managed-service-registry.js +12 -7
  27. package/dist/managed-service-startup.js +4 -1
  28. package/dist/npm-command.js +9 -0
  29. package/dist/project-dependencies.js +4 -4
  30. package/dist/project-skills/runtime-root-bootstrap.js +6 -0
  31. package/dist/project-skills/runtime-root-startup.js +2 -1
  32. package/dist/project-skills/serve-startup.js +23 -0
  33. package/dist/repository-identity.js +2 -0
  34. package/dist/runner.js +3 -0
  35. package/dist/runtime-capabilities.js +3 -1
  36. package/dist/serve.js +7 -6
  37. package/dist/slog.js +14 -1
  38. package/dist/storage-control.js +12 -1
  39. package/dist/storage-identity.js +15 -1
  40. package/dist/storage-layout.js +2 -1
  41. package/dist/storage-migration-controller.js +25 -4
  42. package/dist/storage-ownership.js +55 -0
  43. package/dist/storage-runtime-controller.js +29 -7
  44. package/dist/storage-runtime-readiness.js +26 -4
  45. package/dist/storage-runtime.js +18 -2
  46. package/dist/storage-status-delivery.js +4 -0
  47. package/dist/storage-status-replay.js +16 -1
  48. package/dist/storage-worker-executor.js +1 -0
  49. package/dist/storage-worker-operation.js +4 -0
  50. package/package.json +1 -1
package/dist/config.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /** daemon 配置:从 env 读取,解析 crew CLI 路径 (注入给 agent 的 wrapper 用)。 */
2
2
  import { fileURLToPath } from "node:url";
3
- import { dirname, resolve } from "node:path";
3
+ import { basename, dirname, join, resolve } from "node:path";
4
4
  import { homedir } from "node:os";
5
5
  import { createRequire } from "node:module";
6
6
  import { detectDaemonLang, translateDaemon } from "./i18n.js";
@@ -28,6 +28,23 @@ export const MIN_EXECUTION_EVENT_BYTES = 512;
28
28
  export const MAX_EXECUTION_TIMEOUT_MS = 2_147_483_647;
29
29
  export class ConfigError extends Error {
30
30
  }
31
+ /**
32
+ * Resolve the shared workspace repository root from the daemon's agent root.
33
+ *
34
+ * Standard NowWork storage keeps `agent-items`, `daemon`, `repos`, and the QA
35
+ * report hub as siblings. Legacy/standalone profiles may still point at an
36
+ * arbitrary agents directory, for which the historical `<agentsRoot>/repos`
37
+ * layout remains the compatible fallback.
38
+ */
39
+ export function resolveReposRoot(agentsRoot, configured) {
40
+ const explicit = configured?.trim();
41
+ if (explicit)
42
+ return resolve(explicit);
43
+ const resolvedAgentsRoot = resolve(agentsRoot);
44
+ return basename(resolvedAgentsRoot) === "agent-items"
45
+ ? join(dirname(resolvedAgentsRoot), "repos")
46
+ : join(resolvedAgentsRoot, "repos");
47
+ }
31
48
  function positiveIntegerEnv(env, field, fallback) {
32
49
  const raw = env[field];
33
50
  const value = raw === undefined ? fallback : Number(raw);
@@ -59,11 +76,11 @@ function defaultCliPath() {
59
76
  return resolve(here, "../../cli/dist/main.js");
60
77
  }
61
78
  }
62
- // 临时 dev 验收名单。测试完成后删除此常量,恢复未配置时默认关闭的行为。
79
+ // 临时 dev 验收名单。这里必须填写 Server 生成的 Agent handle,而不是显示名称或频道 ID。
80
+ // 测试完成后删除此常量,恢复未配置时默认关闭的行为。
63
81
  const TEMP_LOCAL_MEMORY_V2_TEST_HANDLES = [
64
- "memory-v2-test-a",
65
- "memory-v2-test-b",
66
- "memory-v2-test-c",
82
+ "94533bf3-b5b7-4a1e-9c41-6a1ab0a0e1ea", // memory-v2-test-a
83
+ "967ab9ce-724c-4f22-a664-2f855cd798af", // memory-v2-test-b
67
84
  ];
68
85
  function isTemporaryDevServer(serverUrl) {
69
86
  try {
@@ -134,10 +151,21 @@ export function loadConfig(env = process.env) {
134
151
  maxFirstOutputWaitMs: positiveIntegerEnv(env, "CREW_EXECUTION_MAX_FIRST_OUTPUT_WAIT_MS", DEFAULT_EXECUTION_LIMITS.maxFirstOutputWaitMs),
135
152
  firstOutputGraceMs: positiveIntegerEnv(env, "CREW_EXECUTION_FIRST_OUTPUT_GRACE_MS", DEFAULT_EXECUTION_LIMITS.firstOutputGraceMs),
136
153
  });
154
+ const agentsRoot = resolveAgentsRoot(env.CREW_AGENTS_ROOT, homedir());
155
+ const configuredReposRoot = env.NOWWORK_REPOS_ROOT?.trim();
156
+ const reposRoot = resolveReposRoot(agentsRoot, configuredReposRoot);
157
+ const configuredQaReportRoot = env.QA_REPORT_HUB?.trim();
158
+ const defaultQaReportRoot = configuredReposRoot
159
+ ? join(dirname(reposRoot), "qa-report-server-v2")
160
+ : join(dirname(agentsRoot), "qa-report-server-v2");
137
161
  return {
138
162
  serverUrl,
139
163
  machineToken,
140
- agentsRoot: resolveAgentsRoot(env.CREW_AGENTS_ROOT, homedir()),
164
+ agentsRoot,
165
+ reposRoot,
166
+ qaReportRoot: configuredQaReportRoot
167
+ ? resolve(configuredQaReportRoot)
168
+ : defaultQaReportRoot,
141
169
  cliPath: env.CREW_CLI_PATH ?? defaultCliPath(),
142
170
  runtimeBin: env.CREW_RUNTIME ?? "claude",
143
171
  codexHomeMode: codexHomeModeEnv(env),
@@ -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
  }
@@ -3,17 +3,13 @@ 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
- }
6
+ import { daemonRegistryCandidates } from "./daemon-registry.js";
7
+ import { npmInvocation } from "./npm-command.js";
8
+ function registryArgs(registry) {
9
+ return ["--registry", registry];
10
+ }
11
+ function isRegistryAuthFailure(stderr) {
12
+ return /(?:\bE401\b|\b401\b.*\bUnauthorized\b)/iu.test(stderr);
17
13
  }
18
14
  export async function prepareDaemonRestart(input) {
19
15
  const local = input.localSlots.tryAcquireExclusive();
@@ -40,10 +36,11 @@ async function readPackageVersion(packageRoot) {
40
36
  const body = JSON.parse(await readFile(resolve(packageRoot, "package.json"), "utf8"));
41
37
  return typeof body.version === "string" ? body.version : "";
42
38
  }
43
- async function restoreInstalledVersion(input, runner, version, registryArgs) {
39
+ async function restoreInstalledVersion(input, runner, command, commandPrefix, version, registryArgs) {
44
40
  if (!isDaemonReleaseVersion(version))
45
41
  return false;
46
- const result = await runner((input.platform ?? process.platform) === "win32" ? "npm.cmd" : "npm", [
42
+ const result = await runner(command, [
43
+ ...commandPrefix,
47
44
  "install",
48
45
  "--global",
49
46
  "--prefix",
@@ -98,6 +95,10 @@ export async function installExactDaemonUpdate(input) {
98
95
  return { ok: false, errorCode: "ineligible" };
99
96
  }
100
97
  const runner = input.runner ?? systemCommandRunner;
98
+ const platform = input.platform ?? process.platform;
99
+ const npm = npmInvocation(platform, input.nodePath ?? process.execPath);
100
+ const command = npm.command;
101
+ const commandPrefix = npm.argsPrefix;
101
102
  let previousVersion = "";
102
103
  try {
103
104
  previousVersion = await (input.readInstalledVersion ?? readPackageVersion)(input.packageRoot);
@@ -111,9 +112,13 @@ export async function installExactDaemonUpdate(input) {
111
112
  await release();
112
113
  return { ok: false, errorCode: "install_failed" };
113
114
  }
114
- const registryArgs = configuredRegistryArgs(daemonReleaseChannel(previousVersion) === "beta"
115
- || daemonReleaseChannel(input.targetVersion) === "beta");
116
- if (registryArgs === null) {
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
+ });
121
+ if (registries === null) {
117
122
  await release();
118
123
  return { ok: false, errorCode: "install_failed" };
119
124
  }
@@ -124,20 +129,41 @@ export async function installExactDaemonUpdate(input) {
124
129
  await release();
125
130
  return { ok: false, errorCode: "install_failed" };
126
131
  }
127
- const result = await runner((input.platform ?? process.platform) === "win32" ? "npm.cmd" : "npm", [
128
- "install",
129
- "--global",
130
- "--prefix",
131
- input.npmPrefix,
132
- "--ignore-scripts",
133
- "--no-audit",
134
- "--no-fund",
135
- ...registryArgs,
136
- `@nowcrew/daemon@${input.targetVersion}`,
137
- ]).catch(() => null);
138
- if (result === null || result.exitCode !== 0) {
132
+ let selectedRegistryArgs = [];
133
+ let installed = false;
134
+ let authFailures = 0;
135
+ let failedAttempts = 0;
136
+ for (const registry of registries) {
137
+ const args = registryArgs(registry);
138
+ const result = await runner(command, [
139
+ ...commandPrefix,
140
+ "install",
141
+ "--global",
142
+ "--prefix",
143
+ input.npmPrefix,
144
+ "--ignore-scripts",
145
+ "--no-audit",
146
+ "--no-fund",
147
+ ...args,
148
+ `@nowcrew/daemon@${input.targetVersion}`,
149
+ ]).catch(() => null);
150
+ if (result?.exitCode === 0) {
151
+ selectedRegistryArgs = args;
152
+ installed = true;
153
+ break;
154
+ }
155
+ failedAttempts += 1;
156
+ if (result !== null && isRegistryAuthFailure(result.stderr))
157
+ authFailures += 1;
158
+ }
159
+ if (!installed) {
139
160
  await release();
140
- return { ok: false, errorCode: "install_failed" };
161
+ return {
162
+ ok: false,
163
+ errorCode: failedAttempts > 0 && authFailures === failedAttempts
164
+ ? "registry_auth_failed"
165
+ : "install_failed",
166
+ };
141
167
  }
142
168
  let installedVersion = "";
143
169
  try {
@@ -147,7 +173,7 @@ export async function installExactDaemonUpdate(input) {
147
173
  // Version verification is authoritative; unreadable package metadata is a mismatch.
148
174
  }
149
175
  if (installedVersion !== input.targetVersion) {
150
- const restored = await restoreInstalledVersion(input, runner, previousVersion, registryArgs);
176
+ const restored = await restoreInstalledVersion(input, runner, command, commandPrefix, previousVersion, selectedRegistryArgs);
151
177
  if (!restored) {
152
178
  console.error(`[daemon-update] version verification failed; restore of ${previousVersion} was unsuccessful`);
153
179
  }
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { ATTACHMENT_MAX_FILE_BYTES, ATTACHMENT_MAX_TOTAL_BYTES, } from "./attachment-limits.js";
2
3
  import { AbilityRepositoryUrlSchema, SafeAbilityPathSchema, WorkspaceAbilityTargetSchema, } from "./agent-ability/types.js";
3
4
  import { compareProjectSkillRefs, MAX_AGENT_PROJECT_SKILL_BINDINGS, MAX_PROJECT_ID_LENGTH, MAX_PROJECT_SKILL_NAME_LENGTH, } from "./project-skills/types.js";
4
5
  const ExecutionIdSchema = z.string().uuid();
@@ -160,8 +161,10 @@ export const ExecutionAttachmentSchema = z.object({
160
161
  id: z.string().min(1).max(200),
161
162
  filename: z.string().min(1).max(255),
162
163
  mime: z.string().min(1).max(200),
163
- sizeBytes: z.number().int().nonnegative().max(25 * 1024 * 1024),
164
+ sizeBytes: z.number().int().nonnegative().max(ATTACHMENT_MAX_FILE_BYTES),
164
165
  }).strict();
166
+ const ExecutionAttachmentsSchema = z.array(ExecutionAttachmentSchema).max(20).refine((attachments) => attachments.reduce((total, attachment) => total + attachment.sizeBytes, 0)
167
+ <= ATTACHMENT_MAX_TOTAL_BYTES, "attachment bytes exceed the execution limit");
165
168
  const ExecutionAgentSchema = z.object({
166
169
  id: z.string().min(1),
167
170
  handle: AgentHandleSchema,
@@ -214,7 +217,7 @@ export const ExecutionStartSchema = z.object({
214
217
  scheduledRunId: ExecutionIdSchema.optional(),
215
218
  externalResponseSessionId: ExecutionIdSchema.optional(),
216
219
  answerStream: z.boolean().optional(),
217
- attachments: z.array(ExecutionAttachmentSchema).max(20).optional(),
220
+ attachments: ExecutionAttachmentsSchema.optional(),
218
221
  }).strict(),
219
222
  reporting: z.object({
220
223
  captureFinal: z.boolean(),
@@ -1,5 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
- import { join } from "node:path";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { resolveReposRoot } from "./config.js";
3
4
  import { DaemonToServerExecutionFrameSchema, ExecutionCompletedSchema, ExecutionRejectedSchema, ExecutionStartSchema, } from "./execution-protocol.js";
4
5
  import { JournalConflictError } from "./execution-journal.js";
5
6
  import { boundExecutionFrame } from "./execution-event-limit.js";
@@ -524,10 +525,11 @@ export async function runExecution(config, input, dependencies) {
524
525
  if (dependencies.abilityRelease !== undefined) {
525
526
  await cancellable(dependencies.abilityRelease.apply(config.agentsRoot, spec.agent.handle, spec.agent.abilityRelease), dependencies.cancellation);
526
527
  }
528
+ const reposRoot = resolveReposRoot(config.agentsRoot, config.reposRoot);
527
529
  const preparedProjectDependencies = await cancellable(dependencies.projectDependencies.prepare({
528
530
  trainingRoot: join(config.agentsRoot, spec.agent.handle, "training"),
529
- reposRoot: join(config.agentsRoot, "repos"),
530
- lockRoot: join(config.agentsRoot, ".crew", "project-dependencies"),
531
+ reposRoot,
532
+ lockRoot: join(dirname(resolve(reposRoot)), ".crew", "project-dependencies"),
531
533
  }), dependencies.cancellation);
532
534
  trainingProjectContext = Object.freeze({
533
535
  projects: Object.freeze(preparedProjectDependencies.map((project) => Object.freeze({
@@ -862,6 +864,8 @@ export async function runExecution(config, input, dependencies) {
862
864
  token: credential.token,
863
865
  agentId: credential.agentId,
864
866
  agentsRoot: config.agentsRoot,
867
+ reposRoot: resolveReposRoot(config.agentsRoot, config.reposRoot),
868
+ ...(config.qaReportRoot === undefined ? {} : { qaReportRoot: config.qaReportRoot }),
865
869
  cliPath: config.cliPath,
866
870
  providerConfig,
867
871
  codexHomeMode: config.codexHomeMode,
@@ -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;
@@ -466,6 +466,13 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
466
466
  CREW_TOKEN: input.launch.token,
467
467
  CREW_CHANNEL: input.channelId,
468
468
  CREW_HOME: workspace.dir,
469
+ // Keep the daemon-selected storage root authoritative for runtimes.
470
+ // In profile-backed NowWork sessions the daemon config may differ from
471
+ // the daemon process environment, so inheriting CREW_AGENTS_ROOT is not
472
+ // sufficient for QA/project diagnostics.
473
+ CREW_AGENTS_ROOT: input.launch.agentsRoot,
474
+ ...(input.launch.reposRoot === undefined ? {} : { NOWWORK_REPOS_ROOT: input.launch.reposRoot }),
475
+ ...(input.launch.qaReportRoot === undefined ? {} : { QA_REPORT_HUB: input.launch.qaReportRoot }),
469
476
  // Expose only the training root; ability-specific workspace layout stays
470
477
  // owned by the training package rather than the NowWork daemon.
471
478
  QA_RUNTIME_ROOT: join(workspace.dir, "training"),
@@ -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) {
@@ -25,6 +25,7 @@ export { executableRuntimes } from "./runtime-capabilities.js";
25
25
  const execFileP = promisify(execFile);
26
26
  export const PROJECT_WORKSPACES_CAPABILITY = "project_workspaces_v1";
27
27
  export const TRAINING_PROJECT_DEPENDENCIES_CAPABILITY = "training_project_dependencies_v1";
28
+ export const EXECUTION_LARGE_ATTACHMENTS_CAPABILITY = "execution_attachments_300m_v1";
28
29
  export const NATIVE_PROJECT_WORKSPACE_READINESS = Object.freeze({
29
30
  resolverReady: true,
30
31
  nativeRuntimeAdaptersReady: true,
@@ -36,6 +37,7 @@ export const DAEMON_CAPABILITIES = Object.freeze([
36
37
  "execution_telemetry_ack_v1",
37
38
  "execution_external_output_v1",
38
39
  "execution_attachments_v1",
40
+ EXECUTION_LARGE_ATTACHMENTS_CAPABILITY,
39
41
  "execution_answer_stream_v1",
40
42
  "execution_machine_queue_v1",
41
43
  "execution_agent_memory_policy_v1",