@platless/axom-cli 0.5.9 → 0.6.36

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 (163) hide show
  1. package/README.md +18 -5
  2. package/dist/axom-patch/cli/README.md +100 -12
  3. package/dist/axom-patch/cli/account-limits/generate.py +15 -12
  4. package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +20 -3
  5. package/dist/axom-patch/cli/claude-account-limits.json +9 -9
  6. package/dist/axom-patch/cli/codex-account-limits.json +11 -11
  7. package/dist/axom-patch/cli/codex-terminal-output.json +16 -0
  8. package/dist/axom-patch/cli/commandcode-account-limits.json +9 -9
  9. package/dist/axom-patch/cli/kimi-quota.mjs +12 -0
  10. package/dist/axom-patch/cli/kimi-subagents.mjs +173 -0
  11. package/dist/axom-patch/cli/nightly-task.md +55 -13
  12. package/dist/axom-patch/cli/probes/codex-terminal-output.mjs +33 -0
  13. package/dist/axom-patch/cli/probes/kimi-subagent-subwork.mjs +82 -0
  14. package/dist/axom-patch/cli/qoder/dist/axom-acp-compat.mjs +1591 -0
  15. package/dist/axom-patch/cli/qoder/releases/1.1.75.json +360 -0
  16. package/dist/axom-patch/cli/qoder-background-terminal.md +13 -6
  17. package/dist/axom-patch/cli/qoder-foreground-output.md +14 -8
  18. package/dist/axom-patch/cli/qoder-native-acp.json +227 -0
  19. package/dist/axom-patch/cli/qoder-steering.md +20 -25
  20. package/dist/axom-patch/cli/qoder-subagent-subwork.md +17 -68
  21. package/dist/axom-patch/cli/runtime.json +16 -18
  22. package/dist/axom-patch/cli/zcode-account-limits.json +10 -11
  23. package/dist/axom-patch/cli/zcode-models.json +1 -2
  24. package/dist/axom-patch/cli/zcode-subagent-subwork.json +9 -10
  25. package/dist/cli.js +4 -36
  26. package/dist/commands/knowledge.js +47 -0
  27. package/dist/commands/update.js +65 -3
  28. package/dist/commands/workstation.js +73 -7
  29. package/dist/install/claude/commands.js +13 -13
  30. package/dist/mcp.js +1 -1
  31. package/dist/native/darwin-arm64/axom-quic +0 -0
  32. package/dist/workstation/acp/acpAgents.js +102 -98
  33. package/dist/workstation/acp/acpClient.js +26 -0
  34. package/dist/workstation/acp/builds/install/npm.js +99 -10
  35. package/dist/workstation/acp/initializeResponse.js +6 -1
  36. package/dist/workstation/acp/managedPatches.js +189 -25
  37. package/dist/workstation/acp/managedRuntime.js +314 -0
  38. package/dist/workstation/acp/patchAssets.js +40 -0
  39. package/dist/workstation/acp/processor/checkpoint.js +25 -34
  40. package/dist/workstation/acp/processor/control.js +5 -78
  41. package/dist/workstation/acp/processor/runtime.js +5 -81
  42. package/dist/workstation/acp/processor/session.js +51 -80
  43. package/dist/workstation/acp/processor/state.js +6 -13
  44. package/dist/workstation/acp/processor/step.js +3 -20
  45. package/dist/workstation/acp/processor/test/fixtureLog.js +9 -16
  46. package/dist/workstation/acp/processor/updates.js +131 -105
  47. package/dist/workstation/acp/processorSession.js +8 -172
  48. package/dist/workstation/acp/registry/catalog.js +65 -39
  49. package/dist/workstation/acp/registry/launch.js +16 -1
  50. package/dist/workstation/acp/switching.js +0 -3
  51. package/dist/workstation/acp/test/fakeAgent.js +13 -0
  52. package/dist/workstation/acp/test/keeperWorld.js +28 -0
  53. package/dist/workstation/acp/test/raw/sources.js +16 -130
  54. package/dist/workstation/acp/test/realAdapter.js +1 -47
  55. package/dist/workstation/acp/test/scriptAgent.js +11 -3
  56. package/dist/workstation/agentFlow.js +1 -1
  57. package/dist/workstation/agentSessions.js +38 -143
  58. package/dist/workstation/channels/management.js +95 -11
  59. package/dist/workstation/channels/url.js +1 -1
  60. package/dist/workstation/config.js +16 -4
  61. package/dist/workstation/connection/encryptedChannel.js +4 -1
  62. package/dist/workstation/control/bindingStore.js +200 -0
  63. package/dist/workstation/control/commands.js +124 -0
  64. package/dist/workstation/control/daemonIdentity.js +53 -0
  65. package/dist/workstation/control/enrollment.js +106 -0
  66. package/dist/workstation/control/inspect.js +71 -0
  67. package/dist/workstation/control/locks.js +63 -0
  68. package/dist/workstation/control/protocol.js +40 -0
  69. package/dist/workstation/control/restartGuard.js +46 -0
  70. package/dist/workstation/control/runtimeClient.js +97 -0
  71. package/dist/workstation/control/runtimeHost.js +148 -0
  72. package/dist/workstation/control/serverIdentity.js +35 -0
  73. package/dist/workstation/daemon.js +106 -6
  74. package/dist/workstation/daemonctl.js +77 -14
  75. package/dist/workstation/desktopControl/audio.js +99 -0
  76. package/dist/workstation/desktopControl/capabilities.js +92 -0
  77. package/dist/workstation/desktopControl/captureModes.js +9 -0
  78. package/dist/workstation/desktopControl/captureTargetLabel.js +9 -0
  79. package/dist/workstation/desktopControl/deviceSockets.js +246 -0
  80. package/dist/workstation/desktopControl/helperProcess.js +353 -0
  81. package/dist/workstation/desktopControl/lobby.js +129 -0
  82. package/dist/workstation/desktopControl/macProvider.js +155 -0
  83. package/dist/workstation/desktopControl/media.js +331 -0
  84. package/dist/workstation/desktopControl/nativeProvider.js +1018 -0
  85. package/dist/workstation/desktopControl/networkEvidence.js +43 -0
  86. package/dist/workstation/desktopControl/overview.js +58 -0
  87. package/dist/workstation/desktopControl/protocol.js +302 -0
  88. package/dist/workstation/desktopControl/quicMedia.js +240 -0
  89. package/dist/workstation/desktopControl/rateController.js +226 -0
  90. package/dist/workstation/desktopControl/supervisor.js +1681 -0
  91. package/dist/workstation/desktopControl/transport.js +27 -0
  92. package/dist/workstation/desktopControl/wallpaper.js +78 -0
  93. package/dist/workstation/files/credentials.js +142 -0
  94. package/dist/workstation/files/text.js +42 -8
  95. package/dist/workstation/keepers/kit/bundle.js +21 -17
  96. package/dist/workstation/keepers/kit/launcher.js +47 -7
  97. package/dist/workstation/keepers/kit/paths.js +2 -1
  98. package/dist/workstation/managedInstall.js +57 -0
  99. package/dist/workstation/protocol/agent.js +2 -15
  100. package/dist/workstation/protocol/management.js +93 -4
  101. package/dist/workstation/protocol/runtimeDiagnostics.js +15 -0
  102. package/dist/workstation/rd/artifact.js +119 -0
  103. package/dist/workstation/rd/control.js +119 -0
  104. package/dist/workstation/rd/display.js +29 -0
  105. package/dist/workstation/rd/provider.js +222 -0
  106. package/dist/workstation/rd/sessions.js +391 -0
  107. package/dist/workstation/rd/state.js +45 -0
  108. package/dist/workstation/rd/update.js +171 -0
  109. package/dist/workstation/rd/upkeep.js +38 -0
  110. package/dist/workstation/register.js +27 -37
  111. package/dist/workstation/service/installer.js +104 -13
  112. package/dist/workstation/service/launchd.js +1 -0
  113. package/dist/workstation/storageMetrics.js +118 -0
  114. package/dist/workstation/systemMetrics.js +44 -92
  115. package/dist/workstation/toolchain.js +94 -0
  116. package/dist/workstation/tools/browser.js +28 -4
  117. package/dist/workstation/update/artifact.js +2 -1
  118. package/dist/workstation/update/globalInstall.js +116 -0
  119. package/dist/workstation/update/npmInstaller.js +49 -23
  120. package/dist/workstation/update/rdRollback.js +145 -0
  121. package/dist/workstation/update/runUpdate.js +3 -2
  122. package/dist/workstation/update/watchdog.js +16790 -334
  123. package/package.json +5 -5
  124. package/skills/axom/SKILL.md +11 -11
  125. package/skills/axom-definition/SKILL.md +3 -3
  126. package/skills/axom-platform/SKILL.md +3 -3
  127. package/dist/axom-patch/cli/probes/qoder-steering.mjs +0 -89
  128. package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +0 -61
  129. package/dist/axom-patch/cli/qoder-steering.json +0 -52
  130. package/dist/axom-patch/cli/qoder-subagent-origin.json +0 -73
  131. package/dist/commands/guide.js +0 -38
  132. package/dist/runtime-deps/qoder-sdk/package-lock.json +0 -1189
  133. package/dist/runtime-deps/qoder-sdk/package.json +0 -8
  134. package/dist/workstation/acp/delegationMcp.js +0 -24
  135. package/dist/workstation/acp/processor/sideChannel.js +0 -80
  136. package/dist/workstation/acp/qoderStartup.js +0 -45
  137. package/dist/workstation/acp/test/raw/qoderCaptures.js +0 -125
  138. package/dist/workstation/qoderAdapter/backgroundProcess.js +0 -118
  139. package/dist/workstation/qoderAdapter/backgroundTerminal.js +0 -193
  140. package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +0 -372
  141. package/dist/workstation/qoderAdapter/callbackContext.js +0 -76
  142. package/dist/workstation/qoderAdapter/completedContinuation.js +0 -139
  143. package/dist/workstation/qoderAdapter/contract.js +0 -78
  144. package/dist/workstation/qoderAdapter/foregroundOutput.js +0 -36
  145. package/dist/workstation/qoderAdapter/history.js +0 -85
  146. package/dist/workstation/qoderAdapter/identityIndex.js +0 -191
  147. package/dist/workstation/qoderAdapter/inputQueue.js +0 -42
  148. package/dist/workstation/qoderAdapter/main.js +0 -95
  149. package/dist/workstation/qoderAdapter/mapper.js +0 -117
  150. package/dist/workstation/qoderAdapter/observerPlugin.js +0 -245
  151. package/dist/workstation/qoderAdapter/observerReceiver.js +0 -187
  152. package/dist/workstation/qoderAdapter/permissions.js +0 -61
  153. package/dist/workstation/qoderAdapter/profile.js +0 -93
  154. package/dist/workstation/qoderAdapter/sdkDriver.js +0 -48
  155. package/dist/workstation/qoderAdapter/sdkRuntime.js +0 -93
  156. package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +0 -145
  157. package/dist/workstation/qoderAdapter/session.js +0 -319
  158. package/dist/workstation/qoderAdapter/subagentMeta.js +0 -28
  159. package/dist/workstation/qoderAdapter/subwork.js +0 -85
  160. package/dist/workstation/qoderAdapter/taskControl.js +0 -38
  161. package/dist/workstation/qoderAdapter/taskLedger.js +0 -163
  162. package/dist/workstation/qoderAdapter/usage.js +0 -37
  163. /package/dist/workstation/{qoderAdapter/types.js → desktopControl/provider.js} +0 -0
@@ -17,13 +17,31 @@ async function bounded(promise, timeout = TIMEOUT) {
17
17
  clearTimeout(timer);
18
18
  }
19
19
  }
20
- /** All ancestors are checked: mkdir recursive must never follow a planted profile symlink. */
20
+ /**
21
+ * All ancestors are checked: mkdir recursive must never follow a planted profile symlink. The one
22
+ * symlinks allowed across are root-owned system symlinks (`/var -> /private/var` on macOS): stock
23
+ * macOS tmp dirs sit behind one, and refusing it would reject every per-user tmpdir. A root-owned
24
+ * symlink whose target is a root-owned, non-group-writable directory is not plantable by the user
25
+ * this runs as; anything else — every user-owned symlink — still fails the check below.
26
+ */
21
27
  async function ownedDirectory(dir, privateMode = false) {
22
28
  const absolute = path.resolve(dir);
23
29
  const parts = absolute.split(path.sep).filter(Boolean);
24
30
  let current = path.parse(absolute).root;
25
31
  const uid = process.getuid?.();
26
32
  let protectedByOwner = false;
33
+ const systemSymlink = async (stat) => {
34
+ if (stat.uid !== 0 || uid === 0)
35
+ return false;
36
+ try {
37
+ const target = await fs.realpath(current);
38
+ const targetStat = await fs.lstat(target);
39
+ return targetStat.isDirectory() && targetStat.uid === 0 && (targetStat.mode & 0o022) === 0;
40
+ }
41
+ catch {
42
+ return false;
43
+ }
44
+ };
27
45
  for (const part of parts) {
28
46
  current = path.join(current, part);
29
47
  await fs.mkdir(current, { mode: 0o700 }).catch((e) => {
@@ -31,8 +49,14 @@ async function ownedDirectory(dir, privateMode = false) {
31
49
  throw e;
32
50
  });
33
51
  const stat = await fs.lstat(current);
52
+ if (stat.isSymbolicLink()) {
53
+ if (!(await systemSymlink(stat)))
54
+ throw Error("Unsafe browser directory");
55
+ // Root-owned system symlink: cross it; the link itself carries no mode or ownership
56
+ // expectation (Linux reports links 0777), its target was just verified.
57
+ continue;
58
+ }
34
59
  if (!stat.isDirectory() ||
35
- stat.isSymbolicLink() ||
36
60
  (!protectedByOwner &&
37
61
  (stat.mode & 0o022) !== 0 &&
38
62
  !(stat.mode & 0o1000)) ||
@@ -44,11 +68,11 @@ async function ownedDirectory(dir, privateMode = false) {
44
68
  if (uid !== undefined && stat.uid === uid && (stat.mode & 0o077) === 0)
45
69
  protectedByOwner = true;
46
70
  }
47
- const stat = await fs.lstat(absolute);
71
+ const stat = await fs.lstat(current);
48
72
  if (process.getuid && stat.uid !== process.getuid())
49
73
  throw Error("Browser directory is not owned by this user");
50
74
  if (privateMode)
51
- await fs.chmod(absolute, 0o700);
75
+ await fs.chmod(current, 0o700);
52
76
  }
53
77
  function allowedUrl(params) {
54
78
  const url = string(params, "url", 8192, "about:blank");
@@ -1,3 +1,4 @@
1
+ import { deploymentDownloadURL } from '../control/serverIdentity.js';
1
2
  import { createHash, timingSafeEqual } from 'node:crypto';
2
3
  import { createWriteStream, promises as fs } from 'node:fs';
3
4
  import { tmpdir } from 'node:os';
@@ -16,7 +17,7 @@ function safeRelease(release, server) {
16
17
  if ((configured.protocol !== 'http:' && configured.protocol !== 'https:') || configured.username || configured.password) {
17
18
  throw new Error('Configured server URL must be an HTTP(S) origin without credentials');
18
19
  }
19
- const url = new URL(release.url, configured);
20
+ const url = new URL(deploymentDownloadURL(server, release.url));
20
21
  // The release descriptor arrives over the authenticated management connection, but keeping the
21
22
  // download on that exact origin prevents an injected relative/absolute URL from turning the
22
23
  // client into a credentialed downloader. HTTP remains supported for private-network development
@@ -0,0 +1,116 @@
1
+ import { spawn as nodeSpawn } from 'node:child_process';
2
+ import { promises as fs } from 'node:fs';
3
+ import path from 'node:path';
4
+ /**
5
+ * `npm install --global <archive>` reinstalls an already-present package by renaming the old
6
+ * install directory aside ("retire") and extracting the new one in its place. On at least one
7
+ * observed host (a container whose target directory sits on the image's read-only layers, backed
8
+ * by `fuse-overlayfs`) that retire-then-extract sequence is unreliable: `npm`'s own tar/arborist
9
+ * step intermittently reports `TAR_ENTRY_ERROR EEXIST` on the package's top-level `package.json`,
10
+ * immediately followed by `ENOENT` when npm reads the same path back — on the *same* path, even
11
+ * starting from an empty directory, and reproducibly across repeated attempts. A plain `tar`
12
+ * extraction of the identical archive onto the identical path, and a non-global `npm install` run
13
+ * inside an already-extracted directory, both succeed every time: the bug is specific to npm's
14
+ * own global-package retire/link machinery, not to the archive, the dependencies, or the
15
+ * filesystem's ability to write there at all.
16
+ *
17
+ * This is therefore a fallback, not the primary path: it only runs after a real `npm install
18
+ * --global` attempt has already failed, and it reimplements only what that attempt was observably
19
+ * unable to do reliably --- replace the target directory's contents and its `bin` entry --- using
20
+ * the two operations proven to work. It intentionally does not run on Windows, where npm's global
21
+ * `bin` entries are shim files (`.cmd`/`.ps1`), not symlinks, and this path has no evidence either
22
+ * way.
23
+ */
24
+ export async function attemptGlobalInstallFallback(npmPath, npmPrefix, archive, env, spawn = nodeSpawn) {
25
+ if (process.platform === 'win32')
26
+ throw new Error('No tested fallback for a global npm install on Windows');
27
+ const manifest = await readArchivePackageJson(archive, spawn);
28
+ const name = manifest.name;
29
+ if (typeof name !== 'string' || name.length === 0)
30
+ throw new Error('Archive package.json has no usable "name"');
31
+ const bin = manifest.bin;
32
+ const target = path.join(npmPrefix, 'lib/node_modules', name);
33
+ await fs.rm(target, { recursive: true, force: true });
34
+ await fs.mkdir(target, { recursive: true });
35
+ await run(spawn, 'tar', ['-xzf', archive, '-C', target, '--strip-components=1'], {});
36
+ const installEnv = { ...process.env, ...env };
37
+ delete installEnv.NPM_CONFIG_PREFIX;
38
+ delete installEnv.npm_config_prefix;
39
+ await run(spawn, npmPath, ['install', '--omit=dev', '--ignore-scripts', '--no-audit', '--no-fund'], { cwd: target, env: installEnv });
40
+ await relinkBinaries(npmPrefix, target, name, bin);
41
+ }
42
+ async function readArchivePackageJson(archive, spawn) {
43
+ const raw = await capture(spawn, 'tar', ['-xzf', archive, '-O', 'package/package.json'], {});
44
+ try {
45
+ return JSON.parse(raw);
46
+ }
47
+ catch {
48
+ throw new Error('Archive package.json is not valid JSON');
49
+ }
50
+ }
51
+ /** Reproduces npm's own `bin` linking: a relative symlink from `<prefix>/bin/<name>` to the script. */
52
+ async function relinkBinaries(npmPrefix, target, packageName, bin) {
53
+ const entries = binEntries(packageName, bin);
54
+ if (entries.length === 0)
55
+ return;
56
+ const binDir = path.join(npmPrefix, 'bin');
57
+ await fs.mkdir(binDir, { recursive: true });
58
+ for (const [command, relativeScript] of entries) {
59
+ const script = path.join(target, relativeScript);
60
+ const link = path.join(binDir, command);
61
+ await fs.rm(link, { force: true });
62
+ await fs.symlink(path.relative(binDir, script), link);
63
+ await fs.chmod(script, 0o755).catch(() => undefined);
64
+ }
65
+ }
66
+ /** Exported for direct unit testing; same resolution npm itself applies to a package.json `bin` field. */
67
+ export function binEntries(packageName, bin) {
68
+ if (typeof bin === 'string') {
69
+ const short = packageName.startsWith('@') ? packageName.split('/')[1] ?? packageName : packageName;
70
+ return [[short, bin]];
71
+ }
72
+ if (bin && typeof bin === 'object') {
73
+ return Object.entries(bin).filter((entry) => typeof entry[1] === 'string');
74
+ }
75
+ return [];
76
+ }
77
+ function run(spawn, command, args, options) {
78
+ return new Promise((resolve, reject) => {
79
+ let child;
80
+ try {
81
+ child = spawn(command, args, { ...options, shell: false, stdio: 'ignore', windowsHide: true });
82
+ }
83
+ catch (error) {
84
+ reject(error instanceof Error ? error : new Error(String(error)));
85
+ return;
86
+ }
87
+ child.once('error', reject);
88
+ child.once('close', (code, signal) => {
89
+ if (code === 0)
90
+ resolve();
91
+ else
92
+ reject(new Error(`${command} failed: exit ${code ?? signal ?? 'unknown'}`));
93
+ });
94
+ });
95
+ }
96
+ function capture(spawn, command, args, options) {
97
+ return new Promise((resolve, reject) => {
98
+ let child;
99
+ try {
100
+ child = spawn(command, args, { ...options, shell: false, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true });
101
+ }
102
+ catch (error) {
103
+ reject(error instanceof Error ? error : new Error(String(error)));
104
+ return;
105
+ }
106
+ let stdout = '';
107
+ child.stdout?.on('data', (chunk) => { stdout += chunk.toString(); });
108
+ child.once('error', reject);
109
+ child.once('close', (code, signal) => {
110
+ if (code === 0)
111
+ resolve(stdout);
112
+ else
113
+ reject(new Error(`${command} failed: exit ${code ?? signal ?? 'unknown'}`));
114
+ });
115
+ });
116
+ }
@@ -1,6 +1,8 @@
1
+ import { withWorkstationLock } from '../control/locks.js';
1
2
  import { spawn as nodeSpawn } from 'node:child_process';
2
3
  import { redactPaths, redactSecrets } from './redact.js';
3
4
  import { verifyOptionalPty } from './watchdog.js';
5
+ import { attemptGlobalInstallFallback } from './globalInstall.js';
4
6
  const STDERR_LIMIT = 4096;
5
7
  function absolute(value, field) {
6
8
  if (!value.startsWith('/') || /[\u0000-\u001f\u007f]/.test(value))
@@ -21,33 +23,57 @@ export class NpmInstaller {
21
23
  this.spawn = options.spawn ?? nodeSpawn;
22
24
  }
23
25
  async install(archive) {
26
+ if (this.options.configDir && !this.options.installationLockHeld)
27
+ return withWorkstationLock(this.options.configDir, 'installation', () => this.installUnlocked(archive));
28
+ return this.installUnlocked(archive);
29
+ }
30
+ async installUnlocked(archive) {
24
31
  absolute(archive, 'archive');
25
- // No `--prefix`: npm installs into its own configured global prefix, which is what puts `axom`
26
- // on the user's PATH. Forcing a private prefix put the binary somewhere nothing looks, so every
27
- // machine needed a hand-made symlink that nothing in this package created or documented.
28
- // `npmPrefix` survives for the writability pre-check, the service definition, and rollback —
29
- // it is derived from where this package actually sits, so it still names npm's real prefix.
32
+ // Pin the already selected installation, including ordinary global npm installs.
33
+ const env = { ...process.env, ...this.options.env, NPM_CONFIG_PREFIX: this.options.npmPrefix };
34
+ delete env.npm_config_prefix;
30
35
  const args = ['install', '--global', archive, '--ignore-scripts', '--no-audit', '--no-fund'];
31
- await new Promise((resolve, reject) => {
32
- let stderr = '';
33
- let child;
36
+ try {
37
+ await new Promise((resolve, reject) => {
38
+ let stderr = '';
39
+ let child;
40
+ try {
41
+ child = this.spawn(this.options.npmPath, args, { env, shell: false, stdio: ['ignore', 'ignore', 'pipe'], windowsHide: true });
42
+ }
43
+ catch (error) {
44
+ reject(new Error(`npm install could not start: ${sanitizeNpmError(error instanceof Error ? error.message : String(error))}`));
45
+ return;
46
+ }
47
+ child.stderr?.on('data', (chunk) => { if (stderr.length < STDERR_LIMIT)
48
+ stderr += chunk.toString().slice(0, STDERR_LIMIT - stderr.length); });
49
+ child.once('error', (error) => reject(new Error(`npm install could not start: ${sanitizeNpmError(error.message)}`)));
50
+ child.once('close', (code, signal) => {
51
+ if (code === 0)
52
+ resolve();
53
+ else
54
+ reject(new Error(`npm install failed: ${sanitizeNpmError(stderr) || `exit ${code ?? signal ?? 'unknown'}`}`));
55
+ });
56
+ });
57
+ }
58
+ catch (primaryError) {
59
+ // `npm install --global` reinstalling an already-present package renames the old install
60
+ // directory aside and extracts the new one in its place; that sequence has been observed to
61
+ // fail unreliably on at least one host/filesystem combination even from an empty target (see
62
+ // globalInstall.ts). The fallback reproduces only the two operations proven to work there. A
63
+ // fallback failure surfaces the *original* error: it is the one a real npm bug report needs.
64
+ //
65
+ // Both console lines below are the only record of how often this fallback actually fires
66
+ // fleet-wide — there is no telemetry channel for "the primary path failed but we recovered",
67
+ // and a daemon's own stdout/stderr is already collected wherever this process's logs are.
68
+ console.warn(`npm install --global failed, attempting the tar/local-install fallback: ${sanitizeNpmError(primaryError instanceof Error ? primaryError.message : String(primaryError))}`);
34
69
  try {
35
- child = this.spawn(this.options.npmPath, args, { shell: false, stdio: ['ignore', 'ignore', 'pipe'], windowsHide: true });
70
+ await attemptGlobalInstallFallback(this.options.npmPath, this.options.npmPrefix, archive, this.options.env, this.spawn);
36
71
  }
37
- catch (error) {
38
- reject(new Error(`npm install could not start: ${sanitizeNpmError(error instanceof Error ? error.message : String(error))}`));
39
- return;
72
+ catch {
73
+ throw primaryError;
40
74
  }
41
- child.stderr?.on('data', (chunk) => { if (stderr.length < STDERR_LIMIT)
42
- stderr += chunk.toString().slice(0, STDERR_LIMIT - stderr.length); });
43
- child.once('error', (error) => reject(new Error(`npm install could not start: ${sanitizeNpmError(error.message)}`)));
44
- child.once('close', (code, signal) => {
45
- if (code === 0)
46
- resolve();
47
- else
48
- reject(new Error(`npm install failed: ${sanitizeNpmError(stderr) || `exit ${code ?? signal ?? 'unknown'}`}`));
49
- });
50
- });
75
+ console.warn('npm install --global fallback succeeded; the global reinstall step is unreliable on this host and should be investigated');
76
+ }
51
77
  await verifyOptionalPty(this.options.npmPrefix, this.spawn);
52
78
  }
53
79
  /** Packs the installed package locally; `--json` gives npm's own name/version validation input. */
@@ -60,7 +86,7 @@ export class NpmInstaller {
60
86
  let stderr = '';
61
87
  let child;
62
88
  try {
63
- child = this.spawn(this.options.npmPath, args, { shell: false, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true });
89
+ child = this.spawn(this.options.npmPath, args, { ...(this.options.env ? { env: this.options.env } : {}), shell: false, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true });
64
90
  }
65
91
  catch (error) {
66
92
  reject(new Error(`npm pack could not start: ${sanitizeNpmError(error instanceof Error ? error.message : String(error))}`));
@@ -0,0 +1,145 @@
1
+ import { constants, promises as fs } from 'node:fs';
2
+ import { createHash, randomUUID } from 'node:crypto';
3
+ import path from 'node:path';
4
+ import { assertLockOwnership, withPrivateLock } from '../control/locks.js';
5
+ import { privateDirectory } from '../keepers/kit/paths.js';
6
+ import { rdStateSchema } from '../rd/state.js';
7
+ export function rdStateDigest(value) { return createHash('sha256').update(value).digest('hex'); }
8
+ function snapshotPath(directory, commandId) { return path.join(directory, 'update', `rd-rollback-${rdStateDigest(commandId)}.json`); }
9
+ export async function readPrivateRdFile(file, maxBytes = 131072) {
10
+ let handle;
11
+ try {
12
+ handle = await fs.open(file, constants.O_RDONLY | constants.O_NOFOLLOW);
13
+ }
14
+ catch (error) {
15
+ if (error.code === 'ENOENT')
16
+ return null;
17
+ throw error;
18
+ }
19
+ try {
20
+ const info = await handle.stat();
21
+ if (!info.isFile() || info.uid !== process.getuid?.() || info.mode & 0o077 || info.size > maxBytes)
22
+ throw new Error('Unsafe RD rollback state');
23
+ return await handle.readFile('utf8');
24
+ }
25
+ finally {
26
+ await handle.close();
27
+ }
28
+ }
29
+ async function writePrivate(file, contents) {
30
+ await privateDirectory(path.dirname(file));
31
+ const staging = path.join(path.dirname(file), `.rd-rollback-${randomUUID()}`);
32
+ try {
33
+ await fs.writeFile(staging, contents, { mode: 0o600, flag: 'wx' });
34
+ assertLockOwnership();
35
+ await fs.rename(staging, file);
36
+ }
37
+ finally {
38
+ await fs.rm(staging, { force: true });
39
+ }
40
+ }
41
+ export async function snapshotRdForRollback(directory, command) {
42
+ return withPrivateLock(path.join(directory, 'rd', 'install'), async () => {
43
+ const previous = await readPrivateRdFile(path.join(directory, 'rd', 'state.json'), 32768);
44
+ const snapshot = snapshotPath(directory, command.commandId);
45
+ const value = { schemaVersion: 1, commandId: command.commandId, targetVersion: command.release.version,
46
+ releaseSha512: command.release.sha512, previous };
47
+ await writePrivate(snapshot, JSON.stringify(value));
48
+ return { directory, snapshot };
49
+ });
50
+ }
51
+ async function pendingUpdate(directory) {
52
+ const contents = await readPrivateRdFile(path.join(directory, 'update-state.json'));
53
+ return contents === null ? null : JSON.parse(contents);
54
+ }
55
+ export async function guardRdActivation(directory, cliVersion, next) {
56
+ const update = await pendingUpdate(directory);
57
+ if (!update || update.version !== cliVersion || !update.commandId)
58
+ return;
59
+ if (update.phase === 'ROLLED_BACK')
60
+ throw new Error('The CLI update was already rolled back before RD activation');
61
+ if (!update.releaseSha512)
62
+ return;
63
+ const planFile = path.join(directory, 'update', `watchdog-${rdStateDigest(update.commandId)}.json`);
64
+ const planContents = await readPrivateRdFile(planFile);
65
+ if (planContents === null)
66
+ return;
67
+ const plan = JSON.parse(planContents);
68
+ if (plan.commandId !== update.commandId || plan.targetVersion !== update.version)
69
+ throw new Error('RD watchdog plan does not match the pending CLI update');
70
+ const current = await readPrivateRdFile(path.join(directory, 'rd', 'state.json'), 32768);
71
+ if (current === null)
72
+ throw new Error('RD state disappeared before activation');
73
+ const guard = { commandId: update.commandId, targetVersion: update.version, releaseSha512: update.releaseSha512, previousSha256: rdStateDigest(current) };
74
+ if (!plan.rdRollback)
75
+ return await authenticatedRdActivation(directory, guard) ? undefined : guard;
76
+ const expected = snapshotPath(directory, update.commandId);
77
+ if (plan.rdRollback.directory !== directory || plan.rdRollback.snapshot !== expected)
78
+ throw new Error('RD rollback snapshot path does not match this update');
79
+ const contents = await readPrivateRdFile(expected);
80
+ if (contents === null)
81
+ throw new Error('RD rollback snapshot is missing');
82
+ const saved = JSON.parse(contents);
83
+ if (saved.schemaVersion !== 1 || saved.commandId !== update.commandId || saved.targetVersion !== update.version || saved.releaseSha512 !== update.releaseSha512)
84
+ throw new Error('RD rollback snapshot belongs to another CLI update');
85
+ const nextDigest = rdStateDigest(JSON.stringify(rdStateSchema.parse(next)));
86
+ if (saved.previous !== current && saved.activatedSha256 !== rdStateDigest(current))
87
+ return guard;
88
+ await writePrivate(expected, JSON.stringify({ ...saved, activatedSha256: nextDigest, activatedIdentity: undefined }));
89
+ }
90
+ async function stateIdentity(directory) {
91
+ const info = await fs.lstat(path.join(directory, 'rd', 'state.json'), { bigint: true });
92
+ return `${info.dev}:${info.ino}:${info.birthtimeNs}:${info.mtimeNs}`;
93
+ }
94
+ export async function completeRdActivation(directory, cliVersion) {
95
+ const update = await pendingUpdate(directory);
96
+ if (!update?.commandId || update.version !== cliVersion)
97
+ return;
98
+ const file = snapshotPath(directory, update.commandId);
99
+ const contents = await readPrivateRdFile(file);
100
+ if (contents === null)
101
+ return;
102
+ const saved = JSON.parse(contents);
103
+ if (saved.commandId !== update.commandId || saved.targetVersion !== update.version || saved.releaseSha512 !== update.releaseSha512 || !saved.activatedSha256)
104
+ return;
105
+ const active = await readPrivateRdFile(path.join(directory, 'rd', 'state.json'), 32768);
106
+ if (active === null || rdStateDigest(active) !== saved.activatedSha256)
107
+ return;
108
+ await writePrivate(file, JSON.stringify({ ...saved, activatedIdentity: await stateIdentity(directory) }));
109
+ }
110
+ export async function authenticatedRdActivation(directory, guard) {
111
+ const update = await pendingUpdate(directory);
112
+ if (update?.phase !== 'SUCCEEDED' || update.commandId !== guard.commandId || update.version !== guard.targetVersion || update.releaseSha512 !== guard.releaseSha512)
113
+ return false;
114
+ const contents = await readPrivateRdFile(path.join(directory, 'update', 'restart-ready.json'));
115
+ if (contents === null)
116
+ return false;
117
+ const marker = JSON.parse(contents);
118
+ return marker.commandId === guard.commandId && marker.version === guard.targetVersion && typeof marker.generation === 'string' && marker.generation.length > 0;
119
+ }
120
+ export async function restoreRdAfterRollback(reference, commandId, targetVersion) {
121
+ const directory = reference.directory;
122
+ if (reference.snapshot !== snapshotPath(directory, commandId))
123
+ throw new Error('RD rollback snapshot path does not match this update');
124
+ await withPrivateLock(path.join(directory, 'rd', 'install'), async () => {
125
+ const update = await pendingUpdate(directory);
126
+ if (update?.commandId !== commandId || update.version !== targetVersion)
127
+ return;
128
+ const contents = await readPrivateRdFile(reference.snapshot);
129
+ if (contents === null)
130
+ throw new Error('RD rollback snapshot is missing');
131
+ const saved = JSON.parse(contents);
132
+ if (saved.schemaVersion !== 1 || saved.commandId !== commandId || saved.targetVersion !== targetVersion || saved.releaseSha512 !== update.releaseSha512)
133
+ throw new Error('RD rollback snapshot belongs to another CLI update');
134
+ if (!saved.activatedSha256)
135
+ return;
136
+ const active = await readPrivateRdFile(path.join(directory, 'rd', 'state.json'), 32768);
137
+ if (active === null || rdStateDigest(active) !== saved.activatedSha256)
138
+ return;
139
+ if (saved.activatedIdentity && await stateIdentity(directory) !== saved.activatedIdentity)
140
+ return;
141
+ if (saved.previous === null)
142
+ throw new Error('RD activation has no previous state to restore');
143
+ await writePrivate(path.join(directory, 'rd', 'state.json'), saved.previous);
144
+ });
145
+ }
@@ -124,7 +124,7 @@ export function lifecyclePolicy(lifecycle) {
124
124
  export function createUpdateCoordinator(deps) {
125
125
  const policy = lifecyclePolicy(deps.serviceLifecycle);
126
126
  const updateContext = updateServiceContext(deps.dir);
127
- const npm = new NpmInstaller(updateContext);
127
+ const npm = new NpmInstaller({ ...updateContext, installationLockHeld: deps.installationLockHeld });
128
128
  // This cache also resumes the restart handshake after service supervision starts a new daemon.
129
129
  const releaseCache = new LastKnownGoodCache({
130
130
  ...updateContext, runningVersion: CLIENT_VERSION, pack: (root, destination) => npm.pack(root, destination),
@@ -180,7 +180,8 @@ export function createUpdateCoordinator(deps) {
180
180
  const log = openSync(`${deps.dir}/daemon.log`, 'a');
181
181
  let child;
182
182
  try {
183
- child = spawn(process.execPath, [deps.cliPath, ...PROVISION_SERVICE_ARGV], {
183
+ child = spawn(updateContext.nodePath, [deps.cliPath, ...PROVISION_SERVICE_ARGV], {
184
+ ...(updateContext.env ? { env: updateContext.env } : {}),
184
185
  detached: true,
185
186
  stdio: ['ignore', log, log],
186
187
  });