@nowcrew/daemon 0.6.98 → 0.6.99-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 (75) hide show
  1. package/README.md +43 -1
  2. package/dist/automatic-daemon-installer.js +41 -10
  3. package/dist/automatic-service-migration.js +7 -6
  4. package/dist/cloud-report-upload.js +1 -10
  5. package/dist/computer-service.js +40 -42
  6. package/dist/computer-setup-client.js +43 -9
  7. package/dist/computer-setup-existing.js +154 -0
  8. package/dist/computer-setup-orchestration.js +32 -6
  9. package/dist/computer-setup.js +37 -6
  10. package/dist/computer-token-setup.js +46 -0
  11. package/dist/execution-journal.js +3 -1
  12. package/dist/execution-protocol.js +1 -1
  13. package/dist/execution-runner.js +4 -1
  14. package/dist/i18n.js +16 -0
  15. package/dist/local-execution-prompt.js +8 -3
  16. package/dist/local-executor.js +64 -39
  17. package/dist/local-memory/commit-telemetry.js +30 -0
  18. package/dist/local-memory/execution-workspace.js +62 -0
  19. package/dist/local-memory/full-index-search.js +47 -0
  20. package/dist/local-memory/index-budget.js +9 -3
  21. package/dist/local-memory/initialization-identity.js +36 -0
  22. package/dist/local-memory/inventory-cli.js +52 -8
  23. package/dist/local-memory/inventory-source.js +15 -8
  24. package/dist/local-memory/inventory-v2.js +362 -0
  25. package/dist/local-memory/legacy-adapter.js +233 -0
  26. package/dist/local-memory/migration-access.js +242 -0
  27. package/dist/local-memory/migration-cli.js +54 -0
  28. package/dist/local-memory/migration-io.js +199 -0
  29. package/dist/local-memory/migration-pending.js +9 -0
  30. package/dist/local-memory/migration-plan.js +432 -0
  31. package/dist/local-memory/migration-schema.js +55 -0
  32. package/dist/local-memory/migration-state-invariants.js +65 -0
  33. package/dist/local-memory/migration-store.js +414 -0
  34. package/dist/local-memory/migration-tool-entry.js +21 -0
  35. package/dist/local-memory/pending-query.js +5 -2
  36. package/dist/local-memory/pending-schema.js +1 -1
  37. package/dist/local-memory/pending.js +16 -2
  38. package/dist/local-memory/prompt.js +21 -5
  39. package/dist/local-memory/read-cli.js +98 -76
  40. package/dist/local-memory/rollout.js +79 -11
  41. package/dist/local-memory/runtime.js +181 -13
  42. package/dist/local-memory/source-path.js +13 -0
  43. package/dist/local-memory/storage-format.js +249 -0
  44. package/dist/local-memory/write.js +189 -21
  45. package/dist/local-memory/writer-mode.js +13 -0
  46. package/dist/local-memory-diagnostics.js +96 -15
  47. package/dist/local-memory-telemetry.js +20 -3
  48. package/dist/machine-info.js +6 -3
  49. package/dist/marketplace-controller.js +8 -21
  50. package/dist/marketplace-protocol.js +1 -1
  51. package/dist/marketplace-remote.js +1 -1
  52. package/dist/marketplace-runner.js +1 -1
  53. package/dist/marketplace-serve.js +2 -4
  54. package/dist/memory-migration-controller.js +89 -0
  55. package/dist/memory-migration-inspect.js +108 -0
  56. package/dist/memory-migration-journal.js +14 -0
  57. package/dist/memory-migration-protocol.js +14 -0
  58. package/dist/memory-migration-runtime.js +305 -0
  59. package/dist/memory-prune-diagnostics.js +60 -4
  60. package/dist/message-file-preview.js +11 -1
  61. package/dist/npm-command.js +5 -5
  62. package/dist/powershell-command.js +12 -0
  63. package/dist/project-skills/machine-hello-publisher.js +95 -6
  64. package/dist/prompt.js +9 -17
  65. package/dist/runner.js +1 -1
  66. package/dist/runtimes/claude-thread-runner.js +25 -4
  67. package/dist/serve.js +92 -10
  68. package/dist/token.js +1 -0
  69. package/dist/windows-scheduled-task.js +3 -3
  70. package/dist/windows-task-principal.js +26 -0
  71. package/dist/workspace-control-handler.js +16 -3
  72. package/dist/workspace.js +13 -2
  73. package/package.json +1 -1
  74. package/dist/marketplace-maintenance-bridge.js +0 -79
  75. package/dist/marketplace-maintenance-protocol.js +0 -40
package/README.md CHANGED
@@ -40,7 +40,40 @@ This compatibility form stays resident in the current terminal and reconnects wi
40
40
  `pnpm --filter @nowcrew/daemon daemon`. `crew-daemon run --agent <handle> --channel <id>` is the legacy
41
41
  manual one-shot entry.
42
42
 
43
- ## Browser-Approved Computer Setup
43
+ ## Single-Command Computer Setup
44
+
45
+ After the deployment enables Token setup, an owner/admin opens **Add Computer**, copies the complete
46
+ command, and pastes it into the target computer's terminal. The same steps apply on macOS, Linux/SSH,
47
+ and Windows. Node.js 22+ and npm must already be installed. The command launches the installer with npx;
48
+ no separate global installation, browser confirmation, or verification code is needed during execution.
49
+
50
+ The generated command pins an exact published installer version and includes a `--setup-token`.
51
+ This is an already-approved one-time connection request, valid for ten minutes. The program exchanges
52
+ it for the machine credential only when executed, then reuses native service installation, new-hello
53
+ verification, credential finalization and rollback. Merely generating or copying a command does not
54
+ rotate the existing credential. An expired or consumed command must be regenerated in the Web UI.
55
+
56
+ For a machine credential you already hold, the same setup entry accepts `--api-key` or
57
+ `CREW_MACHINE_TOKEN` without browser pairing:
58
+
59
+ ```bash
60
+ nowwork-computer setup --server-url https://nowwork.nowcoder.com /nowcoder-app \
61
+ --machine <machine-uuid> --api-key <existing-machine-token>
62
+ ```
63
+
64
+ The machine Token must match the specified Workspace and Computer. This path never rotates the
65
+ credential or creates an approval request; it confirms a new control-plane connection with the expected
66
+ profile and paths before reporting success. Token sources cannot be combined. JSON output puts progress
67
+ on stderr and the final result on stdout. On Windows shells that block npm.ps1/npx.ps1, use npx.cmd as
68
+ the package entry; the setup arguments and workflow are unchanged.
69
+
70
+ Deployment requires `CREW_PUBLIC_URL` (or `CREW_SERVER_URL`) and
71
+ `CREW_COMPUTER_TOKEN_SETUP_VERSION` set to an exact, published version known to support the Token flags.
72
+ The server checks its published release catalog and channel before issuing a Token command. Leave the
73
+ version unset until that installer has been released; it returns `computer_token_setup_unavailable`
74
+ instead of generating a command for an unsupported package. Existing clients retain browser setup.
75
+
76
+ ## Browser-Approved Computer Setup (Compatibility)
44
77
 
45
78
  The Web UI generates a token-free command for a specific Workspace and Machine:
46
79
 
@@ -77,6 +110,15 @@ to confirm Workspace, Machine, environment, profile, and storage identity. It do
77
110
  existing `.crew` data. Existing `crew-daemon install --server-url ... --api-key ...` and profile lifecycle
78
111
  commands remain available as compatibility and recovery entry points.
79
112
 
113
+ Profiles created by older installers may omit `workspaceId` and `machineId`. During setup, the installer
114
+ uses the newly authorized machine credential to call `POST /daemon/computer-setup/legacy-profile` with
115
+ the SHA-256 of the previous profile token. The server checks historical ownership within that exact
116
+ Workspace and Machine, including revoked credentials, without reactivating them. After the server,
117
+ profile name, Agent root and any existing identity fields match, normal installation persists both UUIDs.
118
+ A mismatch or failed verification stops before changing the local profile or service; do not delete a
119
+ profile or fill in UUIDs by guessing. Deploy the server endpoint before publishing the compatible daemon;
120
+ an older server cannot verify legacy profiles, and already published daemon packages remain unchanged.
121
+
80
122
  ## Machine Environment
81
123
 
82
124
  The daemon loads exported variables from its OS account’s bash/zsh configuration on macOS/Linux,
@@ -1,7 +1,8 @@
1
1
  import { homedir } from "node:os";
2
+ import { createHash } from "node:crypto";
2
3
  import { posix, win32 } from "node:path";
3
4
  import { z } from "zod";
4
- import { assertProfileAgentsRootUnique, listProfiles, loadProfile, MachineTokenSchema, removeProfile, saveProfile, windowsDpapiProtector, } from "./computer-profile.js";
5
+ import { assertProfileAgentsRootUnique, listProfiles, loadProfile, MachineTokenSchema, removeProfile, resolveAgentsRoot, saveProfile, windowsDpapiProtector, } from "./computer-profile.js";
5
6
  import { buildServiceSpec, captureExactService, hardenWindowsProfile, replaceExactService, restoreExactService, serviceAction, serviceStatus, systemCommandRunner, uninstallService, } from "./computer-service.js";
6
7
  import { daemonGlobalInstallation } from "./daemon-installation.js";
7
8
  import { buildManagedServiceRecord, installManagedService, uninstallManagedService, } from "./managed-service-lifecycle.js";
@@ -15,8 +16,14 @@ import { daemonRegistryCandidates } from "./daemon-registry.js";
15
16
  import { npmInvocation } from "./npm-command.js";
16
17
  import { discoverExactLegacyService, } from "./automatic-service-migration.js";
17
18
  import { resolveDaemonLocalStateRootFromEnv } from "./daemon-local-home.js";
18
- import { serverHostIdentity } from "./server-url.js";
19
- import { computerSetupRecoveryGuidance } from "./computer-setup-client.js";
19
+ import { serverHostIdentity, serverUrlsMatch } from "./server-url.js";
20
+ import { computerSetupRecoveryGuidance, createComputerSetupClient, } from "./computer-setup-client.js";
21
+ export const automaticDaemonInstallStageMessage = {
22
+ downloading: "Downloading the Daemon package...",
23
+ configuring: "Configuring the Daemon service...",
24
+ starting: "Starting the Daemon service...",
25
+ confirming: "Confirming the computer connection...",
26
+ };
20
27
  function profileStorage(platform, userHome, runner) {
21
28
  return {
22
29
  platform,
@@ -57,6 +64,7 @@ function defaultDependencies() {
57
64
  version: daemonVersion(),
58
65
  ...(process.env.PATH ? { runtimePath: process.env.PATH } : {}),
59
66
  runner,
67
+ verifyLegacyProfileIdentity: (serverUrl, input) => createComputerSetupClient({ serverUrl }).verifyLegacyProfileIdentity(input),
60
68
  captureProfile: async (target) => (await listProfiles(target.daemonHome)).includes(target.profile)
61
69
  ? loadProfile(target.profile, target.daemonHome, storage)
62
70
  : null,
@@ -260,18 +268,37 @@ function setupIdentity(input) {
260
268
  machineId: z.string().uuid().parse(input.machineId),
261
269
  };
262
270
  }
263
- function assertExistingProfileIdentity(previous, identity) {
271
+ async function assertExistingProfileIdentity(previous, identity, target, machineToken, dependencies) {
264
272
  if (previous === null || identity === null)
265
273
  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) {
274
+ if (previous.workspaceId !== undefined && previous.workspaceId !== identity.workspaceId) {
270
275
  throw new Error(`Profile '${previous.name}' belongs to a different Workspace identity`);
271
276
  }
272
- if (previous.machineId !== identity.machineId) {
277
+ if (previous.machineId !== undefined && previous.machineId !== identity.machineId) {
273
278
  throw new Error(`Profile '${previous.name}' belongs to a different Machine identity`);
274
279
  }
280
+ if (previous.workspaceId !== undefined && previous.machineId !== undefined)
281
+ return;
282
+ const rootIdentity = (root) => {
283
+ const resolved = resolveAgentsRoot(root, dependencies.userHome, dependencies.platform);
284
+ return dependencies.platform === "win32" ? resolved.toLowerCase() : resolved;
285
+ };
286
+ if (previous.name !== target.profile || !serverUrlsMatch(previous.serverUrl, target.serverUrl)
287
+ || rootIdentity(previous.agentsRoot) !== rootIdentity(target.agentsRoot)) {
288
+ throw new Error(`Legacy profile '${previous.name}' does not match the requested server, profile, or Agent root`);
289
+ }
290
+ if (dependencies.verifyLegacyProfileIdentity === undefined) {
291
+ throw new Error(`Cannot verify legacy profile '${previous.name}' Workspace and Machine identity`);
292
+ }
293
+ // The old credential may already have been revoked by setup. Only the current credential
294
+ // authenticates this read-only request; the old hash proves historical ownership.
295
+ const verified = await dependencies.verifyLegacyProfileIdentity(target.serverUrl, {
296
+ machineToken,
297
+ previousTokenHash: createHash("sha256").update(previous.machineToken).digest("hex"),
298
+ });
299
+ if (verified.workspaceId !== identity.workspaceId || verified.machineId !== identity.machineId) {
300
+ throw new Error(`Legacy profile '${previous.name}' belongs to a different Workspace or Machine identity`);
301
+ }
275
302
  }
276
303
  export async function installDaemonAutomatically(input, dependencies = defaultDependencies()) {
277
304
  const machineToken = MachineTokenSchema.parse(input.machineToken);
@@ -308,7 +335,7 @@ export async function installDaemonAutomatically(input, dependencies = defaultDe
308
335
  });
309
336
  const previousProfile = await dependencies.captureProfile(target);
310
337
  await dependencies.assertProfileTarget(target, previousProfile);
311
- assertExistingProfileIdentity(previousProfile, identity);
338
+ await assertExistingProfileIdentity(previousProfile, identity, target, machineToken, dependencies);
312
339
  const previousService = await dependencies.captureService(spec, target);
313
340
  if (previousService.installed && !previousService.exact) {
314
341
  throw new Error(`Installed service '${spec.id}' does not match the requested identity`);
@@ -331,6 +358,7 @@ export async function installDaemonAutomatically(input, dependencies = defaultDe
331
358
  });
332
359
  let installFailure;
333
360
  try {
361
+ input.onProgress?.("downloading");
334
362
  await dependencies.installPackage(target.npmPrefix, dependencies.version, dependencies.platform, dependencies.runner, target.registryUrl);
335
363
  }
336
364
  catch (error) {
@@ -353,6 +381,7 @@ export async function installDaemonAutomatically(input, dependencies = defaultDe
353
381
  legacyRemovalStarted = true;
354
382
  await dependencies.removeLegacyService(legacyService);
355
383
  }
384
+ input.onProgress?.("configuring");
356
385
  profileSaved = true;
357
386
  await dependencies.saveTargetProfile({
358
387
  version: 1,
@@ -367,6 +396,7 @@ export async function installDaemonAutomatically(input, dependencies = defaultDe
367
396
  }, target.daemonHome);
368
397
  serviceReplacementStarted = true;
369
398
  await dependencies.replaceService(spec, target);
399
+ input.onProgress?.("starting");
370
400
  await dependencies.startService(spec);
371
401
  const status = await dependencies.verifyService(spec);
372
402
  if (!status.running)
@@ -380,6 +410,7 @@ export async function installDaemonAutomatically(input, dependencies = defaultDe
380
410
  daemonHome: target.daemonHome,
381
411
  agentsRoot: target.agentsRoot,
382
412
  };
413
+ input.onProgress?.("confirming");
383
414
  await input.verifyAfterStart?.(result);
384
415
  return result;
385
416
  }
@@ -3,6 +3,8 @@ import { posix, win32 } from "node:path";
3
3
  import { buildServiceSpec, serviceStatus, windowsCommandArg, } from "./computer-service.js";
4
4
  import { daemonGlobalInstallation } from "./daemon-installation.js";
5
5
  import { inspectLegacyServiceInstallationDetails } from "./legacy-service-installation.js";
6
+ import { powershellCommandArgs } from "./powershell-command.js";
7
+ import { windowsTaskPrincipalFields, windowsTaskPrincipalMatches, windowsTaskPrincipalScript, } from "./windows-task-principal.js";
6
8
  function encoded(value) {
7
9
  return Buffer.from(value, "utf16le").toString("base64");
8
10
  }
@@ -142,12 +144,11 @@ async function discoverWindows(input, identity) {
142
144
  "$n=[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($args[0]));",
143
145
  "$tasks=@(Get-ScheduledTask -ErrorAction Stop|Where-Object{$_.TaskPath -eq '\\' -and $_.TaskName -ceq $n});",
144
146
  "if($tasks.Count -ne 1){[pscustomobject]@{count=$tasks.Count}|ConvertTo-Json -Compress;exit 0};",
145
- "$task=$tasks[0];$actions=@($task.Actions);$me=[Security.Principal.WindowsIdentity]::GetCurrent().Name;",
146
- "[pscustomobject]@{count=1;state=[int]$task.State;actionCount=$actions.Count;execute=if($actions.Count -eq 1){$actions[0].Execute}else{$null};arguments=if($actions.Count -eq 1){$actions[0].Arguments}else{$null};principalMatches=([String]::Equals($task.Principal.UserId,$me,[StringComparison]::OrdinalIgnoreCase)-and [int]$task.Principal.LogonType -eq 3 -and [int]$task.Principal.RunLevel -eq 0)}|ConvertTo-Json -Compress;",
147
+ "$task=$tasks[0];$actions=@($task.Actions);",
148
+ windowsTaskPrincipalScript,
149
+ `[pscustomobject]@{count=1;state=[int]$task.State;actionCount=$actions.Count;execute=if($actions.Count -eq 1){$actions[0].Execute}else{$null};arguments=if($actions.Count -eq 1){$actions[0].Arguments}else{$null};${windowsTaskPrincipalFields}}|ConvertTo-Json -Compress;`,
147
150
  ].join("");
148
- const result = await input.runner("powershell.exe", [
149
- "-NoProfile", "-NonInteractive", "-Command", script, encoded(identity.id),
150
- ]);
151
+ const result = await input.runner("powershell.exe", powershellCommandArgs(script, [encoded(identity.id)]));
151
152
  if (result.exitCode !== 0) {
152
153
  throw new Error(`Unable to inspect legacy scheduled task: ${result.stderr.trim() || `exit ${result.exitCode}`}`);
153
154
  }
@@ -165,7 +166,7 @@ async function discoverWindows(input, identity) {
165
166
  : null;
166
167
  if (definition.count !== 1
167
168
  || definition.actionCount !== 1
168
- || definition.principalMatches !== true
169
+ || !windowsTaskPrincipalMatches(definition)
169
170
  || !win32.isAbsolute(definition.execute ?? "")
170
171
  || match === null
171
172
  || match[2] !== input.desired.profile
@@ -1,6 +1,5 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { homedir, hostname, platform } from "node:os";
3
- import { join } from "node:path";
4
3
  const ALIYUN_MARKER = /alibaba|aliyun|alicloud/iu;
5
4
  function readMarker(path) {
6
5
  try {
@@ -29,15 +28,7 @@ export function detectCloudProvider(env = process.env, platformName = platform()
29
28
  }
30
29
  export function findOssUploadScript(env = process.env, exists = existsSync) {
31
30
  const explicit = env.CREW_OSS_UPLOAD_SCRIPT?.trim();
32
- if (explicit)
33
- return exists(explicit) ? explicit : undefined;
34
- const home = env.HOME?.trim() || homedir();
35
- const candidates = [
36
- join(home, ".claude", "skills", "oss-upload", "scripts", "oss_upload.py"),
37
- join(home, ".codex", "skills", "oss-upload", "scripts", "oss_upload.py"),
38
- join(home, ".agents", "skills", "oss-upload", "scripts", "oss_upload.py"),
39
- ];
40
- return candidates.find(exists);
31
+ return explicit && exists(explicit) ? explicit : undefined;
41
32
  }
42
33
  export function cloudReportUploadFacts(env = process.env, platformName = platform(), hostName = hostname(), readMarkerValue, exists) {
43
34
  const provider = detectCloudProvider(env, platformName, hostName, readMarkerValue);
@@ -6,7 +6,9 @@ import { promisify } from "node:util";
6
6
  import { dirname, resolve } from "node:path";
7
7
  import { normalizeDaemonNpmRegistry } from "./npm-registry.js";
8
8
  import { daemonRegistryCandidates } from "./daemon-registry.js";
9
+ import { windowsTaskPrincipalFields, windowsTaskPrincipalMatches, windowsTaskPrincipalScript, } from "./windows-task-principal.js";
9
10
  import { npmInvocation } from "./npm-command.js";
11
+ import { powershellCommandArgs } from "./powershell-command.js";
10
12
  const execFileAsync = promisify(execFile);
11
13
  const WINDOWS_STOP_TIMEOUT_MS = 5_000;
12
14
  const WINDOWS_STOP_POLL_MS = 100;
@@ -195,12 +197,11 @@ async function windowsTaskDefinitionMatches(spec, runner) {
195
197
  "$n=[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($args[0]));",
196
198
  "$tasks=@(Get-ScheduledTask -ErrorAction Stop|Where-Object{$_.TaskPath -eq '\\' -and $_.TaskName -ceq $n});",
197
199
  "if($tasks.Count -ne 1){[pscustomobject]@{count=$tasks.Count}|ConvertTo-Json -Compress;exit 0};",
198
- "$task=$tasks[0];$actions=@($task.Actions);$me=[Security.Principal.WindowsIdentity]::GetCurrent().Name;",
199
- "[pscustomobject]@{count=1;actionCount=$actions.Count;execute=if($actions.Count -eq 1){$actions[0].Execute}else{$null};arguments=if($actions.Count -eq 1){$actions[0].Arguments}else{$null};principalMatches=([String]::Equals($task.Principal.UserId,$me,[StringComparison]::OrdinalIgnoreCase)-and [int]$task.Principal.LogonType -eq 3 -and [int]$task.Principal.RunLevel -eq 0)}|ConvertTo-Json -Compress;",
200
+ "$task=$tasks[0];$actions=@($task.Actions);",
201
+ windowsTaskPrincipalScript,
202
+ `[pscustomobject]@{count=1;actionCount=$actions.Count;execute=if($actions.Count -eq 1){$actions[0].Execute}else{$null};arguments=if($actions.Count -eq 1){$actions[0].Arguments}else{$null};${windowsTaskPrincipalFields}}|ConvertTo-Json -Compress;`,
200
203
  ].join("");
201
- const result = await runner("powershell.exe", [
202
- "-NoProfile", "-NonInteractive", "-Command", script, encodedName,
203
- ]);
204
+ const result = await runner("powershell.exe", powershellCommandArgs(script, [encodedName]));
204
205
  if (result.exitCode !== 0) {
205
206
  throw new Error(`Unable to inspect scheduled task definition: ${result.stderr.trim() || `exit ${result.exitCode}`}`);
206
207
  }
@@ -214,7 +215,7 @@ async function windowsTaskDefinitionMatches(spec, runner) {
214
215
  const expectedArguments = spec.daemonCommand.slice(1).map(windowsCommandArg).join(" ");
215
216
  return definition.count === 1
216
217
  && definition.actionCount === 1
217
- && definition.principalMatches === true
218
+ && windowsTaskPrincipalMatches(definition)
218
219
  && sameWindowsPath(definition.execute, spec.daemonCommand[0])
219
220
  && definition.arguments === expectedArguments;
220
221
  }
@@ -226,9 +227,7 @@ async function windowsTaskState(spec, runner) {
226
227
  "$tasks=@(Get-ScheduledTask -ErrorAction Stop|Where-Object{$_.TaskPath -eq '\\' -and $_.TaskName -ceq $n});",
227
228
  "if($tasks.Count -eq 0){'missing'}elseif($tasks.Count -eq 1){'state:'+([int]$tasks[0].State)}else{throw 'duplicate scheduled tasks'}",
228
229
  ].join("");
229
- const result = await runner("powershell.exe", [
230
- "-NoProfile", "-NonInteractive", "-Command", script, encodedName,
231
- ]);
230
+ const result = await runner("powershell.exe", powershellCommandArgs(script, [encodedName]));
232
231
  if (result.exitCode !== 0) {
233
232
  throw new Error(`Unable to query scheduled task: ${result.stderr.trim() || `exit ${result.exitCode}`}`);
234
233
  }
@@ -279,10 +278,9 @@ export async function installService(spec, runner = systemCommandRunner) {
279
278
  "$principal=New-ScheduledTaskPrincipal -UserId $identity -LogonType Interactive -RunLevel Limited;",
280
279
  "Register-ScheduledTask -TaskName $name -TaskPath '\\' -Action $action -Trigger $trigger -Settings $settings -Principal $principal -ErrorAction Stop|Out-Null;",
281
280
  ].join("");
282
- await checked(runner, "powershell.exe", [
283
- "-NoProfile", "-NonInteractive", "-Command", script,
281
+ await checked(runner, "powershell.exe", powershellCommandArgs(script, [
284
282
  encodedName, encodedExecutable, encodedArguments,
285
- ], "Scheduled task installation");
283
+ ]), "Scheduled task installation");
286
284
  return;
287
285
  }
288
286
  if (spec.descriptorPath === null || spec.descriptor === null)
@@ -418,6 +416,33 @@ export async function serviceAction(spec, action, runner = systemCommandRunner)
418
416
  }
419
417
  }
420
418
  const systemDetachedSpawn = (command, args, options) => spawn(command, [...args], options);
419
+ function windowsRestartCommandArgs(spec) {
420
+ if (!spec.runtimeProcessIdentity)
421
+ throw new Error("Windows daemon process identity is required for restart");
422
+ const encodedName = Buffer.from(spec.id, "utf16le").toString("base64");
423
+ const encodedExecutable = Buffer.from(spec.daemonCommand[0], "utf16le").toString("base64");
424
+ const encodedArguments = Buffer.from(spec.daemonCommand.slice(1).map(windowsCommandArg).join(" "), "utf16le").toString("base64");
425
+ const script = [
426
+ "$ErrorActionPreference='Stop';",
427
+ "$n=[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($args[1]));",
428
+ "$exe=[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($args[2]));",
429
+ "$argv=[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($args[3]));",
430
+ "$task=@(Get-ScheduledTask -ErrorAction Stop|Where-Object{$_.TaskPath -eq '\\' -and $_.TaskName -ceq $n});if($task.Count -ne 1){throw 'scheduled task identity changed'};",
431
+ "$actions=@($task[0].Actions);if($actions.Count -ne 1 -or -not [String]::Equals($actions[0].Execute,$exe,[StringComparison]::OrdinalIgnoreCase) -or $actions[0].Arguments -cne $argv){throw 'scheduled task action changed'};",
432
+ "$p=Get-Process -Id ([int]$args[0]) -ErrorAction Stop;",
433
+ "if($p.StartTime.ToUniversalTime().ToFileTimeUtc().ToString() -cne $args[4]){throw 'daemon process identity changed'};",
434
+ "$p.Kill();",
435
+ "if(-not $p.WaitForExit(10000)){throw 'daemon exit timeout'};",
436
+ "$deadline=[DateTime]::UtcNow.AddSeconds(10);",
437
+ "do{$task=@(Get-ScheduledTask -ErrorAction Stop|Where-Object{$_.TaskPath -eq '\\' -and $_.TaskName -ceq $n});if($task.Count -ne 1){throw 'scheduled task identity changed'};if([int]$task[0].State -ne 4){break};Start-Sleep -Milliseconds 100}while([DateTime]::UtcNow -lt $deadline);",
438
+ "if([int]$task[0].State -eq 4){throw 'scheduled task stop timeout'};",
439
+ "$actions=@($task[0].Actions);if($actions.Count -ne 1 -or -not [String]::Equals($actions[0].Execute,$exe,[StringComparison]::OrdinalIgnoreCase) -or $actions[0].Arguments -cne $argv){throw 'scheduled task action changed'};",
440
+ "& schtasks.exe /Run /TN $n | Out-Null;if($LASTEXITCODE -ne 0){throw 'scheduled task start failed'};",
441
+ ].join("");
442
+ return powershellCommandArgs(script, [
443
+ String(process.pid), encodedName, encodedExecutable, encodedArguments, spec.runtimeProcessIdentity,
444
+ ]);
445
+ }
421
446
  const RESTART_HELPER_SOURCE = String.raw `
422
447
  const { spawnSync } = require("node:child_process");
423
448
  const spec = JSON.parse(process.argv[1]);
@@ -428,27 +453,7 @@ setTimeout(() => {
428
453
  } else if (spec.platform === "linux") {
429
454
  restarted = spawnSync("systemctl", ["--user", "--no-block", "restart", spec.id], { stdio: "ignore", shell: false });
430
455
  } else {
431
- const encodedName = Buffer.from(spec.id, "utf16le").toString("base64");
432
- const encodedExecutable = Buffer.from(spec.executable, "utf16le").toString("base64");
433
- const encodedArguments = Buffer.from(spec.arguments, "utf16le").toString("base64");
434
- const script = [
435
- "$ErrorActionPreference='Stop';",
436
- "$n=[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($args[1]));",
437
- "$exe=[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($args[2]));",
438
- "$argv=[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($args[3]));",
439
- "$task=@(Get-ScheduledTask -ErrorAction Stop|Where-Object{$_.TaskPath -eq '\\' -and $_.TaskName -ceq $n});if($task.Count -ne 1){throw 'scheduled task identity changed'};",
440
- "$actions=@($task[0].Actions);if($actions.Count -ne 1 -or -not [String]::Equals($actions[0].Execute,$exe,[StringComparison]::OrdinalIgnoreCase) -or $actions[0].Arguments -cne $argv){throw 'scheduled task action changed'};",
441
- "$p=Get-Process -Id ([int]$args[0]) -ErrorAction Stop;",
442
- "if($p.StartTime.ToUniversalTime().ToFileTimeUtc().ToString() -cne $args[4]){throw 'daemon process identity changed'};",
443
- "$p.Kill();",
444
- "if(-not $p.WaitForExit(10000)){throw 'daemon exit timeout'};",
445
- "$deadline=[DateTime]::UtcNow.AddSeconds(10);",
446
- "do{$task=@(Get-ScheduledTask -ErrorAction Stop|Where-Object{$_.TaskPath -eq '\\' -and $_.TaskName -ceq $n});if($task.Count -ne 1){throw 'scheduled task identity changed'};if([int]$task[0].State -ne 4){break};Start-Sleep -Milliseconds 100}while([DateTime]::UtcNow -lt $deadline);",
447
- "if([int]$task[0].State -eq 4){throw 'scheduled task stop timeout'};",
448
- "$actions=@($task[0].Actions);if($actions.Count -ne 1 -or -not [String]::Equals($actions[0].Execute,$exe,[StringComparison]::OrdinalIgnoreCase) -or $actions[0].Arguments -cne $argv){throw 'scheduled task action changed'};",
449
- "& schtasks.exe /Run /TN $n | Out-Null;if($LASTEXITCODE -ne 0){throw 'scheduled task start failed'};",
450
- ].join("");
451
- restarted = spawnSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script, String(spec.daemonPid), encodedName, encodedExecutable, encodedArguments, spec.processIdentity], { stdio: "ignore", shell: false });
456
+ restarted = spawnSync("powershell.exe", spec.powershellArgs, { stdio: "ignore", shell: false });
452
457
  }
453
458
  if (restarted.status !== 0) process.send?.({ type: "restart-failed", status: restarted.status });
454
459
  setTimeout(() => {
@@ -470,15 +475,8 @@ export function scheduleServiceRestart(spec, spawnDetached = systemDetachedSpawn
470
475
  ? { platform: spec.platform, id: spec.id }
471
476
  : {
472
477
  platform: spec.platform,
473
- id: spec.id,
474
- daemonPid: process.pid,
475
- executable: spec.daemonCommand[0],
476
- arguments: spec.daemonCommand.slice(1).map(windowsCommandArg).join(" "),
477
- processIdentity: spec.runtimeProcessIdentity,
478
+ powershellArgs: windowsRestartCommandArgs(spec),
478
479
  };
479
- if (spec.platform === "win32" && !payload.processIdentity) {
480
- throw new Error("Windows daemon process identity is required for restart");
481
- }
482
480
  const child = spawnDetached(process.execPath, ["-e", RESTART_HELPER_SOURCE, JSON.stringify(payload)], { detached: true, stdio: ["ignore", "ignore", "ignore", "ipc"], shell: false });
483
481
  child.unref();
484
482
  let settleFailure;
@@ -627,7 +625,7 @@ export async function windowsProfileIsPrivate(path, runner = systemCommandRunner
627
625
  "$unsafe=$rules|Where-Object { $_.AccessControlType -eq 'Allow' -and $safe -notcontains $_.IdentityReference.Translate([Security.Principal.SecurityIdentifier]).Value };",
628
626
  "if($unsafe){'false'}else{'true'}",
629
627
  ].join("");
630
- const result = await runner("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script, encodedPath]);
628
+ const result = await runner("powershell.exe", powershellCommandArgs(script, [encodedPath]));
631
629
  if (result.exitCode !== 0)
632
630
  throw new Error(`Profile ACL inspection failed: ${result.stderr.trim() || `exit ${result.exitCode}`}`);
633
631
  if (result.stdout.trim() === "true")
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { hostname } from "node:os";
3
3
  import { MachineTokenSchema } from "./computer-profile.js";
4
+ import { powershellCommandArgs } from "./powershell-command.js";
4
5
  import { systemCommandRunner, } from "./computer-service.js";
5
6
  const HttpUrlSchema = z.string().url().refine((value) => {
6
7
  const url = new URL(value);
@@ -38,6 +39,26 @@ const SetupBootstrapSchema = z.object({
38
39
  expiresAt: z.string().datetime({ offset: true }).optional(),
39
40
  machineToken: MachineTokenSchema,
40
41
  }).strict();
42
+ const SetupTokenTargetSchema = SetupBootstrapSchema.omit({ setupId: true, machineToken: true }).extend({
43
+ workspaceSlug: z.string().min(1).max(80),
44
+ releaseTag: z.enum(["latest", "beta"]),
45
+ connection: z.object({
46
+ status: z.enum(["online", "offline"]),
47
+ epoch: z.number().int().nonnegative(),
48
+ profile: z.string().nullable(),
49
+ daemonHome: z.string().nullable(),
50
+ agentsRoot: z.string().nullable(),
51
+ storageId: z.string().optional(),
52
+ credentialMatchesConnection: z.boolean(),
53
+ }).strict(),
54
+ }).strict();
55
+ const SetupReceiptSchema = z.object({
56
+ setupId: z.string().uuid(),
57
+ credentialState: z.enum(["pending", "finalized"]),
58
+ target: SetupTokenTargetSchema.omit({ connection: true }),
59
+ connection: SetupTokenTargetSchema.shape.connection,
60
+ connectionConfirmed: z.boolean(),
61
+ }).strict();
41
62
  const SetupPendingSchema = z.object({
42
63
  status: z.enum(["authorization_pending", "pending"]),
43
64
  intervalSeconds: z.number().int().positive().max(300).optional(),
@@ -75,7 +96,8 @@ export class ComputerSetupHttpError extends Error {
75
96
  }
76
97
  }
77
98
  export function redactSetupSecrets(value, secrets = []) {
78
- let redacted = value.replace(/\bsk_machine_[A-Za-z0-9_-]+\b/gu, "<redacted>");
99
+ let redacted = value.replace(/\bsk_machine_[A-Za-z0-9_-]+\b/gu, "<redacted>")
100
+ .replace(/(--setup-token(?:=|\s+))[A-Za-z0-9_-]+/gu, "$1<redacted>");
79
101
  for (const secret of secrets) {
80
102
  if (secret.length > 0)
81
103
  redacted = redacted.replaceAll(secret, "<redacted>");
@@ -236,8 +258,9 @@ export function createComputerSetupClient(options) {
236
258
  setTimeout(resolve, milliseconds);
237
259
  }));
238
260
  const now = options.now ?? Date.now;
239
- let activeMachineId;
240
261
  const request = async (method, path, body, bearer) => {
262
+ const secretInputs = [bearer ?? "", body?.deviceCode ?? "",
263
+ body?.setupToken ?? ""];
241
264
  const controller = new AbortController();
242
265
  const timeoutId = setTimeout(() => controller.abort(), requestTimeoutMs);
243
266
  try {
@@ -252,7 +275,7 @@ export function createComputerSetupClient(options) {
252
275
  });
253
276
  const payload = await response.json();
254
277
  if (!response.ok || payload?.success === false) {
255
- const message = redactSetupSecrets(payload?.error?.message ?? `computer setup request failed (HTTP ${response.status})`);
278
+ const message = redactSetupSecrets(payload?.error?.message ?? `computer setup request failed (HTTP ${response.status})`, secretInputs);
256
279
  throw new ComputerSetupHttpError(message, response.status, payload?.error?.code ?? `HTTP_${response.status}`);
257
280
  }
258
281
  return (payload?.success === true ? payload.data : payload);
@@ -262,15 +285,26 @@ export function createComputerSetupClient(options) {
262
285
  throw new Error("computer setup request timed out");
263
286
  if (error instanceof ComputerSetupHttpError)
264
287
  throw error;
265
- throw new Error(redactSetupSecrets(error instanceof Error ? error.message : String(error)));
288
+ throw new Error(redactSetupSecrets(error instanceof Error ? error.message : String(error), secretInputs));
266
289
  }
267
290
  finally {
268
291
  clearTimeout(timeoutId);
269
292
  }
270
293
  };
271
294
  return {
295
+ receipt: async ({ setupToken, machineToken }) => {
296
+ const receipt = SetupReceiptSchema.safeParse(await request("POST", "/daemon/computer-setup/receipt", { setupToken }, machineToken));
297
+ if (!receipt.success)
298
+ throw new Error("Invalid computer setup receipt response");
299
+ return receipt.data;
300
+ },
301
+ tokenTarget: async (machineToken) => SetupTokenTargetSchema.parse(await request("GET", "/daemon/computer-setup/target", undefined, machineToken)),
302
+ verifyLegacyProfileIdentity: async (input) => z.object({
303
+ workspaceId: z.string().uuid(), machineId: z.string().uuid(),
304
+ }).strict().parse(await request("POST", "/daemon/computer-setup/legacy-profile", {
305
+ previousTokenHash: input.previousTokenHash,
306
+ }, input.machineToken)),
272
307
  start: async (input) => {
273
- activeMachineId = input.machineId;
274
308
  return parseStart(await request("POST", "/api/auth/computer-setup/start", {
275
309
  ...(input.workspace.kind === "id"
276
310
  ? { workspaceId: input.workspace.value }
@@ -281,8 +315,6 @@ export function createComputerSetupClient(options) {
281
315
  }));
282
316
  },
283
317
  poll: async (deviceCode) => {
284
- if (activeMachineId === undefined)
285
- throw new Error("computer setup polling has no active machine");
286
318
  return parsePoll(await request("POST", "/api/auth/computer-setup/poll", { deviceCode }));
287
319
  },
288
320
  waitForHello: async (input) => {
@@ -309,8 +341,10 @@ export function createComputerSetupClient(options) {
309
341
  };
310
342
  }
311
343
  export async function openSetupBrowser(url, platform = process.platform, runner = systemCommandRunner) {
312
- const command = platform === "win32" ? "explorer.exe" : platform === "darwin" ? "open" : "xdg-open";
313
- const args = [url];
344
+ const command = platform === "win32" ? "powershell.exe" : platform === "darwin" ? "open" : "xdg-open";
345
+ const args = platform === "win32"
346
+ ? powershellCommandArgs("Start-Process -FilePath $args[0] -ErrorAction Stop | Out-Null", [url])
347
+ : [url];
314
348
  const result = await runner(command, args);
315
349
  if (result.exitCode !== 0) {
316
350
  throw new Error(`Unable to open the computer setup browser: ${redactSetupSecrets(result.stderr.trim() || `exit ${result.exitCode}`)}`);