@nowcrew/daemon 0.6.98-beta.3 → 0.6.98

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -40,40 +40,7 @@ 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
- ## 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)
43
+ ## Browser-Approved Computer Setup
77
44
 
78
45
  The Web UI generates a token-free command for a specific Workspace and Machine:
79
46
 
@@ -3,8 +3,6 @@ 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";
8
6
  function encoded(value) {
9
7
  return Buffer.from(value, "utf16le").toString("base64");
10
8
  }
@@ -144,11 +142,12 @@ async function discoverWindows(input, identity) {
144
142
  "$n=[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($args[0]));",
145
143
  "$tasks=@(Get-ScheduledTask -ErrorAction Stop|Where-Object{$_.TaskPath -eq '\\' -and $_.TaskName -ceq $n});",
146
144
  "if($tasks.Count -ne 1){[pscustomobject]@{count=$tasks.Count}|ConvertTo-Json -Compress;exit 0};",
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;`,
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;",
150
147
  ].join("");
151
- const result = await input.runner("powershell.exe", powershellCommandArgs(script, [encoded(identity.id)]));
148
+ const result = await input.runner("powershell.exe", [
149
+ "-NoProfile", "-NonInteractive", "-Command", script, encoded(identity.id),
150
+ ]);
152
151
  if (result.exitCode !== 0) {
153
152
  throw new Error(`Unable to inspect legacy scheduled task: ${result.stderr.trim() || `exit ${result.exitCode}`}`);
154
153
  }
@@ -166,7 +165,7 @@ async function discoverWindows(input, identity) {
166
165
  : null;
167
166
  if (definition.count !== 1
168
167
  || definition.actionCount !== 1
169
- || !windowsTaskPrincipalMatches(definition)
168
+ || definition.principalMatches !== true
170
169
  || !win32.isAbsolute(definition.execute ?? "")
171
170
  || match === null
172
171
  || match[2] !== input.desired.profile
File without changes
@@ -6,9 +6,7 @@ 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";
10
9
  import { npmInvocation } from "./npm-command.js";
11
- import { powershellCommandArgs } from "./powershell-command.js";
12
10
  const execFileAsync = promisify(execFile);
13
11
  const WINDOWS_STOP_TIMEOUT_MS = 5_000;
14
12
  const WINDOWS_STOP_POLL_MS = 100;
@@ -197,11 +195,12 @@ async function windowsTaskDefinitionMatches(spec, runner) {
197
195
  "$n=[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($args[0]));",
198
196
  "$tasks=@(Get-ScheduledTask -ErrorAction Stop|Where-Object{$_.TaskPath -eq '\\' -and $_.TaskName -ceq $n});",
199
197
  "if($tasks.Count -ne 1){[pscustomobject]@{count=$tasks.Count}|ConvertTo-Json -Compress;exit 0};",
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;`,
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;",
203
200
  ].join("");
204
- const result = await runner("powershell.exe", powershellCommandArgs(script, [encodedName]));
201
+ const result = await runner("powershell.exe", [
202
+ "-NoProfile", "-NonInteractive", "-Command", script, encodedName,
203
+ ]);
205
204
  if (result.exitCode !== 0) {
206
205
  throw new Error(`Unable to inspect scheduled task definition: ${result.stderr.trim() || `exit ${result.exitCode}`}`);
207
206
  }
@@ -215,7 +214,7 @@ async function windowsTaskDefinitionMatches(spec, runner) {
215
214
  const expectedArguments = spec.daemonCommand.slice(1).map(windowsCommandArg).join(" ");
216
215
  return definition.count === 1
217
216
  && definition.actionCount === 1
218
- && windowsTaskPrincipalMatches(definition)
217
+ && definition.principalMatches === true
219
218
  && sameWindowsPath(definition.execute, spec.daemonCommand[0])
220
219
  && definition.arguments === expectedArguments;
221
220
  }
@@ -227,7 +226,9 @@ async function windowsTaskState(spec, runner) {
227
226
  "$tasks=@(Get-ScheduledTask -ErrorAction Stop|Where-Object{$_.TaskPath -eq '\\' -and $_.TaskName -ceq $n});",
228
227
  "if($tasks.Count -eq 0){'missing'}elseif($tasks.Count -eq 1){'state:'+([int]$tasks[0].State)}else{throw 'duplicate scheduled tasks'}",
229
228
  ].join("");
230
- const result = await runner("powershell.exe", powershellCommandArgs(script, [encodedName]));
229
+ const result = await runner("powershell.exe", [
230
+ "-NoProfile", "-NonInteractive", "-Command", script, encodedName,
231
+ ]);
231
232
  if (result.exitCode !== 0) {
232
233
  throw new Error(`Unable to query scheduled task: ${result.stderr.trim() || `exit ${result.exitCode}`}`);
233
234
  }
@@ -278,9 +279,10 @@ export async function installService(spec, runner = systemCommandRunner) {
278
279
  "$principal=New-ScheduledTaskPrincipal -UserId $identity -LogonType Interactive -RunLevel Limited;",
279
280
  "Register-ScheduledTask -TaskName $name -TaskPath '\\' -Action $action -Trigger $trigger -Settings $settings -Principal $principal -ErrorAction Stop|Out-Null;",
280
281
  ].join("");
281
- await checked(runner, "powershell.exe", powershellCommandArgs(script, [
282
+ await checked(runner, "powershell.exe", [
283
+ "-NoProfile", "-NonInteractive", "-Command", script,
282
284
  encodedName, encodedExecutable, encodedArguments,
283
- ]), "Scheduled task installation");
285
+ ], "Scheduled task installation");
284
286
  return;
285
287
  }
286
288
  if (spec.descriptorPath === null || spec.descriptor === null)
@@ -416,33 +418,6 @@ export async function serviceAction(spec, action, runner = systemCommandRunner)
416
418
  }
417
419
  }
418
420
  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
- }
446
421
  const RESTART_HELPER_SOURCE = String.raw `
447
422
  const { spawnSync } = require("node:child_process");
448
423
  const spec = JSON.parse(process.argv[1]);
@@ -453,7 +428,27 @@ setTimeout(() => {
453
428
  } else if (spec.platform === "linux") {
454
429
  restarted = spawnSync("systemctl", ["--user", "--no-block", "restart", spec.id], { stdio: "ignore", shell: false });
455
430
  } else {
456
- restarted = spawnSync("powershell.exe", spec.powershellArgs, { stdio: "ignore", shell: false });
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 });
457
452
  }
458
453
  if (restarted.status !== 0) process.send?.({ type: "restart-failed", status: restarted.status });
459
454
  setTimeout(() => {
@@ -475,8 +470,15 @@ export function scheduleServiceRestart(spec, spawnDetached = systemDetachedSpawn
475
470
  ? { platform: spec.platform, id: spec.id }
476
471
  : {
477
472
  platform: spec.platform,
478
- powershellArgs: windowsRestartCommandArgs(spec),
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,
479
478
  };
479
+ if (spec.platform === "win32" && !payload.processIdentity) {
480
+ throw new Error("Windows daemon process identity is required for restart");
481
+ }
480
482
  const child = spawnDetached(process.execPath, ["-e", RESTART_HELPER_SOURCE, JSON.stringify(payload)], { detached: true, stdio: ["ignore", "ignore", "ignore", "ipc"], shell: false });
481
483
  child.unref();
482
484
  let settleFailure;
@@ -625,7 +627,7 @@ export async function windowsProfileIsPrivate(path, runner = systemCommandRunner
625
627
  "$unsafe=$rules|Where-Object { $_.AccessControlType -eq 'Allow' -and $safe -notcontains $_.IdentityReference.Translate([Security.Principal.SecurityIdentifier]).Value };",
626
628
  "if($unsafe){'false'}else{'true'}",
627
629
  ].join("");
628
- const result = await runner("powershell.exe", powershellCommandArgs(script, [encodedPath]));
630
+ const result = await runner("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script, encodedPath]);
629
631
  if (result.exitCode !== 0)
630
632
  throw new Error(`Profile ACL inspection failed: ${result.stderr.trim() || `exit ${result.exitCode}`}`);
631
633
  if (result.stdout.trim() === "true")
@@ -1,7 +1,6 @@
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";
5
4
  import { systemCommandRunner, } from "./computer-service.js";
6
5
  const HttpUrlSchema = z.string().url().refine((value) => {
7
6
  const url = new URL(value);
@@ -39,19 +38,6 @@ const SetupBootstrapSchema = z.object({
39
38
  expiresAt: z.string().datetime({ offset: true }).optional(),
40
39
  machineToken: MachineTokenSchema,
41
40
  }).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
41
  const SetupPendingSchema = z.object({
56
42
  status: z.enum(["authorization_pending", "pending"]),
57
43
  intervalSeconds: z.number().int().positive().max(300).optional(),
@@ -250,8 +236,8 @@ export function createComputerSetupClient(options) {
250
236
  setTimeout(resolve, milliseconds);
251
237
  }));
252
238
  const now = options.now ?? Date.now;
239
+ let activeMachineId;
253
240
  const request = async (method, path, body, bearer) => {
254
- const secretInputs = [bearer ?? "", body?.deviceCode ?? ""];
255
241
  const controller = new AbortController();
256
242
  const timeoutId = setTimeout(() => controller.abort(), requestTimeoutMs);
257
243
  try {
@@ -266,7 +252,7 @@ export function createComputerSetupClient(options) {
266
252
  });
267
253
  const payload = await response.json();
268
254
  if (!response.ok || payload?.success === false) {
269
- const message = redactSetupSecrets(payload?.error?.message ?? `computer setup request failed (HTTP ${response.status})`, secretInputs);
255
+ const message = redactSetupSecrets(payload?.error?.message ?? `computer setup request failed (HTTP ${response.status})`);
270
256
  throw new ComputerSetupHttpError(message, response.status, payload?.error?.code ?? `HTTP_${response.status}`);
271
257
  }
272
258
  return (payload?.success === true ? payload.data : payload);
@@ -276,15 +262,15 @@ export function createComputerSetupClient(options) {
276
262
  throw new Error("computer setup request timed out");
277
263
  if (error instanceof ComputerSetupHttpError)
278
264
  throw error;
279
- throw new Error(redactSetupSecrets(error instanceof Error ? error.message : String(error), secretInputs));
265
+ throw new Error(redactSetupSecrets(error instanceof Error ? error.message : String(error)));
280
266
  }
281
267
  finally {
282
268
  clearTimeout(timeoutId);
283
269
  }
284
270
  };
285
271
  return {
286
- tokenTarget: async (machineToken) => SetupTokenTargetSchema.parse(await request("GET", "/daemon/computer-setup/target", undefined, machineToken)),
287
272
  start: async (input) => {
273
+ activeMachineId = input.machineId;
288
274
  return parseStart(await request("POST", "/api/auth/computer-setup/start", {
289
275
  ...(input.workspace.kind === "id"
290
276
  ? { workspaceId: input.workspace.value }
@@ -295,6 +281,8 @@ export function createComputerSetupClient(options) {
295
281
  }));
296
282
  },
297
283
  poll: async (deviceCode) => {
284
+ if (activeMachineId === undefined)
285
+ throw new Error("computer setup polling has no active machine");
298
286
  return parsePoll(await request("POST", "/api/auth/computer-setup/poll", { deviceCode }));
299
287
  },
300
288
  waitForHello: async (input) => {
@@ -321,10 +309,8 @@ export function createComputerSetupClient(options) {
321
309
  };
322
310
  }
323
311
  export async function openSetupBrowser(url, platform = process.platform, runner = systemCommandRunner) {
324
- const command = platform === "win32" ? "powershell.exe" : platform === "darwin" ? "open" : "xdg-open";
325
- const args = platform === "win32"
326
- ? powershellCommandArgs("Start-Process -FilePath $args[0] -ErrorAction Stop | Out-Null", [url])
327
- : [url];
312
+ const command = platform === "win32" ? "explorer.exe" : platform === "darwin" ? "open" : "xdg-open";
313
+ const args = [url];
328
314
  const result = await runner(command, args);
329
315
  if (result.exitCode !== 0) {
330
316
  throw new Error(`Unable to open the computer setup browser: ${redactSetupSecrets(result.stderr.trim() || `exit ${result.exitCode}`)}`);
@@ -6,7 +6,7 @@ function safeError(error, machineToken) {
6
6
  const message = redactSetupSecrets(error instanceof Error ? error.message : String(error), machineToken === undefined ? [] : [machineToken]);
7
7
  return new Error(message, error instanceof Error ? { cause: error } : undefined);
8
8
  }
9
- export function assertBootstrapMatches(bootstrap, input) {
9
+ function assertBootstrapMatches(bootstrap, input) {
10
10
  if (bootstrap.machineId !== input.args.machineId) {
11
11
  throw new Error("setup bootstrap machine does not match --machine");
12
12
  }
@@ -40,7 +40,7 @@ function secondsUntil(expiresAt, now) {
40
40
  const remaining = Date.parse(expiresAt) - now();
41
41
  return Math.max(0, Math.ceil(remaining / 1_000));
42
42
  }
43
- export function localPathsMatch(left, right, platform) {
43
+ function localPathsMatch(left, right, platform) {
44
44
  const paths = platform === "win32" ? win32 : posix;
45
45
  if (!paths.isAbsolute(left) || !paths.isAbsolute(right))
46
46
  return false;
@@ -51,14 +51,6 @@ export function localPathsMatch(left, right, platform) {
51
51
  return normalize(left) === normalize(right);
52
52
  }
53
53
  async function obtainBootstrap(input, deps) {
54
- if (input.args.setupToken !== undefined) {
55
- deps.reportPairing("Checking computer connection information...");
56
- const result = await deps.client.poll(input.args.setupToken);
57
- if (result.status !== "authorized") {
58
- throw new Error("Setup command expired or unavailable; regenerate it in NowWork and run the new command");
59
- }
60
- return result.bootstrap;
61
- }
62
54
  const pairing = await deps.client.start({
63
55
  workspace: input.args.workspace,
64
56
  machineId: input.args.machineId,
@@ -124,7 +116,6 @@ export async function runComputerSetupOrchestration(input, deps) {
124
116
  bootstrap = authorized;
125
117
  machineToken = token;
126
118
  assertBootstrapMatches(authorized, input);
127
- deps.reportPairing("Downloading, installing, and starting the Daemon service...");
128
119
  const installation = await deps.installAutomatically({
129
120
  serverUrl: authorized.serverUrl,
130
121
  machineToken: token,
@@ -137,7 +128,6 @@ export async function runComputerSetupOrchestration(input, deps) {
137
128
  ...(authorized.registryUrl === undefined ? {} : { requestedRegistryUrl: authorized.registryUrl }),
138
129
  beforeRollback: confirmRollback,
139
130
  verifyAfterStart: async (candidate) => {
140
- deps.reportPairing("Confirming the computer connection...");
141
131
  const hello = await deps.client.waitForHello({
142
132
  machineToken: token,
143
133
  machineId: authorized.machineId,
@@ -184,7 +174,6 @@ export async function runComputerSetupOrchestration(input, deps) {
184
174
  });
185
175
  return {
186
176
  workspaceId: authorized.workspaceId,
187
- ...(authorized.workspaceSlug === undefined ? {} : { workspaceSlug: authorized.workspaceSlug }),
188
177
  machineId: authorized.machineId,
189
178
  environment: authorized.environment,
190
179
  profile: installation.profile,
@@ -1,13 +1,11 @@
1
1
  import { parseArgs } from "node:util";
2
2
  import { z } from "zod";
3
3
  import { DEFAULT_SERVER_URL } from "./config.js";
4
- import { MachineTokenSchema } from "./computer-profile.js";
5
4
  import { installDaemonAutomatically, } from "./automatic-daemon-installer.js";
6
5
  import { createComputerSetupClient, openSetupBrowser, redactSetupSecrets, } from "./computer-setup-client.js";
7
6
  import { runComputerSetupOrchestration, } from "./computer-setup-orchestration.js";
8
7
  import { detectDaemonLang, formatDaemonText } from "./i18n.js";
9
8
  import { systemCommandRunner } from "./computer-service.js";
10
- import { runMachineTokenSetup } from "./computer-token-setup.js";
11
9
  const WorkspaceSlugSchema = z.string()
12
10
  .min(2)
13
11
  .max(40)
@@ -72,8 +70,6 @@ export function parseSetupArgs(argv) {
72
70
  assertRepeatedOptionValuesAgree(argv, "machine", (value) => MachineIdSchema.parse(value).toLowerCase());
73
71
  assertRepeatedOptionValuesAgree(argv, "machine-id", (value) => MachineIdSchema.parse(value).toLowerCase());
74
72
  assertRepeatedOptionValuesAgree(argv, "server-url", normalizedServerOrigin);
75
- assertRepeatedOptionValuesAgree(argv, "setup-token", (value) => value);
76
- assertRepeatedOptionValuesAgree(argv, "api-key", (value) => value);
77
73
  const parsed = parseArgs({
78
74
  args: [...argv],
79
75
  allowPositionals: true,
@@ -86,8 +82,6 @@ export function parseSetupArgs(argv) {
86
82
  "server-url": { type: "string" },
87
83
  json: { type: "boolean", default: false },
88
84
  "no-browser": { type: "boolean", default: false },
89
- "setup-token": { type: "string" },
90
- "api-key": { type: "string" },
91
85
  help: { type: "boolean", short: "h", default: false },
92
86
  },
93
87
  });
@@ -130,14 +124,6 @@ export function parseSetupArgs(argv) {
130
124
  }
131
125
  const machineId = parsedMachine;
132
126
  const serverUrl = stringValue(values, "server-url");
133
- const setupToken = stringValue(values, "setup-token");
134
- const machineToken = stringValue(values, "api-key");
135
- if (setupToken !== undefined && machineToken !== undefined)
136
- throw new Error("Choose one setup credential source");
137
- if (setupToken !== undefined && !/^[A-Za-z0-9_-]{32,512}$/u.test(setupToken))
138
- throw new Error("Invalid setup Token");
139
- if (machineToken !== undefined)
140
- MachineTokenSchema.parse(machineToken);
141
127
  return Object.freeze({
142
128
  command: "setup",
143
129
  workspace,
@@ -145,8 +131,6 @@ export function parseSetupArgs(argv) {
145
131
  ...(serverUrl === undefined ? {} : { serverUrl: ServerUrlSchema.parse(serverUrl) }),
146
132
  ...(booleanValue(values, "json") ? { json: true } : {}),
147
133
  ...(booleanValue(values, "no-browser") ? { noBrowser: true } : {}),
148
- ...(setupToken === undefined ? {} : { setupToken }),
149
- ...(machineToken === undefined ? {} : { machineToken }),
150
134
  });
151
135
  }
152
136
  function resolveSetupServerUrl(args, env) {
@@ -164,8 +148,7 @@ export function setupUsage(td = (message) => message) {
164
148
  " nowwork-computer setup --workspace /workspace --machine <uuid>",
165
149
  " nowwork-computer setup --workspace-id <uuid> --machine-id <uuid>",
166
150
  "",
167
- "Options: --server-url <url> --setup-token <token> --api-key <sk_machine_*> --json --no-browser",
168
- td("Token setup does not require browser authorization. CREW_MACHINE_TOKEN is also supported."),
151
+ "Options: --server-url <url> --json --no-browser",
169
152
  ].join("\n") + "\n";
170
153
  }
171
154
  function defaults() {
@@ -184,7 +167,6 @@ function defaults() {
184
167
  }
185
168
  function humanSummary(result, td) {
186
169
  return [
187
- td("Connected to {{workspace}}.", { workspace: result.workspaceSlug ?? result.workspaceId }),
188
170
  td("Workspace: {{value}}", { value: result.workspaceId }),
189
171
  td("Machine: {{value}}", { value: result.machineId }),
190
172
  td("Environment: {{value}}", { value: result.environment }),
@@ -223,7 +205,6 @@ export async function runComputerSetupCommand(argv, overrides = {}) {
223
205
  return null;
224
206
  const deps = { ...defaults(), ...overrides };
225
207
  const lang = detectDaemonLang(deps.env);
226
- const secrets = [...optionOccurrences(argv, "setup-token"), ...optionOccurrences(argv, "api-key"), deps.env.CREW_MACHINE_TOKEN ?? ""];
227
208
  try {
228
209
  if (argv.includes("--help") || argv.includes("-h")) {
229
210
  deps.stdout(setupUsage((message, values) => formatDaemonText(lang, message, values)));
@@ -232,43 +213,32 @@ export async function runComputerSetupCommand(argv, overrides = {}) {
232
213
  let args;
233
214
  try {
234
215
  args = parseSetupArgs(argv);
235
- const envToken = deps.env.CREW_MACHINE_TOKEN?.trim();
236
- if (envToken) {
237
- if (args.setupToken !== undefined || (args.machineToken !== undefined && args.machineToken !== envToken)) {
238
- throw new Error("Choose one setup credential source; clear CREW_MACHINE_TOKEN when using another Token");
239
- }
240
- args = Object.freeze({ ...args, machineToken: MachineTokenSchema.parse(envToken) });
241
- }
242
216
  }
243
217
  catch (error) {
244
- deps.stderr(`nowwork-computer: ${formatDaemonText(lang, redactSetupSecrets(error.message, secrets))}\n`);
218
+ deps.stderr(`nowwork-computer: ${redactSetupSecrets(error.message)}\n`);
245
219
  return 2;
246
220
  }
247
- if (args.json === true && args.setupToken === undefined && args.machineToken === undefined) {
221
+ if (args.json === true) {
248
222
  deps.stderr(`${formatDaemonText(lang, "Starting computer setup pairing...")}\n`);
249
223
  }
250
224
  const serverUrl = resolveSetupServerUrl(args, deps.env);
251
225
  const createClient = overrides.createClient
252
226
  ?? ((value) => createComputerSetupClient({ serverUrl: value, fetch: deps.fetch }));
253
- const orchestrationDeps = {
227
+ const result = await runComputerSetupOrchestration({ args, serverUrl }, {
254
228
  client: createClient(serverUrl),
255
229
  installAutomatically: deps.installAutomatically,
256
230
  openBrowser: args.noBrowser === true ? async () => { } : deps.openBrowser,
257
231
  sleep: deps.sleep,
258
232
  reportPairing: (message) => deps[args.json === true ? "stderr" : "stdout"](`${localizedPairingMessage(lang, message)}\n`),
259
233
  now: deps.now,
260
- platform: deps.platform,
261
- };
262
- const result = args.machineToken === undefined
263
- ? await runComputerSetupOrchestration({ args, serverUrl }, orchestrationDeps)
264
- : await runMachineTokenSetup({ args, serverUrl }, orchestrationDeps, args.machineToken);
234
+ });
265
235
  deps.stdout(args.json === true
266
236
  ? `${JSON.stringify(result)}\n`
267
237
  : humanSummary(result, (message, values) => formatDaemonText(lang, message, values)));
268
238
  return 0;
269
239
  }
270
240
  catch (error) {
271
- deps.stderr(`nowwork-computer: ${formatDaemonText(lang, redactSetupSecrets(error.message, secrets))}\n`);
241
+ deps.stderr(`nowwork-computer: ${redactSetupSecrets(error.message)}\n`);
272
242
  return 1;
273
243
  }
274
244
  }
@@ -587,6 +587,26 @@ export async function runExecution(config, input, dependencies) {
587
587
  let activitySequence = 0;
588
588
  let consoleSequence = 0;
589
589
  let externalOutputSequence = 0;
590
+ const bufferedPiExternalOutput = [];
591
+ const reportExternalOutput = (text) => {
592
+ if (firstOutputTimer !== undefined) {
593
+ clearTimeout(firstOutputTimer);
594
+ firstOutputTimer = undefined;
595
+ }
596
+ const frame = DaemonToServerExecutionFrameSchema.parse({
597
+ type: "execution:output",
598
+ protocolVersion: 1,
599
+ executionId: spec.executionId,
600
+ channel: "external_answer",
601
+ text,
602
+ seq: externalOutputSequence++,
603
+ at: now().toISOString(),
604
+ });
605
+ try {
606
+ telemetry.enqueue(boundExecutionFrame(frame, config.executionLimits.maxEventBytes));
607
+ }
608
+ catch { /* final completion remains the authoritative repair */ }
609
+ };
590
610
  const callbacks = {
591
611
  onRuntimeStarting: () => {
592
612
  dependencies.onRuntimePhase?.("starting", spec.runtime.name);
@@ -711,23 +731,11 @@ export async function runExecution(config, input, dependencies) {
711
731
  } : {}),
712
732
  ...(spec.context.externalResponseSessionId || spec.context.answerStream ? {
713
733
  onExternalOutput: (text) => {
714
- if (firstOutputTimer !== undefined) {
715
- clearTimeout(firstOutputTimer);
716
- firstOutputTimer = undefined;
717
- }
718
- const frame = DaemonToServerExecutionFrameSchema.parse({
719
- type: "execution:output",
720
- protocolVersion: 1,
721
- executionId: spec.executionId,
722
- channel: "external_answer",
723
- text,
724
- seq: externalOutputSequence++,
725
- at: now().toISOString(),
726
- });
727
- try {
728
- telemetry.enqueue(boundExecutionFrame(frame, config.executionLimits.maxEventBytes));
734
+ if (spec.runtime.name === "pi") {
735
+ bufferedPiExternalOutput.push(text);
736
+ return;
729
737
  }
730
- catch { /* final completion remains the authoritative repair */ }
738
+ reportExternalOutput(text);
731
739
  },
732
740
  } : {}),
733
741
  };
@@ -909,6 +917,10 @@ export async function runExecution(config, input, dependencies) {
909
917
  clearTimeout(timeout);
910
918
  if (firstOutputTimer !== undefined)
911
919
  clearTimeout(firstOutputTimer);
920
+ if (spec.runtime.name === "pi" && result.exitCode === 0 && result.externalAnswer !== null) {
921
+ for (const text of bufferedPiExternalOutput)
922
+ reportExternalOutput(text);
923
+ }
912
924
  const finishedAt = now().toISOString();
913
925
  if (result.exitCode === 0 && result.finalText?.trim())
914
926
  memoryCaptureFinalText = result.finalText;
@@ -110,6 +110,7 @@ export class ExternalAnswerDecoder {
110
110
  pending = "";
111
111
  block = "";
112
112
  closed = null;
113
+ rejected = false;
113
114
  push(text) {
114
115
  if (!text)
115
116
  return [];
@@ -125,7 +126,7 @@ export class ExternalAnswerDecoder {
125
126
  }
126
127
  /** 整轮里最后一个完整闭合的标签块(已 trim);没有闭合过任何块则为 null。 */
127
128
  get lastClosedBlock() {
128
- return this.closed;
129
+ return this.rejected ? null : this.closed;
129
130
  }
130
131
  consume(value, output) {
131
132
  this.pending = "";
@@ -154,12 +155,21 @@ export class ExternalAnswerDecoder {
154
155
  this.block = "";
155
156
  this.state = "inside";
156
157
  }
158
+ else if (!tag.closing && this.state === "inside") {
159
+ // 嵌套答案标记说明模型正在讨论协议或混入了思考文本;
160
+ // 不要把内层短片段误当成最终答案。
161
+ this.rejected = true;
162
+ this.block = "";
163
+ this.state = "outside";
164
+ }
157
165
  else {
166
+ this.rejected = true;
158
167
  this.block = "";
159
168
  this.state = tag.closing ? "outside" : "inside";
160
169
  }
161
170
  }
162
171
  else if (tag.kind === "suspicious" || tag.kind === "known") {
172
+ this.rejected = true;
163
173
  this.block = "";
164
174
  this.state = "outside";
165
175
  }
@@ -171,6 +181,7 @@ export class ExternalAnswerDecoder {
171
181
  else if (this.pending.length > MAX_MARKER_TAG_LENGTH + 2) {
172
182
  const tag = classifyMarkerTag(this.pending.slice(1));
173
183
  if (tag.kind === "suspicious" || tag.kind === "known") {
184
+ this.rejected = true;
174
185
  this.block = "";
175
186
  this.state = "outside";
176
187
  }
package/dist/i18n.js CHANGED
@@ -36,16 +36,6 @@ const zh = {
36
36
  "Code {{code}}": "验证码 {{code}}",
37
37
  "Browser open skipped: {{reason}}": "未能自动打开浏览器:{{reason}}",
38
38
  "Starting computer setup pairing...": "正在发起电脑接入授权…",
39
- "Token setup does not require browser authorization. CREW_MACHINE_TOKEN is also supported.": "Token 接入无需浏览器授权,也支持 CREW_MACHINE_TOKEN。",
40
- "Checking computer connection information...": "正在检查接入信息…",
41
- "Downloading, installing, and starting the Daemon service...": "正在下载、安装并启动 Daemon 服务…",
42
- "Confirming the computer connection...": "正在确认电脑连接…",
43
- "Connected to {{workspace}}.": "已接入 {{workspace}}。",
44
- "Setup command expired or unavailable; regenerate it in NowWork and run the new command": "接入命令已过期或不可用,请在 NowWork 重新生成并执行新命令。",
45
- "Timed out confirming the new computer connection": "确认新连接超时,请检查网络和服务状态。",
46
- "Invalid setup Token": "接入 Token 无效。",
47
- "Choose one setup credential source": "请只使用一种接入凭据。",
48
- "Choose one setup credential source; clear CREW_MACHINE_TOKEN when using another Token": "请只使用一种接入凭据;使用其他 Token 时请清除 CREW_MACHINE_TOKEN。",
49
39
  "Workspace: {{value}}": "工作区:{{value}}",
50
40
  "Machine: {{value}}": "机器: {{value}}",
51
41
  "Environment: {{value}}": "环境:{{value}}",