@nowcrew/daemon 0.6.88-beta.3 → 0.6.88-beta.31

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 (129) hide show
  1. package/README.md +89 -14
  2. package/dist/agent-ability/controller.js +11 -1
  3. package/dist/agent-ability/git-errors.js +22 -0
  4. package/dist/agent-ability/materializer.js +21 -10
  5. package/dist/agent-ability/resolver.js +12 -4
  6. package/dist/agent-environment-contract.js +55 -0
  7. package/dist/agent-environment-control.js +24 -0
  8. package/dist/agent-environment-execution.js +98 -0
  9. package/dist/agent-environments.js +275 -0
  10. package/dist/agent-memory/policy.js +3 -1
  11. package/dist/automatic-daemon-installer.js +51 -2
  12. package/dist/automatic-install-target.js +8 -0
  13. package/dist/computer-main.js +18 -0
  14. package/dist/computer-profile.js +17 -4
  15. package/dist/computer-setup-client.js +318 -0
  16. package/dist/computer-setup-orchestration.js +203 -0
  17. package/dist/computer-setup.js +244 -0
  18. package/dist/config.js +30 -12
  19. package/dist/console-formatter.js +3 -3
  20. package/dist/console.js +15 -2
  21. package/dist/daemon-update-eligibility.js +1 -1
  22. package/dist/execution-journal.js +9 -6
  23. package/dist/execution-protocol.js +2 -1
  24. package/dist/execution-runner.js +16 -10
  25. package/dist/i18n.js +19 -0
  26. package/dist/list-models.js +13 -11
  27. package/dist/local-execution-lease.js +22 -0
  28. package/dist/local-executor.js +88 -92
  29. package/dist/local-memory/candidate.js +43 -0
  30. package/dist/local-memory/index-budget.js +8 -0
  31. package/dist/local-memory/inventory-cli.js +61 -0
  32. package/dist/local-memory/inventory-preview.js +159 -0
  33. package/dist/local-memory/inventory-source.js +159 -0
  34. package/dist/local-memory/pending-match.js +29 -0
  35. package/dist/local-memory/pending-query.js +100 -0
  36. package/dist/local-memory/pending-schema.js +29 -0
  37. package/dist/local-memory/pending-source.js +126 -0
  38. package/dist/local-memory/pending.js +34 -84
  39. package/dist/local-memory/prompt.js +20 -12
  40. package/dist/local-memory/read-cli.js +97 -12
  41. package/dist/local-memory/rollout.js +82 -0
  42. package/dist/local-memory/runtime.js +24 -17
  43. package/dist/local-memory/work-log-execution.js +31 -0
  44. package/dist/local-memory/work-log.js +58 -0
  45. package/dist/local-memory/write.js +27 -58
  46. package/dist/local-runtime-environment.js +50 -0
  47. package/dist/machine-environment-cache.js +147 -0
  48. package/dist/machine-environment-contract.js +10 -0
  49. package/dist/machine-environment-control.js +27 -0
  50. package/dist/machine-environment-policy.js +75 -0
  51. package/dist/machine-environment-process.js +110 -0
  52. package/dist/machine-info.js +70 -25
  53. package/dist/machine-runtime-environment.js +189 -0
  54. package/dist/machine-shell-environment-capture.js +135 -0
  55. package/dist/machine-windows-environment-capture.js +215 -0
  56. package/dist/machine-windows-environment-script.js +158 -0
  57. package/dist/main.js +4 -69
  58. package/dist/marketplace-controller.js +75 -0
  59. package/dist/marketplace-protocol.js +38 -0
  60. package/dist/marketplace-runner.js +234 -0
  61. package/dist/marketplace-serve.js +17 -0
  62. package/dist/message-file-preview.js +246 -0
  63. package/dist/native-thread-context.js +59 -0
  64. package/dist/pre-admission-rejection.js +64 -0
  65. package/dist/project-skills/execution-adapter.js +3 -2
  66. package/dist/project-skills/runtime-launch.js +14 -0
  67. package/dist/prompt.js +15 -15
  68. package/dist/provider-env.js +26 -15
  69. package/dist/runner.js +31 -19
  70. package/dist/runtime-environment-merge.js +16 -0
  71. package/dist/runtime-health-handler.js +10 -0
  72. package/dist/runtime-health.js +3 -1
  73. package/dist/runtime-path.js +22 -12
  74. package/dist/runtime-probe.js +35 -6
  75. package/dist/runtimes/acp-probe-transport.js +69 -0
  76. package/dist/runtimes/claude-models.js +20 -9
  77. package/dist/runtimes/claude-session-start-hook.js +24 -0
  78. package/dist/runtimes/claude-thread-runner.js +266 -0
  79. package/dist/runtimes/claude.js +23 -4
  80. package/dist/runtimes/codex-app-server-runner.js +43 -4
  81. package/dist/runtimes/deepseek-harness-launch.js +10 -0
  82. package/dist/runtimes/deepseek-harness-profile.js +92 -0
  83. package/dist/runtimes/deepseek-harness-runner.js +226 -0
  84. package/dist/runtimes/deepseek-harness.js +76 -15
  85. package/dist/runtimes/harness-probe-retry.js +42 -0
  86. package/dist/runtimes/hermes-models.js +2 -1
  87. package/dist/runtimes/kimi-acp-runner.js +84 -15
  88. package/dist/runtimes/opencode-runner.js +7 -1
  89. package/dist/runtimes/opencode.js +2 -2
  90. package/dist/serve.js +95 -184
  91. package/dist/server-url.js +25 -0
  92. package/dist/session.js +4 -0
  93. package/dist/slog.js +2 -1
  94. package/dist/storage-identity.js +0 -3
  95. package/dist/storage-runtime.js +2 -2
  96. package/dist/storage-serve-adapter.js +6 -72
  97. package/dist/supervised-runtime.js +28 -18
  98. package/dist/thread-input-delivery.js +217 -0
  99. package/dist/token.js +0 -1
  100. package/dist/workspace-control-handler.js +27 -0
  101. package/dist/workspace-fs.js +66 -15
  102. package/dist/workspace.js +1 -1
  103. package/package.json +5 -4
  104. package/dist/codex-home-launch.js +0 -7
  105. package/dist/codex-home-mode-controller.js +0 -68
  106. package/dist/daemon-migration-controller.js +0 -211
  107. package/dist/daemon-migration-wiring.js +0 -22
  108. package/dist/runtimes/codex-home-migration-cli.js +0 -26
  109. package/dist/runtimes/codex-home-migration.js +0 -112
  110. package/dist/runtimes/codex-home.js +0 -236
  111. package/dist/storage-control.js +0 -212
  112. package/dist/storage-copy.js +0 -628
  113. package/dist/storage-credential-guard.js +0 -21
  114. package/dist/storage-doctor.js +0 -228
  115. package/dist/storage-manifest.js +0 -1027
  116. package/dist/storage-migration-controller.js +0 -440
  117. package/dist/storage-migration-manifest.js +0 -105
  118. package/dist/storage-migration-policy.js +0 -100
  119. package/dist/storage-runtime-controller.js +0 -381
  120. package/dist/storage-runtime-readiness.js +0 -240
  121. package/dist/storage-safe-fs.js +0 -302
  122. package/dist/storage-status-delivery.js +0 -224
  123. package/dist/storage-status-replay.js +0 -140
  124. package/dist/storage-target-confirmation.js +0 -129
  125. package/dist/storage-worker-command.js +0 -37
  126. package/dist/storage-worker-executor.js +0 -449
  127. package/dist/storage-worker-handoff.js +0 -151
  128. package/dist/storage-worker-operation.js +0 -294
  129. package/dist/storage-worker-status.js +0 -259
@@ -0,0 +1,275 @@
1
+ /** Server 决策生效计划;本模块只做受控文件物化与当前执行目录选择。 */
2
+ import { chmod, lstat, mkdir, open, readFile, readdir, realpath, rename, unlink } from "node:fs/promises";
3
+ import { createHash, randomUUID } from "node:crypto";
4
+ import { dirname, join, relative, resolve, sep } from "node:path";
5
+ import { z } from "zod";
6
+ import { acquireProfileSaveLock } from "./computer-profile-lock.js";
7
+ import { AgentEnvironmentPlanSchema } from "./agent-environment-contract.js";
8
+ const STATE_DIRECTORY = ".nowwork-environments";
9
+ const FileHashes = z.record(z.string().regex(/^[a-f0-9]{64}$/u));
10
+ const StateSchema = z.object({
11
+ revision: z.number().int().nonnegative(), digest: z.string(), files: FileHashes,
12
+ pending: z.object({ revision: z.number().int().nonnegative(), digest: z.string(), files: FileHashes }).optional(),
13
+ }).strict();
14
+ const digest = (content) => createHash("sha256").update(content).digest("hex");
15
+ const codeOf = (error) => error.code;
16
+ async function safeDirectory(root, path) {
17
+ let current = root;
18
+ for (const segment of path.split("/").filter(Boolean)) {
19
+ if (segment === "." || segment === ".." || segment.includes("\\"))
20
+ throw new Error("environment_path_invalid");
21
+ current = join(current, segment);
22
+ await mkdir(current, { mode: 0o700 }).catch(error => { if (codeOf(error) !== "EEXIST")
23
+ throw error; });
24
+ const info = await lstat(current);
25
+ if (info.isSymbolicLink())
26
+ throw new Error("environment_path_symlink");
27
+ if (!info.isDirectory())
28
+ throw new Error("environment_path_invalid");
29
+ }
30
+ return current;
31
+ }
32
+ async function regularFileContent(path) {
33
+ const info = await lstat(path).catch(error => { if (codeOf(error) === "ENOENT")
34
+ return null; throw error; });
35
+ if (!info)
36
+ return null;
37
+ if (!info.isFile() || info.isSymbolicLink() || info.nlink > 1)
38
+ throw new Error("environment_file_unsafe");
39
+ if (info.size > 8 * 1024 * 1024)
40
+ throw new Error("environment_file_too_large");
41
+ return readFile(path, "utf8");
42
+ }
43
+ async function atomicWrite(path, content, beforeCommit) {
44
+ const temporary = join(dirname(path), `.env-write-${randomUUID()}`);
45
+ try {
46
+ const handle = await open(temporary, "wx", 0o600);
47
+ try {
48
+ await handle.writeFile(content, "utf8");
49
+ await handle.sync();
50
+ }
51
+ finally {
52
+ await handle.close();
53
+ }
54
+ // stage 的慢 IO 完成后重验归属;最终校验 + rename 不是针对外部编辑器的系统级 CAS。
55
+ await beforeCommit?.();
56
+ await rename(temporary, path);
57
+ await chmod(path, 0o600);
58
+ }
59
+ finally {
60
+ await unlink(temporary).catch(error => { if (codeOf(error) !== "ENOENT")
61
+ throw error; });
62
+ }
63
+ }
64
+ async function requireUnchangedFile(path, expectedHash, relativePath) {
65
+ const current = await regularFileContent(path);
66
+ if ((current === null ? null : digest(current)) !== expectedHash)
67
+ throw new Error(`environment_file_conflict:${relativePath}`);
68
+ }
69
+ const relativeFileValid = (path) => /^(?:(?:training|repos\/[A-Za-z0-9][A-Za-z0-9._-]{0,127})\/)?\.env(?:\.[a-z][a-z0-9_-]{0,47})?$/u.test(path);
70
+ export async function withEnvironmentFilesLock(agentRoot, operation) {
71
+ await mkdir(agentRoot, { recursive: true, mode: 0o700 });
72
+ const rootInfo = await lstat(agentRoot);
73
+ if (!rootInfo.isDirectory() || rootInfo.isSymbolicLink())
74
+ throw new Error("environment_path_symlink");
75
+ const stateRoot = await safeDirectory(await realpath(agentRoot), STATE_DIRECTORY);
76
+ const release = await acquireProfileSaveLock(stateRoot);
77
+ try {
78
+ return await operation();
79
+ }
80
+ finally {
81
+ await release();
82
+ }
83
+ }
84
+ const TRAINING_FILE_MARKER = ".nowwork-environment-files.json";
85
+ const trainingFileName = (name) => /^\.env(?:\.[a-z][a-z0-9_-]{0,47})?$/u.test(name);
86
+ const TrainingFileOwnershipSchema = z.object({ files: FileHashes, pending: FileHashes.optional() }).strict();
87
+ const trainingFileHashes = (files) => Object.fromEntries(Object.entries(files).map(([name, content]) => [name, digest(content)]));
88
+ async function trainingFileOwnership(directory) {
89
+ const text = await regularFileContent(join(directory, TRAINING_FILE_MARKER));
90
+ if (text === null)
91
+ return null;
92
+ try {
93
+ const ownership = TrainingFileOwnershipSchema.parse(JSON.parse(text));
94
+ if (Object.keys({ ...ownership.files, ...ownership.pending }).some(name => !trainingFileName(name)))
95
+ throw new Error();
96
+ return ownership;
97
+ }
98
+ catch {
99
+ throw new Error("environment_training_state_invalid");
100
+ }
101
+ }
102
+ /** 训练包安装前,仅接受由环境管理器创建、没有其他用户文件的占位目录。 */
103
+ export async function isEnvironmentOnlyTrainingDirectory(directory) {
104
+ const ownership = await trainingFileOwnership(directory);
105
+ const stateText = ownership === null ? await regularFileContent(join(dirname(directory), STATE_DIRECTORY, "state.json")) : null;
106
+ const state = stateText === null ? null : StateSchema.parse(JSON.parse(stateText));
107
+ if (ownership === null && state === null)
108
+ return false;
109
+ const names = await readdir(directory);
110
+ for (const name of names) {
111
+ if (name === TRAINING_FILE_MARKER && ownership !== null)
112
+ continue;
113
+ if (!trainingFileName(name))
114
+ return false;
115
+ const content = await regularFileContent(join(directory, name));
116
+ const hash = content === null ? null : digest(content);
117
+ const owned = ownership === null
118
+ ? hash !== null && (hash === state?.files[`training/${name}`] || hash === state?.pending?.files[`training/${name}`])
119
+ : hash !== null && (hash === ownership.files[name] || hash === ownership.pending?.[name]);
120
+ if (!owned)
121
+ return false;
122
+ }
123
+ return true;
124
+ }
125
+ /** 保存当前全局状态已确认的文件,并给即将移入 rollback 的目录留下独立归属证明。 */
126
+ export async function captureTrainingEnvironmentFiles(agentRoot) {
127
+ const stateText = await regularFileContent(join(agentRoot, STATE_DIRECTORY, "state.json"));
128
+ if (!stateText)
129
+ return {};
130
+ const state = StateSchema.parse(JSON.parse(stateText));
131
+ const directory = join(agentRoot, "training");
132
+ const info = await lstat(directory).catch(error => { if (codeOf(error) === "ENOENT")
133
+ return null; throw error; });
134
+ if (info === null)
135
+ return {};
136
+ if (!info.isDirectory() || info.isSymbolicLink())
137
+ throw new Error("environment_path_symlink");
138
+ await trainingFileOwnership(directory); // 损坏的归属记录不能被静默覆盖。
139
+ const result = {};
140
+ for (const name of await readdir(directory)) {
141
+ if (!trainingFileName(name))
142
+ continue;
143
+ const path = `training/${name}`;
144
+ const content = await regularFileContent(join(directory, name));
145
+ if (content === null)
146
+ continue;
147
+ if (digest(content) !== state.files[path] && digest(content) !== state.pending?.files[path])
148
+ throw new Error(`environment_file_conflict:${path}`);
149
+ result[name] = content;
150
+ }
151
+ if ((info.mode & 0o200) === 0)
152
+ await chmod(directory, info.mode | 0o200);
153
+ await atomicWrite(join(directory, TRAINING_FILE_MARKER), JSON.stringify({ files: trainingFileHashes(result) }));
154
+ return result;
155
+ }
156
+ /** 发布前收敛完整文件集合;只移除目录自身归属记录确认的旧文件,支持中途失败后重试。 */
157
+ export async function restoreTrainingEnvironmentFiles(directory, files) {
158
+ const info = await lstat(directory);
159
+ if (!info.isDirectory() || info.isSymbolicLink())
160
+ throw new Error("environment_path_symlink");
161
+ const ownership = await trainingFileOwnership(directory);
162
+ const names = [...new Set([
163
+ ...(await readdir(directory)).filter(trainingFileName),
164
+ ...Object.keys(ownership?.files ?? {}), ...Object.keys(ownership?.pending ?? {}), ...Object.keys(files),
165
+ ])].sort();
166
+ const assertOwned = async (name) => {
167
+ if (!trainingFileName(name))
168
+ throw new Error("environment_state_path_invalid");
169
+ const current = await regularFileContent(join(directory, name));
170
+ if (current !== null && digest(current) !== ownership?.files[name] && digest(current) !== ownership?.pending?.[name]) {
171
+ throw new Error(`environment_file_conflict:training/${name}`);
172
+ }
173
+ return current;
174
+ };
175
+ const currentHashes = {};
176
+ for (const name of names) {
177
+ const current = await assertOwned(name);
178
+ if (current !== null)
179
+ currentHashes[name] = digest(current);
180
+ }
181
+ const nextHashes = trainingFileHashes(files);
182
+ if ((info.mode & 0o200) === 0)
183
+ await chmod(directory, info.mode | 0o200);
184
+ await atomicWrite(join(directory, TRAINING_FILE_MARKER), JSON.stringify({ files: currentHashes, pending: nextHashes }));
185
+ for (const name of names) {
186
+ const target = join(directory, name);
187
+ const current = await regularFileContent(target);
188
+ if (current !== null && digest(current) !== currentHashes[name])
189
+ throw new Error(`environment_file_conflict:training/${name}`);
190
+ const content = files[name];
191
+ if (content === undefined)
192
+ await unlink(target).catch(error => { if (codeOf(error) !== "ENOENT")
193
+ throw error; });
194
+ else {
195
+ const expectedHash = current === null ? null : digest(current);
196
+ await atomicWrite(target, content, () => requireUnchangedFile(target, expectedHash, `training/${name}`));
197
+ }
198
+ }
199
+ await atomicWrite(join(directory, TRAINING_FILE_MARKER), JSON.stringify({ files: nextHashes }));
200
+ }
201
+ export async function applyAgentEnvironmentPlan(agentRoot, input) {
202
+ const parsed = AgentEnvironmentPlanSchema.safeParse(input);
203
+ // 不让 Zod 的输入值或上游凭据出现在 RPC / console 错误中。
204
+ if (!parsed.success)
205
+ throw new Error("environment_plan_invalid");
206
+ const plan = parsed.data;
207
+ await mkdir(agentRoot, { recursive: true, mode: 0o700 });
208
+ const rootInfo = await lstat(agentRoot);
209
+ if (!rootInfo.isDirectory() || rootInfo.isSymbolicLink())
210
+ throw new Error("environment_path_symlink");
211
+ const root = await realpath(agentRoot);
212
+ const stateRoot = await safeDirectory(root, STATE_DIRECTORY);
213
+ const release = await acquireProfileSaveLock(stateRoot);
214
+ try {
215
+ const statePath = join(stateRoot, "state.json");
216
+ const stateText = await regularFileContent(statePath);
217
+ const previous = stateText === null ? null : StateSchema.parse(JSON.parse(stateText));
218
+ const hash = digest(JSON.stringify(plan));
219
+ const latest = previous?.pending ?? previous;
220
+ if (latest && plan.revision < latest.revision)
221
+ throw new Error("environment_revision_stale");
222
+ if (latest && plan.revision === latest.revision && hash !== latest.digest)
223
+ throw new Error("environment_revision_conflict");
224
+ const nextContents = Object.fromEntries(plan.scopes.flatMap(scope => scope.files.map(file => [
225
+ scope.path ? `${scope.path}/${file.name}` : file.name, file.content,
226
+ ])));
227
+ const nextHashes = Object.fromEntries(Object.entries(nextContents).map(([path, text]) => [path, digest(text)]));
228
+ const paths = [...new Set([...Object.keys(previous?.files ?? {}), ...Object.keys(previous?.pending?.files ?? {}), ...Object.keys(nextContents)])].sort();
229
+ // 所有候选先预检,再开始写入,避免可预见的冲突造成半套配置。
230
+ const currentHashes = {};
231
+ for (const path of paths) {
232
+ if (!relativeFileValid(path))
233
+ throw new Error("environment_state_path_invalid");
234
+ await safeDirectory(root, path.includes("/") ? path.slice(0, path.lastIndexOf("/")) : "");
235
+ const current = await regularFileContent(join(root, path));
236
+ if (current !== null && digest(current) !== previous?.files[path] && digest(current) !== previous?.pending?.files[path]) {
237
+ throw new Error(`environment_file_conflict:${path}`);
238
+ }
239
+ if (current !== null)
240
+ currentHashes[path] = digest(current);
241
+ }
242
+ const pending = { revision: plan.revision, digest: hash, files: nextHashes };
243
+ await atomicWrite(statePath, JSON.stringify({ revision: previous?.revision ?? 0, digest: previous?.digest ?? "", files: currentHashes, pending }));
244
+ for (const path of paths) {
245
+ // 重检目录和文件,确保写入前仍处于托管边界。
246
+ await safeDirectory(root, path.includes("/") ? path.slice(0, path.lastIndexOf("/")) : "");
247
+ const target = join(root, path);
248
+ const current = await regularFileContent(target);
249
+ if (current !== null && digest(current) !== currentHashes[path]) {
250
+ throw new Error(`environment_file_conflict:${path}`);
251
+ }
252
+ const content = nextContents[path];
253
+ if (content === undefined)
254
+ await unlink(target).catch(error => { if (codeOf(error) !== "ENOENT")
255
+ throw error; });
256
+ else {
257
+ const expectedHash = current === null ? null : digest(current);
258
+ await atomicWrite(target, content, () => requireUnchangedFile(target, expectedHash, path));
259
+ }
260
+ }
261
+ await atomicWrite(statePath, JSON.stringify(pending));
262
+ return { revision: plan.revision, files: Object.keys(nextContents).sort() };
263
+ }
264
+ finally {
265
+ await release();
266
+ }
267
+ }
268
+ /** 按工作目录/绑定项目选择唯一分支;绝不把多个兄弟 Repo 的变量混成进程全局值。 */
269
+ export function environmentForExecution(plan, agentRoot, cwd, projectId) {
270
+ const selected = projectId === undefined ? undefined : plan.scopes.find(scope => scope.id === `repo:${projectId}`);
271
+ const path = relative(resolve(agentRoot), resolve(cwd)).split(sep).join("/");
272
+ const nearest = selected ?? [...plan.scopes].sort((a, b) => b.path.length - a.path.length)
273
+ .find(scope => scope.path !== "" && (path === scope.path || path.startsWith(`${scope.path}/`)));
274
+ return { ...(nearest ?? plan.scopes.find(scope => scope.path === ""))?.effective };
275
+ }
@@ -17,7 +17,9 @@ function truncateUtf8(value, maxBytes) {
17
17
  return output;
18
18
  }
19
19
  export function extractIncomingMessage(wakePrompt) {
20
- const match = wakePrompt.match(/(?:^|\n)(?:Incoming message|来信):[ \t]*(.*?)(?=\n(?:Start with crew|先用 crew)|$)/su);
20
+ // Keep legacy wake delimiters while accepting program-delivered-context prompts.
21
+ // Stop before policy/history blocks; neither is the current user's message.
22
+ const match = wakePrompt.match(/(?:^|\n)(?:Incoming message|来信):[ \t]*(.*?)(?=\n(?:Start with crew|先用 crew|Work directly from this thread's context|直接依据程序交付的本线程上下文|No thread anchor:|没有线程锚点[,,])|$)/su);
21
23
  const content = match?.[1]?.trim();
22
24
  return content ? content : null;
23
25
  }
@@ -1,5 +1,6 @@
1
1
  import { homedir } from "node:os";
2
2
  import { posix, win32 } from "node:path";
3
+ import { z } from "zod";
3
4
  import { assertProfileAgentsRootUnique, listProfiles, loadProfile, MachineTokenSchema, removeProfile, saveProfile, windowsDpapiProtector, } from "./computer-profile.js";
4
5
  import { buildServiceSpec, captureExactService, hardenWindowsProfile, replaceExactService, restoreExactService, serviceAction, serviceStatus, systemCommandRunner, uninstallService, } from "./computer-service.js";
5
6
  import { daemonGlobalInstallation } from "./daemon-installation.js";
@@ -14,6 +15,8 @@ import { daemonRegistryCandidates } from "./daemon-registry.js";
14
15
  import { npmInvocation } from "./npm-command.js";
15
16
  import { discoverExactLegacyService, } from "./automatic-service-migration.js";
16
17
  import { resolveDaemonLocalStateRootFromEnv } from "./daemon-local-home.js";
18
+ import { serverHostIdentity } from "./server-url.js";
19
+ import { computerSetupRecoveryGuidance } from "./computer-setup-client.js";
17
20
  function profileStorage(platform, userHome, runner) {
18
21
  return {
19
22
  platform,
@@ -27,7 +30,7 @@ function profileStorage(platform, userHome, runner) {
27
30
  };
28
31
  }
29
32
  function sameHostname(left, right) {
30
- return new URL(left).hostname.toLowerCase() === new URL(right).hostname.toLowerCase();
33
+ return serverHostIdentity(left) === serverHostIdentity(right);
31
34
  }
32
35
  function localHomeForTarget(target, platform) {
33
36
  const paths = platform === "win32" ? win32 : posix;
@@ -246,8 +249,33 @@ function recoveryError(primary, failures) {
246
249
  return original;
247
250
  return new Error(`${original.message}; recovery failed: ${failures.map((error) => error.message).join("; ")}`, { cause: original });
248
251
  }
252
+ function setupIdentity(input) {
253
+ if (input.workspaceId === undefined && input.machineId === undefined)
254
+ return null;
255
+ if (input.workspaceId === undefined || input.machineId === undefined) {
256
+ throw new Error("Computer setup requires both Workspace and Machine identity");
257
+ }
258
+ return {
259
+ workspaceId: z.string().uuid().parse(input.workspaceId),
260
+ machineId: z.string().uuid().parse(input.machineId),
261
+ };
262
+ }
263
+ function assertExistingProfileIdentity(previous, identity) {
264
+ if (previous === null || identity === null)
265
+ return;
266
+ if (previous.workspaceId === undefined || previous.machineId === undefined) {
267
+ throw new Error(`Profile '${previous.name}' does not record its Workspace and Machine identity`);
268
+ }
269
+ if (previous.workspaceId !== identity.workspaceId) {
270
+ throw new Error(`Profile '${previous.name}' belongs to a different Workspace identity`);
271
+ }
272
+ if (previous.machineId !== identity.machineId) {
273
+ throw new Error(`Profile '${previous.name}' belongs to a different Machine identity`);
274
+ }
275
+ }
249
276
  export async function installDaemonAutomatically(input, dependencies = defaultDependencies()) {
250
277
  const machineToken = MachineTokenSchema.parse(input.machineToken);
278
+ const identity = setupIdentity(input);
251
279
  const requestedLocalHome = input.requestedLocalHome ?? dependencies.environment.NOWWORK_LOCAL_HOME;
252
280
  const requestedLocalHomeMode = input.requestedLocalHomeMode
253
281
  ?? dependencies.environment.NOWWORK_LOCAL_HOME_MODE
@@ -280,6 +308,7 @@ export async function installDaemonAutomatically(input, dependencies = defaultDe
280
308
  });
281
309
  const previousProfile = await dependencies.captureProfile(target);
282
310
  await dependencies.assertProfileTarget(target, previousProfile);
311
+ assertExistingProfileIdentity(previousProfile, identity);
283
312
  const previousService = await dependencies.captureService(spec, target);
284
313
  if (previousService.installed && !previousService.exact) {
285
314
  throw new Error(`Installed service '${spec.id}' does not match the requested identity`);
@@ -330,6 +359,7 @@ export async function installDaemonAutomatically(input, dependencies = defaultDe
330
359
  name: target.profile,
331
360
  serverUrl: target.serverUrl,
332
361
  machineToken,
362
+ ...(identity === null ? {} : identity),
333
363
  agentsRoot: target.agentsRoot,
334
364
  releaseTag: target.releaseTag,
335
365
  ...(target.registryUrl === undefined ? {} : { registryUrl: target.registryUrl }),
@@ -341,14 +371,33 @@ export async function installDaemonAutomatically(input, dependencies = defaultDe
341
371
  const status = await dependencies.verifyService(spec);
342
372
  if (!status.running)
343
373
  throw new Error(`Service '${spec.id}' did not reach running state`);
344
- return {
374
+ const result = {
345
375
  profile: target.profile,
346
376
  version: dependencies.version,
347
377
  serviceId: spec.id,
348
378
  running: true,
379
+ localHome: localHomeForTarget(target, dependencies.platform),
380
+ daemonHome: target.daemonHome,
381
+ agentsRoot: target.agentsRoot,
349
382
  };
383
+ await input.verifyAfterStart?.(result);
384
+ return result;
350
385
  }
351
386
  catch (error) {
387
+ try {
388
+ await input.beforeRollback?.();
389
+ }
390
+ catch (rollbackError) {
391
+ throw new Error(`${error.message}; local recovery blocked: ${rollbackError.message}. `
392
+ + computerSetupRecoveryGuidance({
393
+ serverUrl: target.serverUrl,
394
+ workspaceId: input.workspaceId ?? "unknown",
395
+ machineId: input.machineId ?? "unknown",
396
+ profile: target.profile,
397
+ serviceId: spec.id,
398
+ daemonHome: target.daemonHome,
399
+ }), { cause: error });
400
+ }
352
401
  const failures = [];
353
402
  if (profileSaved) {
354
403
  try {
@@ -4,6 +4,14 @@ import { normalizeDaemonNpmRegistry } from "./npm-registry.js";
4
4
  import { resolveDaemonLocalHome } from "./daemon-local-home.js";
5
5
  const PROFILE_NAME = /^[a-z0-9][a-z0-9_-]{0,47}$/;
6
6
  const KNOWN_TARGETS = {
7
+ // Canonical production hostname. Keep the internal hostname below as a
8
+ // compatibility alias so existing profiles and service identities continue
9
+ // to work when users move between the two URLs.
10
+ "nowwork.nowcoder.com": {
11
+ environment: "production",
12
+ daemonDirectory: "daemon",
13
+ agentsDirectory: "agents",
14
+ },
7
15
  "nowwork-in.nowcoder.com": {
8
16
  environment: "production",
9
17
  daemonDirectory: "daemon",
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ import { detectDaemonLang, formatDaemonText } from "./i18n.js";
3
+ import { runComputerSetupCommand, setupUsage } from "./computer-setup.js";
4
+ import { redactSetupSecrets } from "./computer-setup-client.js";
5
+ async function main() {
6
+ const result = await runComputerSetupCommand(process.argv.slice(2));
7
+ if (result !== null) {
8
+ process.exitCode = result;
9
+ return;
10
+ }
11
+ const lang = detectDaemonLang();
12
+ process.stderr.write(`${formatDaemonText(lang, "Usage:")}\n${setupUsage((message, values) => formatDaemonText(lang, message, values))}`);
13
+ process.exitCode = 2;
14
+ }
15
+ main().catch((error) => {
16
+ process.stderr.write(`nowwork-computer: ${redactSetupSecrets(error.message)}\n`);
17
+ process.exitCode = 1;
18
+ });
@@ -21,11 +21,12 @@ const ProfileFields = z.object({
21
21
  name: z.string().regex(PROFILE_NAME),
22
22
  serverUrl: ServerUrlSchema,
23
23
  machineToken: MachineTokenSchema,
24
+ workspaceId: z.string().uuid().optional(),
25
+ machineId: z.string().uuid().optional(),
24
26
  agentsRoot: z.string().min(1).optional(),
25
27
  runtimePath: z.string().min(1).max(32768).refine((value) => !value.includes("\0") && !value.includes("\n") && !value.includes("\r"), "runtimePath must be a single line").optional(),
26
28
  releaseTag: z.enum(["latest", "beta"]).optional(),
27
29
  registryUrl: z.string().url().optional(),
28
- codexHomeMode: z.enum(["shared", "private"]).optional(),
29
30
  }).strict();
30
31
  function validateProfileRegistry(value, context) {
31
32
  const fields = value;
@@ -60,7 +61,13 @@ const StoredProtectedProfileSchema = ProfileFields.omit({ machineToken: true }).
60
61
  ciphertext: z.string().min(1).max(16384),
61
62
  }).strict(),
62
63
  }).strict().superRefine(validateProfileRegistry);
63
- const StoredProfileSchema = z.union([StoredPlainProfileSchema, StoredProtectedProfileSchema]);
64
+ // 旧版本可能已保存 shared 字段;读取时丢弃退休设置,其余字段仍严格校验。
65
+ const StoredProfileSchema = z.preprocess((raw) => {
66
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw))
67
+ return raw;
68
+ const { codexHomeMode: _retiredMode, ...fields } = raw;
69
+ return fields;
70
+ }, z.union([StoredPlainProfileSchema, StoredProtectedProfileSchema]));
64
71
  export const PROFILE_AGENTS_ROOT_CONFLICT_MESSAGE = "Profile '{{profile}}' conflicts with profile '{{conflict}}': both resolve to agents root '{{agentsRoot}}'. Save it with a unique root, for example: {{command}}";
65
72
  export class ProfileAgentsRootConflictError extends Error {
66
73
  profile;
@@ -253,11 +260,12 @@ export async function saveProfile(input, home = daemonHome(), options = {}) {
253
260
  version: 1,
254
261
  name: profile.name,
255
262
  serverUrl: profile.serverUrl,
263
+ ...(profile.workspaceId === undefined ? {} : { workspaceId: profile.workspaceId }),
264
+ ...(profile.machineId === undefined ? {} : { machineId: profile.machineId }),
256
265
  ...(profile.agentsRoot ? { agentsRoot: profile.agentsRoot } : {}),
257
266
  ...(profile.runtimePath ? { runtimePath: profile.runtimePath } : {}),
258
267
  ...(profile.releaseTag === undefined ? {} : { releaseTag: profile.releaseTag }),
259
268
  ...(profile.registryUrl === undefined ? {} : { registryUrl: profile.registryUrl }),
260
- ...(profile.codexHomeMode === undefined ? {} : { codexHomeMode: profile.codexHomeMode }),
261
269
  machineTokenProtected: { scheme: "dpapi-current-user", ciphertext },
262
270
  });
263
271
  await atomicPrivateWrite(profilePath(profile.name, home), `${JSON.stringify(stored, null, 2)}\n`, configured.harden);
@@ -298,6 +306,8 @@ export async function loadProfile(name, home = daemonHome(), options = {}) {
298
306
  version: stored.version,
299
307
  name: stored.name,
300
308
  serverUrl: stored.serverUrl,
309
+ ...(stored.workspaceId === undefined ? {} : { workspaceId: stored.workspaceId }),
310
+ ...(stored.machineId === undefined ? {} : { machineId: stored.machineId }),
301
311
  ...(stored.agentsRoot ? { agentsRoot: stored.agentsRoot } : {}),
302
312
  ...(stored.runtimePath ? { runtimePath: stored.runtimePath } : {}),
303
313
  ...(stored.releaseTag === undefined ? {} : { releaseTag: stored.releaseTag }),
@@ -384,6 +394,8 @@ export function publicProfile(profile) {
384
394
  name: profile.name,
385
395
  serverUrl: profile.serverUrl,
386
396
  machineTokenConfigured: true,
397
+ ...(profile.workspaceId === undefined ? {} : { workspaceId: profile.workspaceId }),
398
+ ...(profile.machineId === undefined ? {} : { machineId: profile.machineId }),
387
399
  ...(profile.agentsRoot ? { agentsRoot: profile.agentsRoot } : {}),
388
400
  };
389
401
  }
@@ -404,5 +416,6 @@ export function applyProfileToEnv(profile, env) {
404
416
  env.CREW_NPM_REGISTRY = profile.registryUrl;
405
417
  else
406
418
  delete env.CREW_NPM_REGISTRY;
407
- env.CREW_CODEX_HOME_MODE = profile.codexHomeMode ?? "shared";
419
+ // 不再向运行时传播退休的模式开关;CODEX_HOME 沿用用户环境。
420
+ delete env.CREW_CODEX_HOME_MODE;
408
421
  }