@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.
- package/README.md +18 -5
- package/dist/axom-patch/cli/README.md +100 -12
- package/dist/axom-patch/cli/account-limits/generate.py +15 -12
- package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +20 -3
- package/dist/axom-patch/cli/claude-account-limits.json +9 -9
- package/dist/axom-patch/cli/codex-account-limits.json +11 -11
- package/dist/axom-patch/cli/codex-terminal-output.json +16 -0
- package/dist/axom-patch/cli/commandcode-account-limits.json +9 -9
- package/dist/axom-patch/cli/kimi-quota.mjs +12 -0
- package/dist/axom-patch/cli/kimi-subagents.mjs +173 -0
- package/dist/axom-patch/cli/nightly-task.md +55 -13
- package/dist/axom-patch/cli/probes/codex-terminal-output.mjs +33 -0
- package/dist/axom-patch/cli/probes/kimi-subagent-subwork.mjs +82 -0
- package/dist/axom-patch/cli/qoder/dist/axom-acp-compat.mjs +1591 -0
- package/dist/axom-patch/cli/qoder/releases/1.1.75.json +360 -0
- package/dist/axom-patch/cli/qoder-background-terminal.md +13 -6
- package/dist/axom-patch/cli/qoder-foreground-output.md +14 -8
- package/dist/axom-patch/cli/qoder-native-acp.json +227 -0
- package/dist/axom-patch/cli/qoder-steering.md +20 -25
- package/dist/axom-patch/cli/qoder-subagent-subwork.md +17 -68
- package/dist/axom-patch/cli/runtime.json +16 -18
- package/dist/axom-patch/cli/zcode-account-limits.json +10 -11
- package/dist/axom-patch/cli/zcode-models.json +1 -2
- package/dist/axom-patch/cli/zcode-subagent-subwork.json +9 -10
- package/dist/cli.js +4 -36
- package/dist/commands/knowledge.js +47 -0
- package/dist/commands/update.js +65 -3
- package/dist/commands/workstation.js +73 -7
- package/dist/install/claude/commands.js +13 -13
- package/dist/mcp.js +1 -1
- package/dist/native/darwin-arm64/axom-quic +0 -0
- package/dist/workstation/acp/acpAgents.js +102 -98
- package/dist/workstation/acp/acpClient.js +26 -0
- package/dist/workstation/acp/builds/install/npm.js +99 -10
- package/dist/workstation/acp/initializeResponse.js +6 -1
- package/dist/workstation/acp/managedPatches.js +189 -25
- package/dist/workstation/acp/managedRuntime.js +314 -0
- package/dist/workstation/acp/patchAssets.js +40 -0
- package/dist/workstation/acp/processor/checkpoint.js +25 -34
- package/dist/workstation/acp/processor/control.js +5 -78
- package/dist/workstation/acp/processor/runtime.js +5 -81
- package/dist/workstation/acp/processor/session.js +51 -80
- package/dist/workstation/acp/processor/state.js +6 -13
- package/dist/workstation/acp/processor/step.js +3 -20
- package/dist/workstation/acp/processor/test/fixtureLog.js +9 -16
- package/dist/workstation/acp/processor/updates.js +131 -105
- package/dist/workstation/acp/processorSession.js +8 -172
- package/dist/workstation/acp/registry/catalog.js +65 -39
- package/dist/workstation/acp/registry/launch.js +16 -1
- package/dist/workstation/acp/switching.js +0 -3
- package/dist/workstation/acp/test/fakeAgent.js +13 -0
- package/dist/workstation/acp/test/keeperWorld.js +28 -0
- package/dist/workstation/acp/test/raw/sources.js +16 -130
- package/dist/workstation/acp/test/realAdapter.js +1 -47
- package/dist/workstation/acp/test/scriptAgent.js +11 -3
- package/dist/workstation/agentFlow.js +1 -1
- package/dist/workstation/agentSessions.js +38 -143
- package/dist/workstation/channels/management.js +95 -11
- package/dist/workstation/channels/url.js +1 -1
- package/dist/workstation/config.js +16 -4
- package/dist/workstation/connection/encryptedChannel.js +4 -1
- package/dist/workstation/control/bindingStore.js +200 -0
- package/dist/workstation/control/commands.js +124 -0
- package/dist/workstation/control/daemonIdentity.js +53 -0
- package/dist/workstation/control/enrollment.js +106 -0
- package/dist/workstation/control/inspect.js +71 -0
- package/dist/workstation/control/locks.js +63 -0
- package/dist/workstation/control/protocol.js +40 -0
- package/dist/workstation/control/restartGuard.js +46 -0
- package/dist/workstation/control/runtimeClient.js +97 -0
- package/dist/workstation/control/runtimeHost.js +148 -0
- package/dist/workstation/control/serverIdentity.js +35 -0
- package/dist/workstation/daemon.js +106 -6
- package/dist/workstation/daemonctl.js +77 -14
- package/dist/workstation/desktopControl/audio.js +99 -0
- package/dist/workstation/desktopControl/capabilities.js +92 -0
- package/dist/workstation/desktopControl/captureModes.js +9 -0
- package/dist/workstation/desktopControl/captureTargetLabel.js +9 -0
- package/dist/workstation/desktopControl/deviceSockets.js +246 -0
- package/dist/workstation/desktopControl/helperProcess.js +353 -0
- package/dist/workstation/desktopControl/lobby.js +129 -0
- package/dist/workstation/desktopControl/macProvider.js +155 -0
- package/dist/workstation/desktopControl/media.js +331 -0
- package/dist/workstation/desktopControl/nativeProvider.js +1018 -0
- package/dist/workstation/desktopControl/networkEvidence.js +43 -0
- package/dist/workstation/desktopControl/overview.js +58 -0
- package/dist/workstation/desktopControl/protocol.js +302 -0
- package/dist/workstation/desktopControl/quicMedia.js +240 -0
- package/dist/workstation/desktopControl/rateController.js +226 -0
- package/dist/workstation/desktopControl/supervisor.js +1681 -0
- package/dist/workstation/desktopControl/transport.js +27 -0
- package/dist/workstation/desktopControl/wallpaper.js +78 -0
- package/dist/workstation/files/credentials.js +142 -0
- package/dist/workstation/files/text.js +42 -8
- package/dist/workstation/keepers/kit/bundle.js +21 -17
- package/dist/workstation/keepers/kit/launcher.js +47 -7
- package/dist/workstation/keepers/kit/paths.js +2 -1
- package/dist/workstation/managedInstall.js +57 -0
- package/dist/workstation/protocol/agent.js +2 -15
- package/dist/workstation/protocol/management.js +93 -4
- package/dist/workstation/protocol/runtimeDiagnostics.js +15 -0
- package/dist/workstation/rd/artifact.js +119 -0
- package/dist/workstation/rd/control.js +119 -0
- package/dist/workstation/rd/display.js +29 -0
- package/dist/workstation/rd/provider.js +222 -0
- package/dist/workstation/rd/sessions.js +391 -0
- package/dist/workstation/rd/state.js +45 -0
- package/dist/workstation/rd/update.js +171 -0
- package/dist/workstation/rd/upkeep.js +38 -0
- package/dist/workstation/register.js +27 -37
- package/dist/workstation/service/installer.js +104 -13
- package/dist/workstation/service/launchd.js +1 -0
- package/dist/workstation/storageMetrics.js +118 -0
- package/dist/workstation/systemMetrics.js +44 -92
- package/dist/workstation/toolchain.js +94 -0
- package/dist/workstation/tools/browser.js +28 -4
- package/dist/workstation/update/artifact.js +2 -1
- package/dist/workstation/update/globalInstall.js +116 -0
- package/dist/workstation/update/npmInstaller.js +49 -23
- package/dist/workstation/update/rdRollback.js +145 -0
- package/dist/workstation/update/runUpdate.js +3 -2
- package/dist/workstation/update/watchdog.js +16790 -334
- package/package.json +5 -5
- package/skills/axom/SKILL.md +11 -11
- package/skills/axom-definition/SKILL.md +3 -3
- package/skills/axom-platform/SKILL.md +3 -3
- package/dist/axom-patch/cli/probes/qoder-steering.mjs +0 -89
- package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +0 -61
- package/dist/axom-patch/cli/qoder-steering.json +0 -52
- package/dist/axom-patch/cli/qoder-subagent-origin.json +0 -73
- package/dist/commands/guide.js +0 -38
- package/dist/runtime-deps/qoder-sdk/package-lock.json +0 -1189
- package/dist/runtime-deps/qoder-sdk/package.json +0 -8
- package/dist/workstation/acp/delegationMcp.js +0 -24
- package/dist/workstation/acp/processor/sideChannel.js +0 -80
- package/dist/workstation/acp/qoderStartup.js +0 -45
- package/dist/workstation/acp/test/raw/qoderCaptures.js +0 -125
- package/dist/workstation/qoderAdapter/backgroundProcess.js +0 -118
- package/dist/workstation/qoderAdapter/backgroundTerminal.js +0 -193
- package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +0 -372
- package/dist/workstation/qoderAdapter/callbackContext.js +0 -76
- package/dist/workstation/qoderAdapter/completedContinuation.js +0 -139
- package/dist/workstation/qoderAdapter/contract.js +0 -78
- package/dist/workstation/qoderAdapter/foregroundOutput.js +0 -36
- package/dist/workstation/qoderAdapter/history.js +0 -85
- package/dist/workstation/qoderAdapter/identityIndex.js +0 -191
- package/dist/workstation/qoderAdapter/inputQueue.js +0 -42
- package/dist/workstation/qoderAdapter/main.js +0 -95
- package/dist/workstation/qoderAdapter/mapper.js +0 -117
- package/dist/workstation/qoderAdapter/observerPlugin.js +0 -245
- package/dist/workstation/qoderAdapter/observerReceiver.js +0 -187
- package/dist/workstation/qoderAdapter/permissions.js +0 -61
- package/dist/workstation/qoderAdapter/profile.js +0 -93
- package/dist/workstation/qoderAdapter/sdkDriver.js +0 -48
- package/dist/workstation/qoderAdapter/sdkRuntime.js +0 -93
- package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +0 -145
- package/dist/workstation/qoderAdapter/session.js +0 -319
- package/dist/workstation/qoderAdapter/subagentMeta.js +0 -28
- package/dist/workstation/qoderAdapter/subwork.js +0 -85
- package/dist/workstation/qoderAdapter/taskControl.js +0 -38
- package/dist/workstation/qoderAdapter/taskLedger.js +0 -163
- package/dist/workstation/qoderAdapter/usage.js +0 -37
- /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
|
-
/**
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
70
|
+
await attemptGlobalInstallFallback(this.options.npmPath, this.options.npmPrefix, archive, this.options.env, this.spawn);
|
|
36
71
|
}
|
|
37
|
-
catch
|
|
38
|
-
|
|
39
|
-
return;
|
|
72
|
+
catch {
|
|
73
|
+
throw primaryError;
|
|
40
74
|
}
|
|
41
|
-
|
|
42
|
-
|
|
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(
|
|
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
|
});
|