@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.
- package/README.md +27 -0
- 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 +28 -8
- package/dist/control-plane-url.js +3 -9
- 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 +26 -26
- package/dist/execution-runner.js +7 -7
- package/dist/host-execution-coordinator.js +4 -3
- package/dist/local-executor.js +23 -17
- package/dist/local-memory/runtime.js +5 -3
- package/dist/machine-info.js +3 -0
- package/dist/main.js +13 -2
- 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-skills/controller.js +6 -71
- package/dist/project-skills/execution-adapter.js +7 -0
- package/dist/project-skills/execution-projection.js +163 -0
- package/dist/project-skills/initialized-reconciler.js +10 -8
- package/dist/project-skills/legacy-cleanup.js +224 -0
- package/dist/project-skills/machine-hello-publisher.js +20 -0
- package/dist/project-skills/reconciler.js +114 -500
- package/dist/project-skills/resolution-records.js +63 -0
- package/dist/project-skills/runtime-launch.js +0 -42
- package/dist/project-skills/runtime-warning.js +1 -3
- package/dist/project-skills/scanner.js +1 -17
- package/dist/project-skills/startup-cleanup.js +24 -0
- package/dist/runner.js +2 -1
- package/dist/serve.js +49 -49
- package/dist/slog.js +14 -1
- package/dist/storage-control.js +12 -1
- package/dist/storage-identity.js +15 -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
|
@@ -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
|
@@ -1,25 +1,10 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
|
-
import {
|
|
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 {
|
|
7
|
-
|
|
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
|
|
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 =
|
|
128
|
-
|
|
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
|
|
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
|
-
|
|
157
|
+
authFailures += 1;
|
|
163
158
|
}
|
|
164
159
|
if (!installed) {
|
|
165
160
|
await release();
|
|
166
|
-
return {
|
|
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
|
}
|
package/dist/execution-runner.js
CHANGED
|
@@ -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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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,
|
|
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
|
@@ -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,
|
|
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
|
-
|
|
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,
|
|
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
|
|
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 ?? ""}${
|
|
839
|
+
finalText = incremental ? `${finalText ?? ""}${extracted}` : extracted;
|
|
832
840
|
}
|
|
833
841
|
for (const text of decodeExternalOutputEvent(runtime.name, event, externalOutput)) {
|
|
834
|
-
callbacks.onExternalOutput?.(
|
|
842
|
+
callbacks.onExternalOutput?.(text);
|
|
835
843
|
}
|
|
836
844
|
for (const chunk of consoleFormatter.format(event)) {
|
|
837
|
-
callbacks.onConsole?.(
|
|
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 =
|
|
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
|
-
// 截断"
|
|
998
|
+
// 截断"这一老兜底。注意这放宽了"externalAnswer 非空
|
|
993
999
|
// ⇒ finalText 非空"的旧不变式;是否上报仍由 execution-runner 按 exitCode===0 把关。
|
|
994
1000
|
externalAnswer: input.captureFinal
|
|
995
1001
|
? (externalOutput.lastClosedBlock !== null
|
|
996
|
-
?
|
|
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
|
-
|
|
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) {
|