@platless/axom-cli 0.3.13 → 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 +119 -9
- package/dist/axom-patch/cli/account-limits/README.md +27 -0
- package/dist/axom-patch/cli/account-limits/antigravity-proxy.mjs +47 -0
- package/dist/axom-patch/cli/account-limits/antigravity-read.mjs +90 -0
- package/dist/axom-patch/cli/account-limits/antigravity.mjs +46 -0
- package/dist/axom-patch/cli/account-limits/generate.py +123 -0
- package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +81 -0
- package/dist/axom-patch/cli/account-limits/normalize.mjs +151 -0
- package/dist/axom-patch/cli/claude-account-limits.json +37 -0
- package/dist/axom-patch/cli/codex-account-limits.json +37 -0
- package/dist/axom-patch/cli/codex-terminal-output.json +16 -0
- package/dist/axom-patch/cli/commandcode-account-limits.json +30 -0
- 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 +194 -0
- package/dist/axom-patch/cli/pi-subagent-subwork.json +23 -0
- package/dist/axom-patch/cli/probes/claude-late-async-terminal.mjs +5 -1
- 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/probes/pi-subagent-subwork.mjs +211 -0
- package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +257 -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 -5
- package/dist/axom-patch/cli/qoder-foreground-output.md +14 -7
- 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 +39 -15
- package/dist/axom-patch/cli/zcode-account-limits.json +30 -0
- package/dist/axom-patch/cli/zcode-models.json +37 -0
- package/dist/axom-patch/cli/zcode-subagent-subwork.json +72 -0
- package/dist/cli.js +4 -9
- 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/keepers/acp/keeper.mjs +3083 -0
- package/dist/keepers/acp/manifest.json +9 -0
- package/dist/keepers/terminal/keeper.mjs +8469 -0
- package/dist/keepers/terminal/manifest.json +9 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/conpty_console_list_agent.js +25 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/eventEmitter2.js +47 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/index.js +52 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/interfaces.js +7 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/shared/conout.js +10 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/terminal.js +190 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/types.js +7 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/unixTerminal.js +351 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/utils.js +38 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/windowsConoutConnection.js +131 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/windowsPtyAgent.js +344 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/windowsTerminal.js +209 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/worker/conoutSocketWorker.js +22 -0
- package/dist/keepers/terminal/node_modules/node-pty/package.json +66 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/pty.node +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-arm64/pty.node +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-x64/pty.node +0 -0
- package/dist/mcp.js +1 -1
- package/dist/native/darwin-arm64/axom-quic +0 -0
- package/dist/test/keeperKit.js +67 -0
- package/dist/workstation/acp/acpAgents.js +2192 -636
- package/dist/workstation/acp/acpClient.js +84 -495
- package/dist/workstation/acp/adoption.js +10 -396
- package/dist/workstation/acp/binaryInstall.js +100 -27
- package/dist/workstation/acp/builds/gc.js +245 -0
- package/dist/workstation/acp/builds/install/binary.js +28 -0
- package/dist/workstation/acp/builds/install/npm.js +324 -0
- package/dist/workstation/acp/builds/install/uv.js +254 -0
- package/dist/workstation/acp/builds/migrate.js +195 -0
- package/dist/workstation/acp/builds/model.js +128 -0
- package/dist/workstation/acp/builds/nodeGate.js +100 -0
- package/dist/workstation/acp/builds/store.js +352 -0
- package/dist/workstation/acp/commandTableProbe.js +68 -0
- package/dist/workstation/acp/configOptions.js +103 -1
- package/dist/workstation/acp/envSecrets.js +27 -0
- package/dist/workstation/acp/initializeResponse.js +290 -0
- package/dist/workstation/acp/installErrors.js +12 -2
- package/dist/workstation/acp/installProbe.js +1 -1
- package/dist/workstation/acp/managedPatches.js +239 -159
- package/dist/workstation/acp/managedRuntime.js +314 -0
- package/dist/workstation/acp/outbox.js +330 -0
- package/dist/workstation/acp/patchAssets.js +60 -3
- package/dist/workstation/acp/permission.js +42 -10
- package/dist/workstation/acp/processor/checkpoint.js +78 -0
- package/dist/workstation/acp/processor/commands.js +35 -0
- package/dist/workstation/acp/processor/control.js +282 -0
- package/dist/workstation/acp/processor/emit.js +26 -0
- package/dist/workstation/acp/processor/jsonrpc.js +172 -0
- package/dist/workstation/acp/processor/memoryLog.js +49 -0
- package/dist/workstation/acp/processor/origins.js +48 -0
- package/dist/workstation/acp/processor/output.js +67 -0
- package/dist/workstation/acp/processor/projection.js +42 -0
- package/dist/workstation/acp/processor/record.js +128 -0
- package/dist/workstation/acp/processor/runtime.js +105 -0
- package/dist/workstation/acp/processor/sdkSchemas.js +61 -0
- package/dist/workstation/acp/processor/session.js +496 -0
- package/dist/workstation/acp/processor/state.js +67 -0
- package/dist/workstation/acp/processor/step.js +442 -0
- package/dist/workstation/acp/processor/test/fixtureLog.js +58 -0
- package/dist/workstation/acp/processor/test/harness.js +120 -0
- package/dist/workstation/acp/processor/test/largeIdEcho.js +50 -0
- package/dist/workstation/acp/processor/test/rawAgent.js +70 -0
- package/dist/workstation/acp/processor/transport/generations.js +76 -0
- package/dist/workstation/acp/processor/transport/keeper.js +130 -0
- package/dist/workstation/acp/processor/transport/local.js +269 -0
- package/dist/workstation/acp/processor/transport/types.js +78 -0
- package/dist/workstation/acp/processor/updates.js +390 -0
- package/dist/workstation/acp/processorSession.js +1348 -0
- package/dist/workstation/acp/registry/catalog.js +189 -41
- package/dist/workstation/acp/registry/launch.js +76 -119
- package/dist/workstation/acp/sessionUpdateMapper.js +18 -7
- package/dist/workstation/acp/steering.js +58 -0
- package/dist/workstation/acp/switching.js +111 -0
- package/dist/workstation/acp/terminalLogin.js +16 -42
- package/dist/workstation/acp/terminalLoginSpec.js +41 -0
- package/dist/workstation/acp/test/adapterFixtures.js +256 -0
- package/dist/workstation/acp/test/daemonLife.js +106 -0
- package/dist/workstation/acp/test/fakeAgent.js +13 -0
- package/dist/workstation/acp/test/keeperWorld.js +295 -0
- package/dist/workstation/acp/test/memoryOutbox.js +55 -0
- package/dist/workstation/acp/test/raw/fixtures.js +196 -0
- package/dist/workstation/acp/test/raw/sanitize.js +344 -0
- package/dist/workstation/acp/test/raw/sources.js +135 -0
- package/dist/workstation/acp/test/realAdapter.js +331 -0
- package/dist/workstation/acp/test/scriptAgent.js +214 -0
- package/dist/workstation/acp/test/sessionFrames.js +42 -0
- package/dist/workstation/acp/timedCommand.js +4 -2
- package/dist/workstation/acp/uvxInstall.js +72 -122
- package/dist/workstation/agentFlow.js +103 -0
- package/dist/workstation/agentSessions.js +1289 -544
- package/dist/workstation/channels/agent.js +7 -5
- package/dist/workstation/channels/computer.js +69 -21
- package/dist/workstation/channels/computerExecs.js +200 -0
- package/dist/workstation/channels/management.js +172 -8
- 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 +393 -12
- package/dist/workstation/daemonctl.js +103 -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/exec/attach.js +126 -0
- package/dist/workstation/exec/command.js +40 -0
- package/dist/workstation/exec/durable.js +517 -0
- package/dist/workstation/exec/fallback.js +106 -0
- package/dist/workstation/exec/policy.js +837 -0
- package/dist/workstation/exec/shaping.js +211 -0
- package/dist/workstation/exec.js +3 -112
- package/dist/workstation/files/adapters/computerFileFrames.js +51 -0
- package/dist/workstation/files/adapters/toolRequests.js +121 -0
- package/dist/workstation/files/client.js +455 -0
- package/dist/workstation/files/credentials.js +142 -0
- package/dist/workstation/files/errors.js +28 -0
- package/dist/workstation/files/fence.js +350 -0
- package/dist/workstation/files/find/fileIndex.js +153 -0
- package/dist/workstation/files/find/fuzzy.js +155 -0
- package/dist/workstation/files/host.js +122 -0
- package/dist/workstation/files/hostProtocol.js +2 -0
- package/dist/workstation/files/media/cache.js +620 -0
- package/dist/workstation/files/media/encoding.js +308 -0
- package/dist/workstation/files/media/estimator.js +100 -0
- package/dist/workstation/files/media/media.js +325 -0
- package/dist/workstation/files/media/packedAudio.js +23 -0
- package/dist/workstation/files/media/scheduler.js +551 -0
- package/dist/workstation/files/media/types.js +5 -0
- package/dist/workstation/files/scope.js +45 -0
- package/dist/workstation/files/structured.js +284 -0
- package/dist/workstation/{fileOps.js → files/text.js} +152 -39
- package/dist/workstation/files/uploads.js +351 -0
- package/dist/workstation/journal.js +65 -236
- package/dist/workstation/keepers/acp/client.js +101 -0
- package/dist/workstation/keepers/acp/envelope.js +62 -0
- package/dist/workstation/keepers/acp/host.js +419 -0
- package/dist/workstation/keepers/acp/main.js +18 -0
- package/dist/workstation/keepers/acp/memoryStream.js +121 -0
- package/dist/workstation/keepers/acp/params.js +137 -0
- package/dist/workstation/keepers/acp/protocol.js +66 -0
- package/dist/workstation/keepers/acp/recover.js +120 -0
- package/dist/workstation/keepers/acp/session.js +584 -0
- package/dist/workstation/keepers/acp/stderr.js +154 -0
- package/dist/workstation/keepers/kit/activity.js +103 -0
- package/dist/workstation/keepers/kit/bootstrap.js +164 -0
- package/dist/workstation/keepers/kit/buildVersion.js +51 -0
- package/dist/workstation/keepers/kit/bundle.js +124 -0
- package/dist/workstation/keepers/kit/client.js +432 -0
- package/dist/workstation/keepers/kit/identity.js +21 -0
- package/dist/workstation/keepers/kit/ipc.js +112 -0
- package/dist/workstation/keepers/kit/launchConfig.js +8 -0
- package/dist/workstation/keepers/kit/launcher.js +289 -0
- package/dist/workstation/keepers/kit/lock.js +123 -0
- package/dist/workstation/keepers/kit/orphanSweep.js +86 -0
- package/dist/workstation/keepers/kit/paths.js +76 -0
- package/dist/workstation/keepers/kit/protocol.js +43 -0
- package/dist/workstation/keepers/kit/server.js +518 -0
- package/dist/workstation/keepers/kit/streamLog.js +697 -0
- package/dist/workstation/keepers/legacy.js +296 -0
- package/dist/workstation/keepers/policy.js +576 -0
- package/dist/workstation/keepers/stop.js +34 -0
- package/dist/workstation/keepers/terminal/client.js +213 -0
- package/dist/workstation/keepers/terminal/exec.js +981 -0
- package/dist/workstation/keepers/terminal/generations.js +129 -0
- package/dist/workstation/keepers/terminal/keeper.js +62 -0
- package/dist/workstation/keepers/terminal/main.js +43 -0
- package/dist/workstation/keepers/terminal/orphanSweep.js +3 -0
- package/dist/workstation/keepers/terminal/params.js +27 -0
- package/dist/workstation/keepers/terminal/protocol.js +56 -0
- package/dist/workstation/keepers/terminal/pty.js +330 -0
- package/dist/workstation/managedInstall.js +57 -0
- package/dist/workstation/protocol/accountLimits.js +47 -0
- package/dist/workstation/protocol/agent.js +50 -19
- package/dist/workstation/protocol/common.js +30 -0
- package/dist/workstation/protocol/computer.js +15 -4
- package/dist/workstation/protocol/management.js +141 -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 +154 -93
- package/dist/workstation/toolchain.js +94 -0
- package/dist/workstation/tools/browser.js +28 -4
- package/dist/workstation/tools/client.js +9 -1
- package/dist/workstation/tools/info.js +50 -0
- package/dist/workstation/tools/keeperProcess.js +15 -5
- package/dist/workstation/tools/router.js +55 -0
- package/dist/workstation/tools/terminalRouter.js +222 -0
- package/dist/workstation/tools/validation.js +37 -1
- package/dist/workstation/update/artifact.js +2 -1
- package/dist/workstation/update/coordinator.js +3 -0
- package/dist/workstation/update/globalInstall.js +116 -0
- package/dist/workstation/update/npmInstaller.js +49 -23
- package/dist/workstation/update/ptyPreflight.js +21 -0
- package/dist/workstation/update/rdRollback.js +145 -0
- package/dist/workstation/update/runUpdate.js +3 -2
- package/dist/workstation/update/taskRegistry.js +105 -11
- package/dist/workstation/update/watchdog.js +16790 -334
- package/package.json +11 -8
- 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 -49
- 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/workstation/acp/acpSession.js +0 -1475
- package/dist/workstation/acp/contract.js +0 -499
- package/dist/workstation/acp/delegationMcp.js +0 -24
- package/dist/workstation/acp/draftInterceptor.js +0 -163
- package/dist/workstation/acp/qoderStartup.js +0 -45
- package/dist/workstation/acp/sessionScope.js +0 -19
- package/dist/workstation/filePaths.js +0 -174
- package/dist/workstation/fileWorker.js +0 -81
- package/dist/workstation/fileWorkerClient.js +0 -121
- package/dist/workstation/qoderAdapter/backgroundProcess.js +0 -106
- package/dist/workstation/qoderAdapter/backgroundTerminal.js +0 -262
- package/dist/workstation/qoderAdapter/completedContinuation.js +0 -139
- package/dist/workstation/qoderAdapter/contract.js +0 -78
- package/dist/workstation/qoderAdapter/foregroundOutput.js +0 -30
- package/dist/workstation/qoderAdapter/history.js +0 -87
- package/dist/workstation/qoderAdapter/identityIndex.js +0 -191
- package/dist/workstation/qoderAdapter/inputQueue.js +0 -42
- package/dist/workstation/qoderAdapter/main.js +0 -93
- 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 -49
- package/dist/workstation/qoderAdapter/session.js +0 -319
- package/dist/workstation/qoderAdapter/subagentMeta.js +0 -28
- package/dist/workstation/qoderAdapter/subwork.js +0 -77
- package/dist/workstation/qoderAdapter/taskControl.js +0 -38
- package/dist/workstation/qoderAdapter/taskEvent.js +0 -26
- package/dist/workstation/qoderAdapter/taskLedger.js +0 -163
- package/dist/workstation/qoderAdapter/usage.js +0 -37
- package/dist/workstation/tools/core.js +0 -50
- package/dist/workstation/tools/files.js +0 -525
- package/dist/workstation/tools/terminal.js +0 -214
- /package/dist/workstation/{qoderAdapter/types.js → desktopControl/provider.js} +0 -0
|
@@ -2,8 +2,8 @@ import { execFile } from 'node:child_process';
|
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
3
|
import { readFileSync, statfsSync, statSync } from 'node:fs';
|
|
4
4
|
import os from 'node:os';
|
|
5
|
-
import path from 'node:path';
|
|
6
5
|
import { promisify } from 'node:util';
|
|
6
|
+
import { discoverStoragePools } from './storageMetrics.js';
|
|
7
7
|
const execFileAsync = promisify(execFile);
|
|
8
8
|
export const STORAGE_TOPOLOGY_REFRESH_MS = 10 * 60 * 1000;
|
|
9
9
|
function cpuTotals(cpus) {
|
|
@@ -36,45 +36,6 @@ function linuxMemoryPressure(readFile) {
|
|
|
36
36
|
function stablePoolId(key) {
|
|
37
37
|
return createHash('sha256').update(key).digest('hex').slice(0, 16);
|
|
38
38
|
}
|
|
39
|
-
async function defaultIdentifyStorage(platform, cwd, device) {
|
|
40
|
-
try {
|
|
41
|
-
if (platform === 'darwin') {
|
|
42
|
-
const mounted = await execFileAsync('/bin/df', ['-P', cwd], {
|
|
43
|
-
timeout: 1_000,
|
|
44
|
-
env: { ...process.env, LC_ALL: 'C' },
|
|
45
|
-
});
|
|
46
|
-
const mountFields = mounted.stdout.trim().split('\n').at(-1)?.trim().split(/\s+/) ?? [];
|
|
47
|
-
const mountPoint = mountFields.slice(5).join(' ') || path.parse(cwd).root;
|
|
48
|
-
const { stdout } = await execFileAsync('/usr/sbin/diskutil', ['info', mountPoint], {
|
|
49
|
-
timeout: 1_000,
|
|
50
|
-
env: { ...process.env, LC_ALL: 'C' },
|
|
51
|
-
});
|
|
52
|
-
const container = /^\s*APFS Container Reference:\s*(\S+)\s*$/m.exec(stdout)?.[1];
|
|
53
|
-
const volume = /^\s*Volume Name:\s*(.+?)\s*$/m.exec(stdout)?.[1];
|
|
54
|
-
return container
|
|
55
|
-
? { key: `apfs:${container}`, label: `APFS Container ${container}` }
|
|
56
|
-
: { key: `dev:${device}`, label: volume };
|
|
57
|
-
}
|
|
58
|
-
if (platform === 'linux') {
|
|
59
|
-
const { stdout } = await execFileAsync('findmnt', ['-n', '-P', '-o', 'FSTYPE,SOURCE,UUID,LABEL,TARGET', '--target', cwd], {
|
|
60
|
-
timeout: 2_000,
|
|
61
|
-
env: { ...process.env, LC_ALL: 'C' },
|
|
62
|
-
});
|
|
63
|
-
const fields = Object.fromEntries([...stdout.matchAll(/(\w+)="([^"]*)"/g)].map((match) => [match[1], match[2]]));
|
|
64
|
-
if (fields.FSTYPE === 'btrfs' && fields.UUID)
|
|
65
|
-
return { key: `btrfs:${fields.UUID}`, label: fields.SOURCE };
|
|
66
|
-
if (fields.FSTYPE === 'zfs' && fields.SOURCE) {
|
|
67
|
-
const pool = fields.SOURCE.split('/')[0];
|
|
68
|
-
return { key: `zfs:${pool}`, label: pool };
|
|
69
|
-
}
|
|
70
|
-
return { key: `dev:${device}`, label: fields.LABEL || fields.TARGET || fields.SOURCE };
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
catch {
|
|
74
|
-
// Resource-pool recognition is best-effort. Device identity below is the conservative fallback.
|
|
75
|
-
}
|
|
76
|
-
return { key: `dev:${device}` };
|
|
77
|
-
}
|
|
78
39
|
async function defaultDarwinAvailableMemory() {
|
|
79
40
|
try {
|
|
80
41
|
const { stdout } = await execFileAsync('/usr/bin/vm_stat', [], { timeout: 2_000 });
|
|
@@ -100,6 +61,97 @@ async function defaultDarwinMemoryPressure() {
|
|
|
100
61
|
return null;
|
|
101
62
|
}
|
|
102
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Parses `ioreg -r -d 1 -w 0 -c AGXAccelerator` output. Apple Silicon publishes live GPU
|
|
66
|
+
* utilization (`Device Utilization %`, a driver-side running average) and GPU-resident unified
|
|
67
|
+
* memory (`In use system memory`) in each accelerator's `PerformanceStatistics`, without any
|
|
68
|
+
* privileges. Intel Macs expose no `AGXAccelerator` class, so they parse to null.
|
|
69
|
+
*/
|
|
70
|
+
export function parseAppleAgxStats(output) {
|
|
71
|
+
const gpus = output.split('+-o ').slice(1).map((block, index) => {
|
|
72
|
+
const stats = /"PerformanceStatistics"\s*=\s*\{([^}]*)\}/.exec(block)?.[1];
|
|
73
|
+
if (stats === undefined)
|
|
74
|
+
return null;
|
|
75
|
+
const utilizationPercent = Number(/"Device Utilization %"\s*=\s*(\d+(?:\.\d+)?)/.exec(stats)?.[1]);
|
|
76
|
+
const memoryUsedBytes = Number(/"In use system memory"\s*=\s*(\d+)/.exec(stats)?.[1]);
|
|
77
|
+
return {
|
|
78
|
+
id: `apple:agx-${index}`,
|
|
79
|
+
name: /"model"\s*=\s*"([^"]+)"/.exec(block)?.[1] ?? 'Apple GPU',
|
|
80
|
+
utilization: Number.isFinite(utilizationPercent)
|
|
81
|
+
? Math.max(0, Math.min(1, utilizationPercent / 100))
|
|
82
|
+
: null,
|
|
83
|
+
memoryTotalBytes: null,
|
|
84
|
+
memoryUsedBytes: Number.isFinite(memoryUsedBytes) && memoryUsedBytes >= 0 ? memoryUsedBytes : null,
|
|
85
|
+
coreCount: (() => {
|
|
86
|
+
const cores = Number(/"gpu-core-count"\s*=\s*(\d+)/.exec(block)?.[1]);
|
|
87
|
+
return Number.isFinite(cores) && cores > 0 ? cores : null;
|
|
88
|
+
})(),
|
|
89
|
+
};
|
|
90
|
+
}).filter((gpu) => gpu !== null);
|
|
91
|
+
return gpus.length > 0 ? gpus : null;
|
|
92
|
+
}
|
|
93
|
+
async function appleAgxGpuProbe() {
|
|
94
|
+
try {
|
|
95
|
+
const { stdout } = await execFileAsync('/usr/sbin/ioreg', ['-r', '-d', '1', '-w', '0', '-c', 'AGXAccelerator'], {
|
|
96
|
+
timeout: 2_000,
|
|
97
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
98
|
+
});
|
|
99
|
+
return parseAppleAgxStats(stdout);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/** Parses `nvidia-smi --query-gpu=... --format=csv,noheader,nounits` output; `[N/A]` reads as null. */
|
|
106
|
+
export function parseNvidiaSmiGpus(output) {
|
|
107
|
+
const mib = (field) => {
|
|
108
|
+
if (field === undefined)
|
|
109
|
+
return null;
|
|
110
|
+
const value = Number(field);
|
|
111
|
+
return Number.isFinite(value) && value >= 0 ? Math.round(value * 1024 * 1024) : null;
|
|
112
|
+
};
|
|
113
|
+
const gpus = output.split(/\r?\n/).map((line) => {
|
|
114
|
+
const fields = line.split(',').map((field) => field.trim());
|
|
115
|
+
if (fields.length < 6)
|
|
116
|
+
return null;
|
|
117
|
+
const [, uuid, name, utilizationField, totalField, usedField] = fields;
|
|
118
|
+
if (!/^GPU-/.test(uuid) || name.length === 0)
|
|
119
|
+
return null;
|
|
120
|
+
const utilizationPercent = Number(utilizationField);
|
|
121
|
+
return {
|
|
122
|
+
id: `nvidia:${uuid}`,
|
|
123
|
+
name,
|
|
124
|
+
utilization: Number.isFinite(utilizationPercent)
|
|
125
|
+
? Math.max(0, Math.min(1, utilizationPercent / 100))
|
|
126
|
+
: null,
|
|
127
|
+
memoryTotalBytes: mib(totalField),
|
|
128
|
+
memoryUsedBytes: mib(usedField),
|
|
129
|
+
coreCount: null,
|
|
130
|
+
};
|
|
131
|
+
}).filter((gpu) => gpu !== null);
|
|
132
|
+
return gpus.length > 0 ? gpus : null;
|
|
133
|
+
}
|
|
134
|
+
async function nvidiaSmiGpuProbe() {
|
|
135
|
+
try {
|
|
136
|
+
const { stdout } = await execFileAsync('nvidia-smi', [
|
|
137
|
+
'--query-gpu=index,uuid,name,utilization.gpu,memory.total,memory.used',
|
|
138
|
+
'--format=csv,noheader,nounits',
|
|
139
|
+
], { timeout: 3_000 });
|
|
140
|
+
return parseNvidiaSmiGpus(stdout);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// Absent binary, driver hiccup, or unsupported GPU -- all read as "no reading", never as failure.
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async function defaultProbeGpus(platform) {
|
|
148
|
+
const [apple, nvidia] = await Promise.all([
|
|
149
|
+
platform === 'darwin' ? appleAgxGpuProbe() : Promise.resolve(null),
|
|
150
|
+
nvidiaSmiGpuProbe(),
|
|
151
|
+
]);
|
|
152
|
+
const gpus = [...(apple ?? []), ...(nvidia ?? [])];
|
|
153
|
+
return gpus.length > 0 ? gpus : null;
|
|
154
|
+
}
|
|
103
155
|
/**
|
|
104
156
|
* Low-overhead host sampler. CPU and memory are read only while a heartbeat is being built; storage
|
|
105
157
|
* topology is discovered out of band and capacity reads use the cached representative paths.
|
|
@@ -114,6 +166,8 @@ export class SystemMetricsSampler {
|
|
|
114
166
|
darwinMemoryRefreshInFlight = null;
|
|
115
167
|
darwinMemoryAvailable = null;
|
|
116
168
|
darwinMemoryPressure = null;
|
|
169
|
+
gpuRefreshInFlight = null;
|
|
170
|
+
gpus = null;
|
|
117
171
|
constructor(workspaces, deps = {
|
|
118
172
|
platform: process.platform,
|
|
119
173
|
cpus: os.cpus,
|
|
@@ -123,15 +177,16 @@ export class SystemMetricsSampler {
|
|
|
123
177
|
readFile: readFileSync,
|
|
124
178
|
statfs: statfsSync,
|
|
125
179
|
stat: statSync,
|
|
126
|
-
|
|
180
|
+
discoverStorage: () => discoverStoragePools(process.platform, workspaces),
|
|
127
181
|
darwinAvailableMemory: defaultDarwinAvailableMemory,
|
|
128
182
|
darwinMemoryPressure: defaultDarwinMemoryPressure,
|
|
183
|
+
probeGpus: () => defaultProbeGpus(process.platform),
|
|
129
184
|
}) {
|
|
130
185
|
this.workspaces = workspaces;
|
|
131
186
|
this.deps = deps;
|
|
132
187
|
}
|
|
133
188
|
async start() {
|
|
134
|
-
await Promise.all([this.refreshTopology(), this.refreshDarwinMemory()]);
|
|
189
|
+
await Promise.all([this.refreshTopology(), this.refreshDarwinMemory(), this.refreshGpus()]);
|
|
135
190
|
this.refreshTimer = setInterval(() => this.refreshTopologyInBackground(), STORAGE_TOPOLOGY_REFRESH_MS);
|
|
136
191
|
this.refreshTimer.unref();
|
|
137
192
|
}
|
|
@@ -143,6 +198,7 @@ export class SystemMetricsSampler {
|
|
|
143
198
|
snapshot() {
|
|
144
199
|
try {
|
|
145
200
|
this.refreshDarwinMemoryInBackground();
|
|
201
|
+
this.refreshGpusInBackground();
|
|
146
202
|
const cpus = this.deps.cpus();
|
|
147
203
|
const currentCpu = cpuTotals(cpus);
|
|
148
204
|
const previousCpu = this.previousCpu;
|
|
@@ -166,20 +222,23 @@ export class SystemMetricsSampler {
|
|
|
166
222
|
: (this.deps.platform === 'darwin' ? this.darwinMemoryPressure : null);
|
|
167
223
|
const pools = new Map();
|
|
168
224
|
for (const pool of this.storage) {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
225
|
+
for (const cwd of pool.paths) {
|
|
226
|
+
try {
|
|
227
|
+
if (String(this.deps.stat(cwd).dev) !== pool.devices[cwd])
|
|
228
|
+
continue;
|
|
229
|
+
const stats = this.deps.statfs(cwd);
|
|
230
|
+
const totalBytes = Number(stats.blocks) * Number(stats.bsize);
|
|
231
|
+
const availableBytes = Number(stats.bavail) * Number(stats.bsize);
|
|
232
|
+
pools.set(pool.key, {
|
|
233
|
+
id: stablePoolId(pool.key),
|
|
234
|
+
label: pool.label,
|
|
235
|
+
workspaces: pool.workspaces,
|
|
236
|
+
totalBytes,
|
|
237
|
+
availableBytes,
|
|
238
|
+
});
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
catch { }
|
|
183
242
|
}
|
|
184
243
|
}
|
|
185
244
|
return {
|
|
@@ -192,6 +251,7 @@ export class SystemMetricsSampler {
|
|
|
192
251
|
memoryTotalBytes,
|
|
193
252
|
memoryAvailableBytes,
|
|
194
253
|
storagePools: [...pools.values()],
|
|
254
|
+
gpus: this.gpus,
|
|
195
255
|
};
|
|
196
256
|
}
|
|
197
257
|
catch {
|
|
@@ -218,6 +278,19 @@ export class SystemMetricsSampler {
|
|
|
218
278
|
this.darwinMemoryRefreshInFlight = null;
|
|
219
279
|
});
|
|
220
280
|
}
|
|
281
|
+
refreshGpusInBackground() {
|
|
282
|
+
if (this.gpuRefreshInFlight)
|
|
283
|
+
return;
|
|
284
|
+
const refresh = this.refreshGpus();
|
|
285
|
+
this.gpuRefreshInFlight = refresh;
|
|
286
|
+
void refresh.finally(() => {
|
|
287
|
+
if (this.gpuRefreshInFlight === refresh)
|
|
288
|
+
this.gpuRefreshInFlight = null;
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
async refreshGpus() {
|
|
292
|
+
this.gpus = await this.deps.probeGpus().catch(() => null);
|
|
293
|
+
}
|
|
221
294
|
async refreshDarwinMemory() {
|
|
222
295
|
if (this.deps.platform !== 'darwin')
|
|
223
296
|
return;
|
|
@@ -229,43 +302,31 @@ export class SystemMetricsSampler {
|
|
|
229
302
|
this.darwinMemoryPressure = pressure;
|
|
230
303
|
}
|
|
231
304
|
async refreshTopology() {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
catch {
|
|
258
|
-
return null;
|
|
259
|
-
}
|
|
260
|
-
});
|
|
261
|
-
const grouped = new Map();
|
|
262
|
-
for (const entry of identified.filter((candidate) => candidate !== null)) {
|
|
263
|
-
const existing = grouped.get(entry.key);
|
|
264
|
-
if (existing)
|
|
265
|
-
existing.workspaces.push(entry.name);
|
|
266
|
-
else
|
|
267
|
-
grouped.set(entry.key, { cwd: entry.cwd, key: entry.key, label: entry.label, workspaces: [entry.name] });
|
|
305
|
+
try {
|
|
306
|
+
const pools = await this.deps.discoverStorage();
|
|
307
|
+
this.storage = pools.map((pool) => {
|
|
308
|
+
const devices = Object.fromEntries(pool.paths.flatMap((cwd) => {
|
|
309
|
+
try {
|
|
310
|
+
return [[cwd, String(this.deps.stat(cwd).dev)]];
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
return [];
|
|
314
|
+
}
|
|
315
|
+
}));
|
|
316
|
+
const poolDevices = new Set(Object.values(devices));
|
|
317
|
+
const workspaces = this.workspaces.filter(({ cwd }) => {
|
|
318
|
+
try {
|
|
319
|
+
return poolDevices.has(String(this.deps.stat(cwd).dev));
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
}).map(({ name }) => name);
|
|
325
|
+
return { ...pool, workspaces, devices };
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
return;
|
|
268
330
|
}
|
|
269
|
-
this.storage = [...grouped.values()];
|
|
270
331
|
}
|
|
271
332
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const PROBE_TIMEOUT_MS = 10_000;
|
|
4
|
+
const VERIFY_TIMEOUT_MS = 3_000;
|
|
5
|
+
/** Interpreters rarely appear or vanish; a slow refresh keeps the heartbeat itself free of any spawning. */
|
|
6
|
+
export const TOOLCHAIN_REFRESH_MS = 15 * 60_000;
|
|
7
|
+
/**
|
|
8
|
+
* Shells whose `-lc` runs a POSIX-ish command string with `command -v` (fish ≥ 3 included). Any other shell (csh, tcsh,
|
|
9
|
+
* nu, pwsh, …) is not probed: its answer is unknown, never "missing".
|
|
10
|
+
*/
|
|
11
|
+
const PROBED_SHELLS = new Set(['sh', 'bash', 'zsh', 'dash', 'ksh', 'mksh', 'fish']);
|
|
12
|
+
/** Separates the probe's answers; no leading `=` (zsh would expand a bare `=word`). */
|
|
13
|
+
const SPLIT = 'AXOM-PROBE-SPLIT';
|
|
14
|
+
const DONE = 'AXOM-PROBE-DONE';
|
|
15
|
+
const NAMES = ['python3', 'python', 'node'];
|
|
16
|
+
/**
|
|
17
|
+
* One `<shell> -lc` run of `command -v` for each interpreter, the way commands run on this workstation (exec spawns the
|
|
18
|
+
* same login shell), so a Python found only through the user's profile counts. Each answer is the last absolute-path
|
|
19
|
+
* line of its segment (a login profile may print its own lines first). Null = unknown: a shell we do not probe, a shell
|
|
20
|
+
* that could not run, or a run that did not reach its end marker.
|
|
21
|
+
*/
|
|
22
|
+
export async function probeToolchain(shell, env = process.env, options = {}) {
|
|
23
|
+
if (!PROBED_SHELLS.has(path.basename(shell)))
|
|
24
|
+
return null;
|
|
25
|
+
const script = [...NAMES.map((name) => `command -v ${name}`), `echo ${DONE}`].join(`; echo ${SPLIT}; `);
|
|
26
|
+
const stdout = await run(shell, ['-lc', script], env, PROBE_TIMEOUT_MS);
|
|
27
|
+
if (stdout === undefined)
|
|
28
|
+
return null;
|
|
29
|
+
const lines = stdout.split('\n').map((line) => line.trim());
|
|
30
|
+
const doneAt = lines.lastIndexOf(DONE);
|
|
31
|
+
if (doneAt < 0)
|
|
32
|
+
return null;
|
|
33
|
+
const segments = [[]];
|
|
34
|
+
for (const line of lines.slice(0, doneAt)) {
|
|
35
|
+
if (line === SPLIT)
|
|
36
|
+
segments.push([]);
|
|
37
|
+
else
|
|
38
|
+
segments[segments.length - 1].push(line);
|
|
39
|
+
}
|
|
40
|
+
// `command -v python3; echo SPLIT; …; echo SPLIT; echo DONE`: one segment per name, then an empty one. A login
|
|
41
|
+
// profile's own output lands in the first segment, before the answer, so each answer is its segment's last path.
|
|
42
|
+
if (segments.length !== NAMES.length + 1)
|
|
43
|
+
return null;
|
|
44
|
+
const answers = segments.slice(0, NAMES.length);
|
|
45
|
+
const found = (index) => [...answers[index]].reverse().find((line) => path.isAbsolute(line)) ?? null;
|
|
46
|
+
let python3 = found(0);
|
|
47
|
+
if (python3 === '/usr/bin/python3' && (options.platform ?? process.platform) === 'darwin') {
|
|
48
|
+
if (!(await (options.macStubRuns ?? macCommandLineToolsInstalled)()))
|
|
49
|
+
python3 = null;
|
|
50
|
+
}
|
|
51
|
+
return { python: python3 ?? found(1), node: found(2) };
|
|
52
|
+
}
|
|
53
|
+
async function macCommandLineToolsInstalled() {
|
|
54
|
+
return (await run('/usr/bin/xcode-select', ['-p'], process.env, VERIFY_TIMEOUT_MS, true)) !== undefined;
|
|
55
|
+
}
|
|
56
|
+
/** The command's stdout; undefined = it did not run, timed out, or (with [strict]) exited non-zero. */
|
|
57
|
+
function run(file, args, env, timeout, strict = false) {
|
|
58
|
+
return new Promise((resolve) => {
|
|
59
|
+
execFile(file, args, { env, timeout, windowsHide: true }, (error, stdout) => {
|
|
60
|
+
const code = error?.code;
|
|
61
|
+
if (error && (strict || typeof code !== 'number')) {
|
|
62
|
+
resolve(undefined);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
resolve(String(stdout));
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
/** Holds the latest probe; the first answer arrives shortly after start, until then the heartbeat simply omits it. */
|
|
70
|
+
export class ToolchainProbe {
|
|
71
|
+
shell;
|
|
72
|
+
env;
|
|
73
|
+
latest = null;
|
|
74
|
+
timer;
|
|
75
|
+
constructor(shell, env = () => process.env) {
|
|
76
|
+
this.shell = shell;
|
|
77
|
+
this.env = env;
|
|
78
|
+
}
|
|
79
|
+
current() { return this.latest; }
|
|
80
|
+
start() {
|
|
81
|
+
if (process.platform === 'win32' || this.timer)
|
|
82
|
+
return;
|
|
83
|
+
const refresh = async () => {
|
|
84
|
+
const found = await probeToolchain(this.shell(), this.env()).catch(() => null);
|
|
85
|
+
if (found)
|
|
86
|
+
this.latest = found;
|
|
87
|
+
};
|
|
88
|
+
void refresh();
|
|
89
|
+
this.timer = setInterval(() => { void refresh(); }, TOOLCHAIN_REFRESH_MS);
|
|
90
|
+
this.timer.unref();
|
|
91
|
+
}
|
|
92
|
+
stop() { if (this.timer)
|
|
93
|
+
clearInterval(this.timer); this.timer = undefined; }
|
|
94
|
+
}
|
|
@@ -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");
|
|
@@ -84,15 +84,23 @@ function pidAlive(pid) {
|
|
|
84
84
|
return error.code === 'EPERM';
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* A tools keeper client. The daemon only ever makes the browser kind (`browser`). The kind is required — no default — so nothing can
|
|
89
|
+
* start the retired terminal-kind tools keeper by accident (spec §9; terminals run in keeper-terminal, files in the daemon's files
|
|
90
|
+
* host); an explicit `terminal` client exists only for the tests that pin how the retired keeper's startup and tools.info behave.
|
|
91
|
+
* (`keeperPaths` keeps its terminal default: `keepers/legacy.ts` finds a legacy tools keeper an older CLI left running by it.)
|
|
92
|
+
*/
|
|
87
93
|
export class ToolsClient {
|
|
88
94
|
options;
|
|
89
95
|
kind;
|
|
90
96
|
paths;
|
|
91
97
|
starting;
|
|
92
98
|
browser;
|
|
93
|
-
constructor(options, kind
|
|
99
|
+
constructor(options, kind) {
|
|
94
100
|
this.options = options;
|
|
95
101
|
this.kind = kind;
|
|
102
|
+
if (kind !== 'terminal' && kind !== 'browser')
|
|
103
|
+
throw new Error('ToolsClient needs an explicit kind (browser)');
|
|
96
104
|
this.paths = keeperPaths(options, undefined, undefined, kind);
|
|
97
105
|
}
|
|
98
106
|
async probe(timeoutMs = 250) {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
/*
|
|
5
|
+
* `tools.info`, aggregated by the daemon. Consumers (analysis C §3.6): Workbench app.mjs (version), CoworkService
|
|
6
|
+
* (files / mediaPreviewVersion / fileFindVersion / home / userName), files.mjs (roots / downloads / terminal),
|
|
7
|
+
* terminal.mjs (home, terminal / terminalError), browser.mjs (browser* fields). Every field keeps the value and meaning
|
|
8
|
+
* the legacy tools keeper gave it (tools/core.ts at the merge-base, since deleted, + tools/client.ts), and answering it never
|
|
9
|
+
* starts any keeper:
|
|
10
|
+
* - file fields come from the files host's last `ready` (or are computed here before the first one); `files: true`
|
|
11
|
+
* is reported even while the host restarts, so a probe never mistakes the machine for an old CLI;
|
|
12
|
+
* - terminal fields come from keeper-terminal's hello capabilities when that keeper is already running (connecting to it,
|
|
13
|
+
* never starting it: keepers/terminal/client.ts `terminalStatus`), otherwise from a cached node-pty load check
|
|
14
|
+
* (update/ptyPreflight.ts);
|
|
15
|
+
* - browser fields come from the browser-kind ToolsClient, whose `tools.info` only returns metadata.
|
|
16
|
+
*/
|
|
17
|
+
/** The legacy keeper always sent this (tools/core.ts at the merge-base); the browser metadata never removed it. */
|
|
18
|
+
export const BROWSER_DISABLED_ERROR = 'Browser tools are not enabled in this release';
|
|
19
|
+
export async function aggregateToolsInfo(sources) {
|
|
20
|
+
const [files, browser, terminal] = await Promise.all([sources.files(), sources.browser('tools.info', {}), sources.terminal()]);
|
|
21
|
+
return {
|
|
22
|
+
version: 1,
|
|
23
|
+
terminal: terminal.terminal,
|
|
24
|
+
browser: false,
|
|
25
|
+
...files,
|
|
26
|
+
...(terminal.terminalError ? { terminalError: terminal.terminalError } : {}),
|
|
27
|
+
browserError: BROWSER_DISABLED_ERROR,
|
|
28
|
+
...browser,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The file fields before any files host was ready: the same canonical home and roots the host computes
|
|
33
|
+
* (scope.ts `configuredRoots`), except that a root which cannot be resolved is left out instead of failing the probe.
|
|
34
|
+
*/
|
|
35
|
+
export async function computeFilesInfo(home, roots) {
|
|
36
|
+
const canonicalHome = await fs.realpath(home).catch(() => home);
|
|
37
|
+
const canonical = new Set([canonicalHome]);
|
|
38
|
+
for (const root of roots) {
|
|
39
|
+
try {
|
|
40
|
+
const real = await fs.realpath(root);
|
|
41
|
+
if ((await fs.stat(real)).isDirectory())
|
|
42
|
+
canonical.add(real);
|
|
43
|
+
}
|
|
44
|
+
catch { /* skipped, as the daemon skips it when starting the host */ }
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
files: true, readOnlyFilesVersion: 1, filesystemFilesVersion: 1, mediaPreviewVersion: 2, fileFindVersion: 1,
|
|
48
|
+
home: canonicalHome, userName: os.userInfo().username, roots: [...canonical], downloads: path.join(canonicalHome, 'Downloads'),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -2,18 +2,18 @@ import net from 'node:net';
|
|
|
2
2
|
import { promises as fs } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import { ToolsCore } from './core.js';
|
|
6
5
|
import { BrowserCore } from './browser.js';
|
|
7
6
|
import os from 'node:os';
|
|
8
7
|
import { privateDirectory } from './client.js';
|
|
9
8
|
import { object, TOOL_REQUEST_BYTES, TOOL_RESULT_BYTES } from './validation.js';
|
|
10
9
|
export async function runKeeper(config) {
|
|
10
|
+
// Only the browser keeper runs from here (spec §9): keeper-terminal owns PTYs and the files host owns files.
|
|
11
|
+
if (config.kind !== 'browser')
|
|
12
|
+
throw new Error('Only the browser keeper runs from this entry');
|
|
11
13
|
await privateDirectory(path.dirname(config.socket));
|
|
12
14
|
const pidFile = path.join(path.dirname(config.socket), 'keeper.pid.json');
|
|
13
15
|
await fs.writeFile(pidFile, JSON.stringify({ pid: process.pid, identity: config.identity }), { mode: 0o600 });
|
|
14
|
-
const core =
|
|
15
|
-
if (config.kind !== 'browser')
|
|
16
|
-
process.chdir(path.dirname(config.socket));
|
|
16
|
+
const core = await BrowserCore.create({ home: config.home ?? os.homedir(), profileRoot: config.profileRoot, executablePath: config.executablePath, proxy: config.proxy });
|
|
17
17
|
const sockets = new Set();
|
|
18
18
|
let pending = 0;
|
|
19
19
|
const server = net.createServer({ allowHalfOpen: true }, socket => {
|
|
@@ -104,7 +104,17 @@ export async function runKeeper(config) {
|
|
|
104
104
|
} };
|
|
105
105
|
}
|
|
106
106
|
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
107
|
-
|
|
107
|
+
let config;
|
|
108
|
+
try {
|
|
109
|
+
config = JSON.parse(Buffer.from(process.argv[2] ?? '', 'base64').toString('utf8'));
|
|
110
|
+
}
|
|
111
|
+
catch { /* below */ }
|
|
112
|
+
// Anti-resurrection (spec §9): only the browser keeper still starts from this entry. A terminal-kind config (kind 'terminal',
|
|
113
|
+
// or no kind from older CLIs) comes from a legacy tools keeper unit restarting (`Restart=on-failure`) or an old launcher:
|
|
114
|
+
// keeper-terminal owns PTYs now and the files host owns files, so exit cleanly (0, which systemd does not restart) before
|
|
115
|
+
// touching the socket, the pid manifest or any state.
|
|
116
|
+
if (config?.kind !== 'browser')
|
|
117
|
+
process.exit(0);
|
|
108
118
|
const keeper = await runKeeper(config);
|
|
109
119
|
let stopping = false;
|
|
110
120
|
const stop = () => { if (stopping)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { isFilesToolMethod } from '../files/adapters/toolRequests.js';
|
|
2
|
+
import { keys, validateRequest } from './validation.js';
|
|
3
|
+
export function routeTool(method) {
|
|
4
|
+
if (method === 'tools.info')
|
|
5
|
+
return 'info';
|
|
6
|
+
if (method === 'browser.sharedText')
|
|
7
|
+
return 'shared-text';
|
|
8
|
+
if (isFilesToolMethod(method))
|
|
9
|
+
return 'files';
|
|
10
|
+
if (method.startsWith('terminal.'))
|
|
11
|
+
return 'terminal';
|
|
12
|
+
if (method.startsWith('browser.'))
|
|
13
|
+
return 'browser';
|
|
14
|
+
return 'unsupported';
|
|
15
|
+
}
|
|
16
|
+
/** A probe is answered even while the workstation drains and is not work a restart would interrupt. */
|
|
17
|
+
export function isProbeTool(method) { return routeTool(method) === 'info'; }
|
|
18
|
+
/**
|
|
19
|
+
* Answered while draining and not counted as a shell task: probes, and `terminal.*` (spec §4.1: a PTY lives in its keeper, not in
|
|
20
|
+
* the daemon, so a restart does not interrupt it; counting every poll made the activity flicker).
|
|
21
|
+
*/
|
|
22
|
+
export function isUnaccountedTool(method) {
|
|
23
|
+
const route = routeTool(method);
|
|
24
|
+
return route === 'info' || route === 'terminal';
|
|
25
|
+
}
|
|
26
|
+
export class WorkstationTools {
|
|
27
|
+
deps;
|
|
28
|
+
constructor(deps) {
|
|
29
|
+
this.deps = deps;
|
|
30
|
+
}
|
|
31
|
+
/** One `tool_request` (not `browser.sharedText`). Rejects with the `tool_result.error` text. */
|
|
32
|
+
async request(method, params) {
|
|
33
|
+
// Validated at the daemon boundary as well as by whoever runs it.
|
|
34
|
+
validateRequest(method, params);
|
|
35
|
+
switch (routeTool(method)) {
|
|
36
|
+
case 'info':
|
|
37
|
+
keys(params, []);
|
|
38
|
+
return this.deps.info();
|
|
39
|
+
case 'files': return this.deps.files.tool(method, params);
|
|
40
|
+
case 'terminal':
|
|
41
|
+
if (!this.deps.terminal)
|
|
42
|
+
throw new Error('Workstation terminal keeper is not configured');
|
|
43
|
+
return this.deps.terminal(method, params);
|
|
44
|
+
case 'browser':
|
|
45
|
+
if (!this.deps.browser)
|
|
46
|
+
throw new Error('Workstation browser keeper is not configured');
|
|
47
|
+
return this.deps.browser(method, params);
|
|
48
|
+
default: throw new Error('Unsupported workstation tool');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** One `file_*_request`; never rejects (errors are result frames). */
|
|
52
|
+
runFrame(request, signal) {
|
|
53
|
+
return this.deps.files.runFrame(request, signal);
|
|
54
|
+
}
|
|
55
|
+
}
|