@nowcrew/daemon 0.6.89 → 0.6.91-beta.1

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 (116) hide show
  1. package/README.md +64 -3
  2. package/dist/agent-ability/materializer.js +21 -10
  3. package/dist/agent-environment-contract.js +55 -0
  4. package/dist/agent-environment-control.js +24 -0
  5. package/dist/agent-environment-execution.js +98 -0
  6. package/dist/agent-environments.js +275 -0
  7. package/dist/automatic-daemon-installer.js +49 -1
  8. package/dist/computer-main.js +18 -0
  9. package/dist/computer-profile.js +17 -4
  10. package/dist/computer-setup-client.js +318 -0
  11. package/dist/computer-setup-orchestration.js +203 -0
  12. package/dist/computer-setup.js +244 -0
  13. package/dist/config.js +30 -12
  14. package/dist/console-formatter.js +3 -3
  15. package/dist/console.js +15 -2
  16. package/dist/daemon-update-eligibility.js +1 -1
  17. package/dist/daemon-updater.js +2 -12
  18. package/dist/execution-journal.js +9 -6
  19. package/dist/execution-protocol.js +1 -0
  20. package/dist/execution-runner.js +13 -8
  21. package/dist/i18n.js +19 -0
  22. package/dist/list-models.js +12 -12
  23. package/dist/local-execution-lease.js +22 -0
  24. package/dist/local-executor.js +57 -84
  25. package/dist/local-memory/candidate.js +43 -0
  26. package/dist/local-memory/index-budget.js +8 -0
  27. package/dist/local-memory/inventory-cli.js +61 -0
  28. package/dist/local-memory/inventory-preview.js +159 -0
  29. package/dist/local-memory/inventory-source.js +159 -0
  30. package/dist/local-memory/pending-match.js +29 -0
  31. package/dist/local-memory/pending-query.js +100 -0
  32. package/dist/local-memory/pending-schema.js +29 -0
  33. package/dist/local-memory/pending-source.js +126 -0
  34. package/dist/local-memory/pending.js +34 -84
  35. package/dist/local-memory/prompt.js +19 -12
  36. package/dist/local-memory/read-cli.js +97 -12
  37. package/dist/local-memory/rollout.js +82 -0
  38. package/dist/local-memory/runtime.js +23 -17
  39. package/dist/local-memory/work-log-execution.js +31 -0
  40. package/dist/local-memory/work-log.js +58 -0
  41. package/dist/local-memory/write.js +27 -58
  42. package/dist/local-runtime-environment.js +50 -0
  43. package/dist/machine-environment-cache.js +147 -0
  44. package/dist/machine-environment-contract.js +10 -0
  45. package/dist/machine-environment-control.js +27 -0
  46. package/dist/machine-environment-policy.js +75 -0
  47. package/dist/machine-environment-process.js +110 -0
  48. package/dist/machine-info.js +70 -25
  49. package/dist/machine-runtime-environment.js +189 -0
  50. package/dist/machine-shell-environment-capture.js +135 -0
  51. package/dist/machine-windows-environment-capture.js +215 -0
  52. package/dist/machine-windows-environment-script.js +158 -0
  53. package/dist/main.js +4 -69
  54. package/dist/marketplace-command.js +21 -0
  55. package/dist/marketplace-controller.js +80 -0
  56. package/dist/marketplace-protocol.js +38 -0
  57. package/dist/marketplace-remote.js +48 -0
  58. package/dist/marketplace-runner.js +235 -0
  59. package/dist/marketplace-serve.js +41 -0
  60. package/dist/marketplace-source.js +103 -0
  61. package/dist/message-file-preview.js +246 -0
  62. package/dist/pre-admission-rejection.js +64 -0
  63. package/dist/project-skills/execution-adapter.js +3 -2
  64. package/dist/project-skills/runtime-launch.js +14 -0
  65. package/dist/provider-env.js +26 -15
  66. package/dist/runner.js +17 -4
  67. package/dist/runtime-environment-merge.js +16 -0
  68. package/dist/runtime-health-handler.js +10 -0
  69. package/dist/runtime-health.js +3 -1
  70. package/dist/runtime-path.js +22 -12
  71. package/dist/runtime-probe.js +35 -6
  72. package/dist/runtimes/acp-probe-transport.js +69 -0
  73. package/dist/runtimes/claude-models.js +20 -9
  74. package/dist/runtimes/deepseek-harness-launch.js +10 -0
  75. package/dist/runtimes/deepseek-harness-profile.js +92 -0
  76. package/dist/runtimes/deepseek-harness-runner.js +226 -0
  77. package/dist/runtimes/deepseek-harness.js +76 -15
  78. package/dist/runtimes/harness-probe-retry.js +42 -0
  79. package/dist/runtimes/hermes-models.js +2 -1
  80. package/dist/runtimes/kimi-acp-runner.js +78 -13
  81. package/dist/runtimes/opencode.js +2 -2
  82. package/dist/serve.js +88 -178
  83. package/dist/storage-identity.js +0 -3
  84. package/dist/storage-serve-adapter.js +6 -72
  85. package/dist/supervised-runtime.js +17 -2
  86. package/dist/token.js +0 -1
  87. package/dist/workspace-control-handler.js +27 -0
  88. package/dist/workspace-fs.js +66 -15
  89. package/dist/workspace.js +1 -1
  90. package/package.json +13 -12
  91. package/dist/codex-home-launch.js +0 -7
  92. package/dist/codex-home-mode-controller.js +0 -68
  93. package/dist/daemon-migration-controller.js +0 -211
  94. package/dist/daemon-migration-wiring.js +0 -22
  95. package/dist/runtimes/codex-home-migration-cli.js +0 -26
  96. package/dist/runtimes/codex-home-migration.js +0 -112
  97. package/dist/runtimes/codex-home.js +0 -236
  98. package/dist/storage-control.js +0 -212
  99. package/dist/storage-copy.js +0 -628
  100. package/dist/storage-credential-guard.js +0 -21
  101. package/dist/storage-doctor.js +0 -228
  102. package/dist/storage-manifest.js +0 -1027
  103. package/dist/storage-migration-controller.js +0 -440
  104. package/dist/storage-migration-manifest.js +0 -105
  105. package/dist/storage-migration-policy.js +0 -100
  106. package/dist/storage-runtime-controller.js +0 -381
  107. package/dist/storage-runtime-readiness.js +0 -241
  108. package/dist/storage-safe-fs.js +0 -302
  109. package/dist/storage-status-delivery.js +0 -224
  110. package/dist/storage-status-replay.js +0 -140
  111. package/dist/storage-target-confirmation.js +0 -129
  112. package/dist/storage-worker-command.js +0 -37
  113. package/dist/storage-worker-executor.js +0 -447
  114. package/dist/storage-worker-handoff.js +0 -151
  115. package/dist/storage-worker-operation.js +0 -294
  116. package/dist/storage-worker-status.js +0 -259
@@ -0,0 +1,203 @@
1
+ import { computerSetupRecoveryGuidance, redactSetupSecrets, } from "./computer-setup-client.js";
2
+ import { isProductionServerHostname, serverUrlsMatch } from "./server-url.js";
3
+ import { posix, win32 } from "node:path";
4
+ import { resolveAgentsRoot } from "./computer-profile.js";
5
+ function safeError(error, machineToken) {
6
+ const message = redactSetupSecrets(error instanceof Error ? error.message : String(error), machineToken === undefined ? [] : [machineToken]);
7
+ return new Error(message, error instanceof Error ? { cause: error } : undefined);
8
+ }
9
+ function assertBootstrapMatches(bootstrap, input) {
10
+ if (bootstrap.machineId !== input.args.machineId) {
11
+ throw new Error("setup bootstrap machine does not match --machine");
12
+ }
13
+ const workspaceMatches = input.args.workspace.kind === "id"
14
+ ? input.args.workspace.value === bootstrap.workspaceId
15
+ : bootstrap.workspaceSlug !== undefined && input.args.workspace.value === bootstrap.workspaceSlug;
16
+ if (!workspaceMatches)
17
+ throw new Error("setup bootstrap Workspace does not match the requested Workspace");
18
+ const bootstrapUrl = new URL(bootstrap.serverUrl);
19
+ const requestedUrl = new URL(input.serverUrl);
20
+ if (!serverUrlsMatch(bootstrapUrl.origin, requestedUrl.origin)) {
21
+ throw new Error("setup bootstrap server does not match the requested server context");
22
+ }
23
+ const hostname = bootstrapUrl.hostname.toLowerCase();
24
+ const knownEnvironment = hostname === "nowwork-dev.nowcoder.com"
25
+ ? "dev"
26
+ : isProductionServerHostname(hostname)
27
+ ? "production"
28
+ : null;
29
+ if (knownEnvironment !== null) {
30
+ const expectedReleaseTag = knownEnvironment === "dev" ? "beta" : "latest";
31
+ if (bootstrap.environment !== knownEnvironment) {
32
+ throw new Error("setup bootstrap environment does not match the server hostname");
33
+ }
34
+ if (bootstrap.releaseTag !== undefined && bootstrap.releaseTag !== expectedReleaseTag) {
35
+ throw new Error("setup bootstrap release tag does not match the server hostname");
36
+ }
37
+ }
38
+ }
39
+ function secondsUntil(expiresAt, now) {
40
+ const remaining = Date.parse(expiresAt) - now();
41
+ return Math.max(0, Math.ceil(remaining / 1_000));
42
+ }
43
+ function localPathsMatch(left, right, platform) {
44
+ const paths = platform === "win32" ? win32 : posix;
45
+ if (!paths.isAbsolute(left) || !paths.isAbsolute(right))
46
+ return false;
47
+ const normalize = (value) => {
48
+ const resolved = resolveAgentsRoot(value, undefined, platform);
49
+ return platform === "win32" ? win32.normalize(resolved).toLocaleLowerCase("en-US") : resolved;
50
+ };
51
+ return normalize(left) === normalize(right);
52
+ }
53
+ async function obtainBootstrap(input, deps) {
54
+ const pairing = await deps.client.start({
55
+ workspace: input.args.workspace,
56
+ machineId: input.args.machineId,
57
+ });
58
+ deps.reportPairing(`Open ${pairing.verificationUri}`);
59
+ deps.reportPairing(`Code ${pairing.userCode}`);
60
+ try {
61
+ await deps.openBrowser(pairing.verificationUri);
62
+ }
63
+ catch (error) {
64
+ deps.reportPairing(`Browser open skipped: ${redactSetupSecrets(error.message)}`);
65
+ }
66
+ let intervalSeconds = pairing.intervalSeconds;
67
+ for (;;) {
68
+ const remainingSeconds = secondsUntil(pairing.expiresAt, deps.now);
69
+ if (remainingSeconds <= 0)
70
+ throw new Error("computer setup pairing expired");
71
+ await deps.sleep(Math.min(intervalSeconds * 1_000, remainingSeconds * 1_000));
72
+ const result = await deps.client.poll(pairing.deviceCode);
73
+ if (result.status === "authorization_pending" || result.status === "slow_down") {
74
+ intervalSeconds = result.intervalSeconds;
75
+ continue;
76
+ }
77
+ if (result.status === "access_denied")
78
+ throw new Error("computer setup pairing was denied");
79
+ if (result.status === "expired_token")
80
+ throw new Error("computer setup pairing expired");
81
+ return result.bootstrap;
82
+ }
83
+ }
84
+ export async function runComputerSetupOrchestration(input, deps) {
85
+ let machineToken;
86
+ let bootstrap;
87
+ let credentialFinalized = false;
88
+ let verifiedStorageId;
89
+ let rollbackAttempt;
90
+ const confirmRollback = () => {
91
+ // Reuse both success and failure so an outer catch cannot change the recovery decision.
92
+ rollbackAttempt ??= (async () => {
93
+ if (credentialFinalized)
94
+ throw new Error("computer setup credentials already finalized");
95
+ if (bootstrap === undefined || machineToken === undefined)
96
+ throw new Error("computer setup credentials unavailable");
97
+ for (let attempt = 0;; attempt += 1) {
98
+ try {
99
+ const confirmed = await deps.client.rollback({ setupId: bootstrap.setupId, machineToken });
100
+ if (confirmed !== true)
101
+ throw new Error("computer setup rollback not confirmed");
102
+ return;
103
+ }
104
+ catch (error) {
105
+ if (attempt >= 2)
106
+ throw error;
107
+ await deps.sleep(1_000);
108
+ }
109
+ }
110
+ })();
111
+ return rollbackAttempt;
112
+ };
113
+ try {
114
+ const authorized = await obtainBootstrap(input, deps);
115
+ const token = authorized.machineToken;
116
+ bootstrap = authorized;
117
+ machineToken = token;
118
+ assertBootstrapMatches(authorized, input);
119
+ const installation = await deps.installAutomatically({
120
+ serverUrl: authorized.serverUrl,
121
+ machineToken: token,
122
+ workspaceId: authorized.workspaceId,
123
+ machineId: authorized.machineId,
124
+ ...(authorized.profile === undefined ? {} : { requestedProfile: authorized.profile }),
125
+ ...(authorized.releaseTag === "latest" || authorized.releaseTag === "beta"
126
+ ? { requestedReleaseTag: authorized.releaseTag }
127
+ : {}),
128
+ ...(authorized.registryUrl === undefined ? {} : { requestedRegistryUrl: authorized.registryUrl }),
129
+ beforeRollback: confirmRollback,
130
+ verifyAfterStart: async (candidate) => {
131
+ const hello = await deps.client.waitForHello({
132
+ machineToken: token,
133
+ machineId: authorized.machineId,
134
+ workspaceId: authorized.workspaceId,
135
+ profile: candidate.profile,
136
+ setupId: authorized.setupId,
137
+ minimumConnectionEpoch: authorized.minimumConnectionEpoch,
138
+ });
139
+ if (hello.machineId !== authorized.machineId)
140
+ throw new Error("machine hello identity mismatch");
141
+ if (hello.workspaceId !== authorized.workspaceId)
142
+ throw new Error("machine hello workspace identity mismatch");
143
+ if (hello.environment !== authorized.environment)
144
+ throw new Error("machine hello environment identity mismatch");
145
+ if (hello.profile !== candidate.profile)
146
+ throw new Error("machine hello profile identity mismatch");
147
+ const platform = deps.platform ?? process.platform;
148
+ if (!localPathsMatch(hello.daemonHome, candidate.daemonHome, platform))
149
+ throw new Error("machine hello daemon home mismatch");
150
+ if (!localPathsMatch(hello.agentsRoot, candidate.agentsRoot, platform))
151
+ throw new Error("machine hello agents root mismatch");
152
+ if (authorized.storageId !== undefined && hello.storageId !== authorized.storageId) {
153
+ throw new Error("machine hello storage identity mismatch");
154
+ }
155
+ verifiedStorageId = hello.storageId;
156
+ let finalizeError;
157
+ for (let attempt = 0; attempt < 3; attempt += 1) {
158
+ try {
159
+ await deps.client.finalize({ setupId: authorized.setupId, machineToken: token });
160
+ credentialFinalized = true;
161
+ finalizeError = undefined;
162
+ break;
163
+ }
164
+ catch (error) {
165
+ finalizeError = error;
166
+ if (attempt < 2)
167
+ await deps.sleep(1_000);
168
+ }
169
+ }
170
+ if (!credentialFinalized) {
171
+ throw finalizeError ?? new Error("computer setup credential finalization failed");
172
+ }
173
+ },
174
+ });
175
+ return {
176
+ workspaceId: authorized.workspaceId,
177
+ machineId: authorized.machineId,
178
+ environment: authorized.environment,
179
+ profile: installation.profile,
180
+ daemonVersion: installation.version,
181
+ serviceId: installation.serviceId,
182
+ serviceStatus: "running",
183
+ helloConfirmed: true,
184
+ ...(authorized.environmentId === undefined ? {} : { environmentId: authorized.environmentId }),
185
+ ...(verifiedStorageId === undefined ? {} : { storageId: verifiedStorageId }),
186
+ localNowworkHome: installation.localHome,
187
+ daemonHome: installation.daemonHome,
188
+ agentsRoot: installation.agentsRoot,
189
+ };
190
+ }
191
+ catch (error) {
192
+ if (bootstrap !== undefined && machineToken !== undefined && !credentialFinalized && rollbackAttempt === undefined) {
193
+ try {
194
+ await confirmRollback();
195
+ }
196
+ catch (rollbackError) {
197
+ error = new Error(`${error.message}; computer setup rollback failed: ${rollbackError.message}; `
198
+ + `local recovery blocked. ${computerSetupRecoveryGuidance(bootstrap)}`, { cause: error });
199
+ }
200
+ }
201
+ throw safeError(error, machineToken);
202
+ }
203
+ }
@@ -0,0 +1,244 @@
1
+ import { parseArgs } from "node:util";
2
+ import { z } from "zod";
3
+ import { DEFAULT_SERVER_URL } from "./config.js";
4
+ import { installDaemonAutomatically, } from "./automatic-daemon-installer.js";
5
+ import { createComputerSetupClient, openSetupBrowser, redactSetupSecrets, } from "./computer-setup-client.js";
6
+ import { runComputerSetupOrchestration, } from "./computer-setup-orchestration.js";
7
+ import { detectDaemonLang, formatDaemonText } from "./i18n.js";
8
+ import { systemCommandRunner } from "./computer-service.js";
9
+ const WorkspaceSlugSchema = z.string()
10
+ .min(2)
11
+ .max(40)
12
+ .regex(/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/u);
13
+ const WorkspaceIdSchema = z.string().uuid();
14
+ const MachineIdSchema = z.string().uuid();
15
+ const ServerUrlSchema = z.string().url().refine((value) => {
16
+ const url = new URL(value);
17
+ return (url.protocol === "http:" || url.protocol === "https:")
18
+ && url.username.length === 0
19
+ && url.password.length === 0
20
+ && (url.pathname === "" || url.pathname === "/")
21
+ && url.search.length === 0
22
+ && url.hash.length === 0;
23
+ }, "server URL must be an HTTP(S) origin without embedded credentials");
24
+ function stringValue(values, key) {
25
+ const value = values[key];
26
+ return typeof value === "string" ? value : undefined;
27
+ }
28
+ function booleanValue(values, key) {
29
+ return values[key] === true;
30
+ }
31
+ function optionOccurrences(argv, name) {
32
+ const prefix = `--${name}`;
33
+ const values = [];
34
+ for (let index = 0; index < argv.length; index += 1) {
35
+ const value = argv[index];
36
+ if (value === prefix) {
37
+ const next = argv[index + 1];
38
+ if (next !== undefined && !next.startsWith("--"))
39
+ values.push(next);
40
+ index += 1;
41
+ continue;
42
+ }
43
+ if (value?.startsWith(`${prefix}=`))
44
+ values.push(value.slice(prefix.length + 1));
45
+ }
46
+ return values;
47
+ }
48
+ function assertRepeatedOptionValuesAgree(argv, name, normalize) {
49
+ const normalized = optionOccurrences(argv, name).map(normalize);
50
+ if (new Set(normalized).size > 1) {
51
+ throw new Error(`Repeated --${name} values conflict`);
52
+ }
53
+ }
54
+ function normalizedServerOrigin(value) {
55
+ const parsed = new URL(ServerUrlSchema.parse(value));
56
+ return `${parsed.protocol}//${parsed.host}`;
57
+ }
58
+ function workspaceFromSlug(value) {
59
+ if (!value.startsWith("/") || value.startsWith("//")) {
60
+ throw new Error("Workspace slug must use exactly one leading slash, for example /nowcoder-app");
61
+ }
62
+ return WorkspaceSlugSchema.parse(value.slice(1));
63
+ }
64
+ function workspaceFromAlias(value) {
65
+ return value.startsWith("/") ? workspaceFromSlug(value) : WorkspaceSlugSchema.parse(value);
66
+ }
67
+ export function parseSetupArgs(argv) {
68
+ assertRepeatedOptionValuesAgree(argv, "workspace", (value) => workspaceFromAlias(value));
69
+ assertRepeatedOptionValuesAgree(argv, "workspace-id", (value) => WorkspaceIdSchema.parse(value).toLowerCase());
70
+ assertRepeatedOptionValuesAgree(argv, "machine", (value) => MachineIdSchema.parse(value).toLowerCase());
71
+ assertRepeatedOptionValuesAgree(argv, "machine-id", (value) => MachineIdSchema.parse(value).toLowerCase());
72
+ assertRepeatedOptionValuesAgree(argv, "server-url", normalizedServerOrigin);
73
+ const parsed = parseArgs({
74
+ args: [...argv],
75
+ allowPositionals: true,
76
+ strict: true,
77
+ options: {
78
+ workspace: { type: "string" },
79
+ "workspace-id": { type: "string" },
80
+ machine: { type: "string" },
81
+ "machine-id": { type: "string" },
82
+ "server-url": { type: "string" },
83
+ json: { type: "boolean", default: false },
84
+ "no-browser": { type: "boolean", default: false },
85
+ help: { type: "boolean", short: "h", default: false },
86
+ },
87
+ });
88
+ const values = parsed.values;
89
+ const [command, ...positionals] = parsed.positionals;
90
+ if (command !== "setup")
91
+ throw new Error("Expected 'setup'");
92
+ if (booleanValue(values, "help"))
93
+ throw new Error(setupUsage());
94
+ if (positionals.length > 1)
95
+ throw new Error("Setup accepts one Workspace argument");
96
+ const workspaceAlias = stringValue(values, "workspace");
97
+ const workspaceIdAlias = stringValue(values, "workspace-id");
98
+ if (workspaceAlias !== undefined && workspaceIdAlias !== undefined) {
99
+ throw new Error("Choose one Workspace source: positional, --workspace, or --workspace-id");
100
+ }
101
+ const positionalWorkspace = positionals[0] === undefined ? undefined : workspaceFromSlug(positionals[0]);
102
+ const aliasWorkspace = workspaceAlias === undefined ? undefined : workspaceFromAlias(workspaceAlias);
103
+ if (positionalWorkspace !== undefined && workspaceIdAlias !== undefined) {
104
+ throw new Error("A positional Workspace slug cannot be combined with --workspace-id");
105
+ }
106
+ if (positionalWorkspace !== undefined && aliasWorkspace !== undefined && positionalWorkspace !== aliasWorkspace) {
107
+ throw new Error("Positional Workspace conflicts with --workspace");
108
+ }
109
+ const workspace = positionalWorkspace !== undefined
110
+ ? Object.freeze({ kind: "slug", value: positionalWorkspace })
111
+ : aliasWorkspace !== undefined
112
+ ? Object.freeze({ kind: "slug", value: aliasWorkspace })
113
+ : workspaceIdAlias === undefined
114
+ ? (() => { throw new Error("Missing Workspace; use /<slug> or --workspace-id <uuid>"); })()
115
+ : Object.freeze({ kind: "id", value: WorkspaceIdSchema.parse(workspaceIdAlias).toLowerCase() });
116
+ const machineAlias = stringValue(values, "machine");
117
+ const machineIdAlias = stringValue(values, "machine-id");
118
+ const parsedMachine = MachineIdSchema.parse(machineAlias ?? machineIdAlias ?? (() => {
119
+ throw new Error("Missing --machine <uuid>");
120
+ })()).toLowerCase();
121
+ if (machineAlias !== undefined && machineIdAlias !== undefined
122
+ && parsedMachine !== MachineIdSchema.parse(machineIdAlias).toLowerCase()) {
123
+ throw new Error("--machine conflicts with --machine-id");
124
+ }
125
+ const machineId = parsedMachine;
126
+ const serverUrl = stringValue(values, "server-url");
127
+ return Object.freeze({
128
+ command: "setup",
129
+ workspace,
130
+ machineId,
131
+ ...(serverUrl === undefined ? {} : { serverUrl: ServerUrlSchema.parse(serverUrl) }),
132
+ ...(booleanValue(values, "json") ? { json: true } : {}),
133
+ ...(booleanValue(values, "no-browser") ? { noBrowser: true } : {}),
134
+ });
135
+ }
136
+ function resolveSetupServerUrl(args, env) {
137
+ const configured = args.serverUrl ?? env.CREW_SERVER_URL?.trim();
138
+ if (configured !== undefined && configured.length > 0)
139
+ return normalizedServerOrigin(configured);
140
+ if (env.NODE_ENV === "development" || env.NODE_ENV === "test")
141
+ return DEFAULT_SERVER_URL;
142
+ throw new Error("server URL is required; pass --server-url or set CREW_SERVER_URL");
143
+ }
144
+ export function setupUsage(td = (message) => message) {
145
+ return [
146
+ td("Usage:"),
147
+ " nowwork-computer setup /workspace --machine <uuid>",
148
+ " nowwork-computer setup --workspace /workspace --machine <uuid>",
149
+ " nowwork-computer setup --workspace-id <uuid> --machine-id <uuid>",
150
+ "",
151
+ "Options: --server-url <url> --json --no-browser",
152
+ ].join("\n") + "\n";
153
+ }
154
+ function defaults() {
155
+ return {
156
+ env: process.env,
157
+ platform: process.platform,
158
+ fetch: globalThis.fetch,
159
+ stdout: (text) => process.stdout.write(text),
160
+ stderr: (text) => process.stderr.write(text),
161
+ createClient: (serverUrl) => createComputerSetupClient({ serverUrl }),
162
+ installAutomatically: installDaemonAutomatically,
163
+ openBrowser: (url) => openSetupBrowser(url, process.platform, systemCommandRunner),
164
+ sleep: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
165
+ now: Date.now,
166
+ };
167
+ }
168
+ function humanSummary(result, td) {
169
+ return [
170
+ td("Workspace: {{value}}", { value: result.workspaceId }),
171
+ td("Machine: {{value}}", { value: result.machineId }),
172
+ td("Environment: {{value}}", { value: result.environment }),
173
+ td("Daemon: profile {{profile}} / v{{version}} / {{status}}", {
174
+ profile: result.profile, version: result.daemonVersion, status: result.serviceStatus,
175
+ }),
176
+ td("Local: {{value}}", { value: result.localNowworkHome }),
177
+ td("Config: {{value}}", { value: result.daemonHome }),
178
+ td("Agents: {{value}}", { value: result.agentsRoot }),
179
+ ...(result.storageId === undefined ? [] : [td("Storage ID: {{value}}", { value: result.storageId })]),
180
+ td(result.helloConfirmed ? "Status: connected and ready" : "Status: not ready"),
181
+ ].join("\n") + "\n";
182
+ }
183
+ function localizedPairingMessage(lang, message) {
184
+ if (message.startsWith("Open ")) {
185
+ return formatDaemonText(lang, "Open {{url}}", { url: message.slice("Open ".length) });
186
+ }
187
+ if (message.startsWith("Code ")) {
188
+ return formatDaemonText(lang, "Code {{code}}", { code: message.slice("Code ".length) });
189
+ }
190
+ if (message.startsWith("Browser open skipped: ")) {
191
+ return formatDaemonText(lang, "Browser open skipped: {{reason}}", {
192
+ reason: message.slice("Browser open skipped: ".length),
193
+ });
194
+ }
195
+ return formatDaemonText(lang, message);
196
+ }
197
+ export async function runComputerSetupCommand(argv, overrides = {}) {
198
+ if (argv.length === 1 && (argv[0] === "--help" || argv[0] === "-h")) {
199
+ const deps = { ...defaults(), ...overrides };
200
+ const lang = detectDaemonLang(deps.env);
201
+ deps.stdout(setupUsage((message, values) => formatDaemonText(lang, message, values)));
202
+ return 0;
203
+ }
204
+ if (argv[0] !== "setup")
205
+ return null;
206
+ const deps = { ...defaults(), ...overrides };
207
+ const lang = detectDaemonLang(deps.env);
208
+ try {
209
+ if (argv.includes("--help") || argv.includes("-h")) {
210
+ deps.stdout(setupUsage((message, values) => formatDaemonText(lang, message, values)));
211
+ return 0;
212
+ }
213
+ let args;
214
+ try {
215
+ args = parseSetupArgs(argv);
216
+ }
217
+ catch (error) {
218
+ deps.stderr(`nowwork-computer: ${redactSetupSecrets(error.message)}\n`);
219
+ return 2;
220
+ }
221
+ if (args.json === true) {
222
+ deps.stderr(`${formatDaemonText(lang, "Starting computer setup pairing...")}\n`);
223
+ }
224
+ const serverUrl = resolveSetupServerUrl(args, deps.env);
225
+ const createClient = overrides.createClient
226
+ ?? ((value) => createComputerSetupClient({ serverUrl: value, fetch: deps.fetch }));
227
+ const result = await runComputerSetupOrchestration({ args, serverUrl }, {
228
+ client: createClient(serverUrl),
229
+ installAutomatically: deps.installAutomatically,
230
+ openBrowser: args.noBrowser === true ? async () => { } : deps.openBrowser,
231
+ sleep: deps.sleep,
232
+ reportPairing: (message) => deps[args.json === true ? "stderr" : "stdout"](`${localizedPairingMessage(lang, message)}\n`),
233
+ now: deps.now,
234
+ });
235
+ deps.stdout(args.json === true
236
+ ? `${JSON.stringify(result)}\n`
237
+ : humanSummary(result, (message, values) => formatDaemonText(lang, message, values)));
238
+ return 0;
239
+ }
240
+ catch (error) {
241
+ deps.stderr(`nowwork-computer: ${redactSetupSecrets(error.message)}\n`);
242
+ return 1;
243
+ }
244
+ }
package/dist/config.js CHANGED
@@ -76,12 +76,32 @@ function defaultCliPath() {
76
76
  return resolve(here, "../../cli/dist/main.js");
77
77
  }
78
78
  }
79
- // 临时 dev 验收名单。这里必须填写 Server 生成的 Agent handle,而不是显示名称或频道 ID。
80
- // 测试完成后删除此常量,恢复未配置时默认关闭的行为。
79
+ // 保留本地开发的原默认名单;远端 dev 全量不再依赖该集合。
81
80
  const TEMP_LOCAL_MEMORY_V2_TEST_HANDLES = [
82
- "94533bf3-b5b7-4a1e-9c41-6a1ab0a0e1ea", // memory-v2-test-a
83
- "967ab9ce-724c-4f22-a664-2f855cd798af", // memory-v2-test-b
81
+ "94533bf3-b5b7-4a1e-9c41-6a1ab0a0e1ea", // memory-v2-test-b
82
+ "967ab9ce-724c-4f22-a664-2f855cd798af", // memory-v2-test-a
84
83
  ];
84
+ export function localMemoryV2Environment(serverUrl) {
85
+ try {
86
+ const url = new URL(serverUrl);
87
+ if (url.protocol !== "http:" && url.protocol !== "https:")
88
+ return "custom";
89
+ const hostname = url.hostname.toLowerCase().replace(/\.$/u, "");
90
+ if (hostname === "nowwork-dev.nowcoder.com")
91
+ return "dev";
92
+ if (hostname === "nowwork-in.nowcoder.com" || hostname === "nowwork.nowcoder.com")
93
+ return "production";
94
+ }
95
+ catch { /* 其它地址保留原配置行为。 */ }
96
+ return "custom";
97
+ }
98
+ /** 环境资格不代表旧格式已兼容;实际格式由本轮工作区准备阶段核对。 */
99
+ export function isLocalMemoryV2Enabled(config, handle) {
100
+ const environment = localMemoryV2Environment(config.serverUrl);
101
+ if (environment !== "custom")
102
+ return environment === "dev";
103
+ return config.localMemoryV2Handles?.has(handle) === true;
104
+ }
85
105
  function isTemporaryDevServer(serverUrl) {
86
106
  try {
87
107
  const hostname = new URL(serverUrl).hostname.toLowerCase();
@@ -111,13 +131,11 @@ function localMemoryV2Handles(env, serverUrl) {
111
131
  return new Set(handles);
112
132
  }
113
133
  export const DEFAULT_SERVER_URL = "http://127.0.0.1:3001";
114
- function codexHomeModeEnv(env) {
115
- const value = env.CREW_CODEX_HOME_MODE;
116
- if (value === undefined || value === "shared")
117
- return "shared";
118
- if (value === "private")
119
- return "private";
120
- throw new ConfigError("CREW_CODEX_HOME_MODE must be shared or private");
134
+ function machineEnvironmentModeEnv(env) {
135
+ const value = env.NOWWORK_MACHINE_ENV_MODE ?? "auto";
136
+ if (value === "auto" || value === "zsh" || value === "bash" || value === "windows" || value === "off")
137
+ return value;
138
+ throw new ConfigError("NOWWORK_MACHINE_ENV_MODE must be auto, zsh, bash, windows, or off");
121
139
  }
122
140
  function codexCollaborationToolsEnv(env) {
123
141
  const value = (env.CREW_CODEX_COLLABORATION_TOOLS ?? "").trim().toLowerCase();
@@ -168,7 +186,7 @@ export function loadConfig(env = process.env) {
168
186
  : defaultQaReportRoot,
169
187
  cliPath: env.CREW_CLI_PATH ?? defaultCliPath(),
170
188
  runtimeBin: env.CREW_RUNTIME ?? "claude",
171
- codexHomeMode: codexHomeModeEnv(env),
189
+ machineEnvironmentMode: machineEnvironmentModeEnv(env),
172
190
  codexCollaborationTools: codexCollaborationToolsEnv(env),
173
191
  dangerous: env.CREW_RUNTIME_SAFE !== "1", // 默认开启 (headless agent 在自有 workspace 内运行)
174
192
  resume: env.CREW_RESUME !== "off" && env.CREW_RESUME !== "0", // 默认开启;一键回退现状用 CREW_RESUME=off
@@ -22,7 +22,7 @@ const clip = (text) => text.length > TOOL_RESULT_CAP
22
22
  ? `${text.slice(0, TOOL_RESULT_CAP)}… (+${text.length - TOOL_RESULT_CAP})`
23
23
  : text;
24
24
  const SCRIPT_TOOLS = new Set(["Bash", "PowerShell", "Shell"]);
25
- export function createConsoleFormatter() {
25
+ export function createConsoleFormatter(runtime) {
26
26
  const pending = new Map();
27
27
  // listId → 上一条 todos 快照的指纹;仅用于丢弃「连续两次完全相同」的重复事件(无损去重)。
28
28
  // 只比对相邻快照,不做跨快照合并——进度推进的中间态必须逐条下发,否则前端进度条会停在旧值。
@@ -86,7 +86,7 @@ export function createConsoleFormatter() {
86
86
  const filler = consumePendingThinking();
87
87
  if (filler) {
88
88
  const content = message.content.map((block) => block.type === "thinking" && !block.thinking?.trim() ? { ...block, thinking: filler } : block);
89
- return dedupeTodos(toConsoleLines({ ...rec, message: { ...message, content } }));
89
+ return dedupeTodos(toConsoleLines({ ...rec, message: { ...message, content } }, runtime));
90
90
  }
91
91
  }
92
92
  }
@@ -146,7 +146,7 @@ export function createConsoleFormatter() {
146
146
  }
147
147
  return out;
148
148
  }
149
- return dedupeTodos(toConsoleLines(event));
149
+ return dedupeTodos(toConsoleLines(event, runtime));
150
150
  },
151
151
  };
152
152
  }
package/dist/console.js CHANGED
@@ -263,7 +263,7 @@ function codexItemChunks(item, td) {
263
263
  return [];
264
264
  }
265
265
  /** 把一个 stream-json 事件转成 0..N 条 console 行(完全透传)。 */
266
- export function toConsoleLines(event) {
266
+ export function toConsoleLines(event, runtime) {
267
267
  const e = (event ?? {});
268
268
  const lang = detectDaemonLang();
269
269
  const td = (message) => translateDaemon(lang, message);
@@ -284,7 +284,20 @@ export function toConsoleLines(event) {
284
284
  return [{ stream: "system", text: `● ${td("Claude session started")}` }];
285
285
  }
286
286
  if (e.type === "thread.started") {
287
- return [{ stream: "system", text: `● ${td("Codex session started")}` }];
287
+ // thread.started is shared by Codex and ACP adapters; its shape does not
288
+ // identify the runtime. Use the executor-owned selection, not event fields.
289
+ const messages = {
290
+ claude: "Claude session started",
291
+ codex: "Codex session started",
292
+ kimi: "Kimi session started",
293
+ hermes: "Hermes session started",
294
+ opencode: "OpenCode session started",
295
+ "deepseek-harness": "DeepSeek Harness session started",
296
+ };
297
+ const message = runtime && Object.hasOwn(messages, runtime)
298
+ ? messages[runtime]
299
+ : "Session started";
300
+ return [{ stream: "system", text: `● ${td(message)}` }];
288
301
  }
289
302
  if (e.type === "turn.plan.updated") {
290
303
  const plan = boundedCodexPlan(e.plan);
@@ -14,7 +14,7 @@ import { assertRegistryCoversDescriptors, listManagedServiceDescriptorPaths, man
14
14
  export async function managedDaemonCapabilities(eligibility) {
15
15
  try {
16
16
  return (await eligibility()).eligible
17
- ? ["daemon_update_v1", "daemon_restart_v1", "daemon_update_registry_v1", "daemon_layout_migration_v1"]
17
+ ? ["daemon_update_v1", "daemon_restart_v1", "daemon_update_registry_v1"]
18
18
  : [];
19
19
  }
20
20
  catch {
@@ -11,15 +11,12 @@ function registryArgs(registry) {
11
11
  function isRegistryAuthFailure(stderr) {
12
12
  return /(?:\bE401\b|\b401\b.*\bUnauthorized\b)/iu.test(stderr);
13
13
  }
14
+ // Managed eligibility guarantees an exclusive installation and native service per profile.
15
+ // Other profiles share compute capacity, but do not share the package being replaced.
14
16
  export async function prepareDaemonRestart(input) {
15
17
  const local = input.localSlots.tryAcquireExclusive();
16
18
  if (local === null)
17
19
  return { ok: false, errorCode: "runtime_busy" };
18
- const host = await input.hostCoordinator.tryAcquireExclusiveExecution();
19
- if (host === null) {
20
- local.release();
21
- return { ok: false, errorCode: "runtime_busy" };
22
- }
23
20
  let released = false;
24
21
  return {
25
22
  ok: true,
@@ -27,7 +24,6 @@ export async function prepareDaemonRestart(input) {
27
24
  if (released)
28
25
  return;
29
26
  released = true;
30
- await host.release();
31
27
  local.release();
32
28
  },
33
29
  };
@@ -74,17 +70,11 @@ export async function installExactDaemonUpdate(input) {
74
70
  const local = input.localSlots.tryAcquireExclusive();
75
71
  if (local === null)
76
72
  return { ok: false, errorCode: "runtime_busy" };
77
- const host = await input.hostCoordinator.tryAcquireExclusiveExecution();
78
- if (host === null) {
79
- local.release();
80
- return { ok: false, errorCode: "runtime_busy" };
81
- }
82
73
  let released = false;
83
74
  const release = async () => {
84
75
  if (released)
85
76
  return;
86
77
  released = true;
87
- await host.release();
88
78
  local.release();
89
79
  };
90
80
  const lockedInstallation = daemonGlobalInstallation(input.currentEntryPath ?? process.argv[1] ?? "", input.platform ?? process.platform);
@@ -878,23 +878,26 @@ export function createExecutionJournal(agentsRoot, options = {}) {
878
878
  },
879
879
  acknowledgeCompletion: async (executionId) => {
880
880
  validateExecutionId(executionId);
881
- const updated = await serializedExecution(executionId, "acknowledge_completion", async () => {
881
+ const result = await serializedExecution(executionId, "acknowledge_completion", async () => {
882
882
  const entry = await requireRecord(executionId);
883
883
  if (entry.state !== "completed" && entry.state !== "interrupted") {
884
884
  throw new JournalTransitionError(`Cannot acknowledge ${entry.state} execution`);
885
885
  }
886
- if (entry.completionAcknowledged)
887
- return confirmDurable(entry);
886
+ if (entry.completionAcknowledged) {
887
+ return { entry: await confirmDurable(entry), newlyAcknowledged: false };
888
+ }
888
889
  const updated = JournalEntrySchema.parse({
889
890
  ...entry,
890
891
  completionAcknowledged: true,
891
892
  updatedAt: now().toISOString(),
892
893
  });
893
894
  await writeRecord(updated);
894
- return updated;
895
+ return { entry: updated, newlyAcknowledged: true };
895
896
  });
896
- await serializedDirectory(pruneInternal);
897
- return updated;
897
+ // 重复 ACK 仍确认持久化,但不能反复全目录清理并阻塞后续控制帧。
898
+ if (result.newlyAcknowledged)
899
+ await serializedDirectory(pruneInternal);
900
+ return result.entry;
898
901
  },
899
902
  get: async (executionId) => {
900
903
  validateExecutionId(executionId);
@@ -264,6 +264,7 @@ export const ExecutionRejectedSchema = z.object({
264
264
  reason: RejectionReasonSchema,
265
265
  message: z.string().optional(),
266
266
  at: TimestampSchema,
267
+ preAdmissionProof: z.object({ specHash: z.string().regex(/^[a-f0-9]{64}$/u) }).strict().optional(),
267
268
  }).strict();
268
269
  export const ExecutionStartedSchema = z.object({
269
270
  type: z.literal("execution:started"),