@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.
- package/README.md +27 -0
- package/dist/agent-ability/resolver.js +1 -1
- package/dist/attachment-limits.js +4 -0
- package/dist/attachments.js +33 -27
- package/dist/automatic-daemon-installer.js +44 -24
- package/dist/automatic-install-target.js +34 -9
- package/dist/computer-cli.js +43 -14
- package/dist/computer-profile.js +15 -2
- package/dist/computer-service.js +26 -5
- package/dist/config.js +34 -6
- package/dist/daemon-installation-lease.js +7 -4
- package/dist/daemon-local-home.js +108 -0
- package/dist/daemon-migration-controller.js +15 -4
- package/dist/daemon-registry.js +20 -0
- package/dist/daemon-update-controller.js +1 -0
- package/dist/daemon-update-eligibility.js +17 -7
- package/dist/daemon-updater.js +56 -30
- package/dist/execution-protocol.js +5 -2
- package/dist/execution-runner.js +7 -3
- package/dist/host-execution-coordinator.js +4 -3
- package/dist/local-executor.js +7 -0
- package/dist/local-memory/runtime.js +5 -3
- package/dist/machine-info.js +2 -0
- package/dist/main.js +33 -14
- package/dist/managed-service-diagnostics.js +1 -0
- package/dist/managed-service-registry.js +12 -7
- package/dist/managed-service-startup.js +4 -1
- package/dist/npm-command.js +9 -0
- package/dist/project-dependencies.js +4 -4
- package/dist/project-skills/runtime-root-bootstrap.js +6 -0
- package/dist/project-skills/runtime-root-startup.js +2 -1
- package/dist/project-skills/serve-startup.js +23 -0
- package/dist/repository-identity.js +2 -0
- package/dist/runner.js +3 -0
- package/dist/runtime-capabilities.js +3 -1
- package/dist/serve.js +7 -6
- package/dist/slog.js +14 -1
- package/dist/storage-control.js +12 -1
- package/dist/storage-identity.js +15 -1
- package/dist/storage-layout.js +2 -1
- package/dist/storage-migration-controller.js +25 -4
- package/dist/storage-ownership.js +55 -0
- package/dist/storage-runtime-controller.js +29 -7
- package/dist/storage-runtime-readiness.js +26 -4
- package/dist/storage-runtime.js +18 -2
- package/dist/storage-status-delivery.js +4 -0
- package/dist/storage-status-replay.js +16 -1
- package/dist/storage-worker-executor.js +1 -0
- package/dist/storage-worker-operation.js +4 -0
- package/package.json +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
|
|
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
|
-
|
|
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
|
|
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({
|
|
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
|
|
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,
|
|
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,
|
|
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
|
}
|
package/dist/daemon-updater.js
CHANGED
|
@@ -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 {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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(
|
|
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
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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 {
|
|
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,
|
|
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(
|
|
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:
|
|
220
|
+
attachments: ExecutionAttachmentsSchema.optional(),
|
|
218
221
|
}).strict(),
|
|
219
222
|
reporting: z.object({
|
|
220
223
|
captureFinal: z.boolean(),
|
package/dist/execution-runner.js
CHANGED
|
@@ -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
|
|
530
|
-
lockRoot: join(
|
|
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,
|
|
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;
|
package/dist/local-executor.js
CHANGED
|
@@ -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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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) {
|
package/dist/machine-info.js
CHANGED
|
@@ -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",
|