@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
|
@@ -1,7 +1,13 @@
|
|
|
1
|
+
import { claimDaemonIdentity } from './control/daemonIdentity.js';
|
|
2
|
+
import { openRuntimeHost } from './control/runtimeHost.js';
|
|
3
|
+
import { stoppedRuntime } from './control/protocol.js';
|
|
4
|
+
import { normalizeWorkstationServer } from './control/serverIdentity.js';
|
|
5
|
+
import { promises as fs } from 'node:fs';
|
|
6
|
+
import os from 'node:os';
|
|
1
7
|
import path from 'node:path';
|
|
2
8
|
import { fileURLToPath } from 'node:url';
|
|
3
9
|
import { AGENT_WIRE_MIN_VERSION } from './protocol/agent.js';
|
|
4
|
-
import { clientDir, loadClientConfig,
|
|
10
|
+
import { clientDir, loadClientConfig, updateServiceContext } from './config.js';
|
|
5
11
|
import { AgentSessionManager } from './agentSessions.js';
|
|
6
12
|
import { AcpAgents } from './acp/acpAgents.js';
|
|
7
13
|
import { ConnectionSupervisor } from './connection/supervisor.js';
|
|
@@ -9,10 +15,26 @@ import { ManagementChannel } from './channels/management.js';
|
|
|
9
15
|
import { ComputerChannel } from './channels/computer.js';
|
|
10
16
|
import { AgentChannel, dispatchAgentFrame as dispatchAgentChannelFrame } from './channels/agent.js';
|
|
11
17
|
import { TaskRegistry } from './update/taskRegistry.js';
|
|
18
|
+
import { RdUpkeep } from './rd/upkeep.js';
|
|
12
19
|
import { createUpdateCoordinator } from './update/runUpdate.js';
|
|
13
20
|
import { resolveCliPath } from './daemonctl.js';
|
|
14
21
|
import { SystemMetricsSampler } from './systemMetrics.js';
|
|
15
|
-
import { preflightOptionalPty } from './update/ptyPreflight.js';
|
|
22
|
+
import { cachedPtyStatus, preflightOptionalPty } from './update/ptyPreflight.js';
|
|
23
|
+
import { FilesHostClient } from './files/client.js';
|
|
24
|
+
import { ToolsClient } from './tools/client.js';
|
|
25
|
+
import { WorkstationTools } from './tools/router.js';
|
|
26
|
+
import { aggregateToolsInfo, computeFilesInfo } from './tools/info.js';
|
|
27
|
+
import { defaultDistDir, KeeperLauncher } from './keepers/kit/launcher.js';
|
|
28
|
+
import { readKeeperManifest } from './keepers/kit/bundle.js';
|
|
29
|
+
import { KEEPER_POLICY_TICK_MS, KeeperPolicy, oneShotDrain, oneShotProbe } from './keepers/policy.js';
|
|
30
|
+
import { TerminalGenerations } from './keepers/terminal/generations.js';
|
|
31
|
+
import { AcpKeeperClient } from './keepers/acp/client.js';
|
|
32
|
+
import { KeeperTransport } from './acp/processor/transport/keeper.js';
|
|
33
|
+
import { keeperGenerations } from './acp/processor/transport/generations.js';
|
|
34
|
+
import { ExecPolicy } from './exec/policy.js';
|
|
35
|
+
import { LegacyToolsKeeper } from './keepers/legacy.js';
|
|
36
|
+
import { TerminalRouter, terminalEnv, terminalShell } from './tools/terminalRouter.js';
|
|
37
|
+
import { ToolchainProbe } from './toolchain.js';
|
|
16
38
|
/**
|
|
17
39
|
* The path `runDaemon` hands `createUpdateCoordinator` as `cliPath`, used to re-provision the OS
|
|
18
40
|
* service after an update (see runUpdate.ts's `provisionService`).
|
|
@@ -106,6 +128,45 @@ export const IDLE_SWEEP_INTERVAL_MS = 10 * 60 * 1000;
|
|
|
106
128
|
* nobody awaits a timer callback, and an escaping rejection is an unhandled rejection, which Node
|
|
107
129
|
* answers by exiting.
|
|
108
130
|
*/
|
|
131
|
+
/**
|
|
132
|
+
* The daemon's start-time adapter build upkeep: legacy migration, then the certified reconcile; calls
|
|
133
|
+
* `onRosterChanged` once if either changed any agent's current build. Never rejects.
|
|
134
|
+
*/
|
|
135
|
+
export async function maintainAdapterBuilds(acp, onRosterChanged) {
|
|
136
|
+
try {
|
|
137
|
+
const migrated = await acp.migrateLegacyAdapters();
|
|
138
|
+
const moves = await acp.reconcileCertifiedBuilds();
|
|
139
|
+
if (migrated.length || moves.length)
|
|
140
|
+
await onRosterChanged();
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
console.warn(`[acp-builds] adapter build upkeep failed: ${String(error)}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/** How often held sessions are looked at for a pending switch (spec §6.3 step 7) or a lost adapter to reopen (Task 24). */
|
|
147
|
+
export const SWITCH_POLL_INTERVAL_MS = 5_000;
|
|
148
|
+
/** How often unused adapter builds are collected (spec §6.3 "GC"; the first run is one grace period after start). */
|
|
149
|
+
export const BUILD_GC_INTERVAL_MS = 10 * 60_000;
|
|
150
|
+
/**
|
|
151
|
+
* Task 24: the switch scheduler (every {@link SWITCH_POLL_INTERVAL_MS}: `agent_reopen_request` for idle sessions with a pending
|
|
152
|
+
* switch, and for keeper-hosted sessions whose adapter keeper-acp lost) and build GC (every {@link BUILD_GC_INTERVAL_MS}). Both
|
|
153
|
+
* timers are `unref`'d, and neither callback can reject. Returns the stop.
|
|
154
|
+
*/
|
|
155
|
+
export function startSwitchUpkeep(agents, acp, pollMs = SWITCH_POLL_INTERVAL_MS, gcMs = BUILD_GC_INTERVAL_MS) {
|
|
156
|
+
let polling = false;
|
|
157
|
+
const poll = setInterval(() => {
|
|
158
|
+
if (polling)
|
|
159
|
+
return;
|
|
160
|
+
polling = true;
|
|
161
|
+
void Promise.resolve().then(() => agents.pollSwitches()).catch((err) => {
|
|
162
|
+
console.error(`[agent] switch poll failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
163
|
+
}).finally(() => { polling = false; });
|
|
164
|
+
}, pollMs);
|
|
165
|
+
poll.unref();
|
|
166
|
+
const gc = setInterval(() => { void Promise.resolve().then(() => acp.collectBuilds()).catch(() => undefined); }, gcMs);
|
|
167
|
+
gc.unref();
|
|
168
|
+
return () => { clearInterval(poll); clearInterval(gc); };
|
|
169
|
+
}
|
|
109
170
|
export function startIdleSweep(agents, ttlMs = IDLE_SESSION_TTL_MS, intervalMs = IDLE_SWEEP_INTERVAL_MS) {
|
|
110
171
|
const timer = setInterval(() => {
|
|
111
172
|
void (async () => {
|
|
@@ -157,7 +218,99 @@ export function resolveServiceLifecycle(options) {
|
|
|
157
218
|
export async function buildManagementSnapshot(supervisor, tasks, updates) {
|
|
158
219
|
return { channels: supervisor.channelSnapshot(), ...tasks.snapshot(), update: await updates.snapshotAsync() };
|
|
159
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* The files host's configured roots: every workspace cwd, absolute (a relative one is taken relative to the home
|
|
223
|
+
* directory; FileTools refuses relative roots because they would resolve against whatever directory a pinned section
|
|
224
|
+
* holds).
|
|
225
|
+
*/
|
|
226
|
+
export function workspaceRoots(cfg, home = os.homedir()) {
|
|
227
|
+
return [path.join(home, '.axom', 'playground'), ...(cfg.workspaces ?? []).filter(workspace => typeof workspace?.cwd === 'string' && workspace.cwd.length > 0).map(workspace => path.resolve(home, workspace.cwd))];
|
|
228
|
+
}
|
|
229
|
+
/** The roots that exist as directories right now. A missing workspace must not keep the host (all of Files) down. */
|
|
230
|
+
export async function existingDirectories(roots) {
|
|
231
|
+
const existing = [];
|
|
232
|
+
for (const root of roots) {
|
|
233
|
+
try {
|
|
234
|
+
if ((await fs.stat(root)).isDirectory())
|
|
235
|
+
existing.push(root);
|
|
236
|
+
}
|
|
237
|
+
catch { /* left out until it exists */ }
|
|
238
|
+
}
|
|
239
|
+
return existing;
|
|
240
|
+
}
|
|
241
|
+
/** How often the daemon re-reads the workspaces config (and root existence) to restart the files host on a change. */
|
|
242
|
+
export const FILES_ROOTS_POLL_MS = 10_000;
|
|
243
|
+
/** How often the legacy tools keeper is re-probed and checked for retirement (keepers/legacy.ts). */
|
|
244
|
+
export const LEGACY_TICK_MS = 30_000;
|
|
245
|
+
/**
|
|
246
|
+
* Builds the object `ManagementChannelOptions.keepers` reports (Task 26): the policy's statuses, never a rejection (a failure is
|
|
247
|
+
* `null`, which the channel also guarantees). `countedExecs` (final review minor 7): keeper-terminal execs `activeShellTasks` also
|
|
248
|
+
* counts right now — reported as the current terminal keeper's `execCounted` metric (sampled with this heartbeat), so the
|
|
249
|
+
* Workstations page counts `activeShellTasks + execRunning − execCounted` commands, each once.
|
|
250
|
+
*/
|
|
251
|
+
export async function buildKeeperStatuses(policy, countedExecs = () => 0) {
|
|
252
|
+
let statuses;
|
|
253
|
+
try {
|
|
254
|
+
statuses = await policy.statuses();
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
const terminals = statuses.filter((status) => status.kind === 'terminal' && !status.legacy);
|
|
260
|
+
const target = terminals.find((status) => status.replacement === 'none') ?? terminals[0];
|
|
261
|
+
if (!target)
|
|
262
|
+
return statuses;
|
|
263
|
+
let counted = 0;
|
|
264
|
+
try {
|
|
265
|
+
const value = countedExecs();
|
|
266
|
+
counted = Number.isSafeInteger(value) && value > 0 ? value : 0;
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
counted = 0;
|
|
270
|
+
}
|
|
271
|
+
// A copy: the policy reuses its sample for a while.
|
|
272
|
+
return statuses.map((status) => (status === target ? { ...status, metrics: { ...status.metrics, execCounted: counted } } : status));
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Runs `poll` every `intervalMs`, skipping a tick while the previous run is still in flight (on a hung mount a stat
|
|
276
|
+
* may never return; overlapping runs would pile up behind it). The timer is unref'd. Returns the stop function.
|
|
277
|
+
*/
|
|
278
|
+
export function startExclusivePoll(poll, intervalMs) {
|
|
279
|
+
let running = false;
|
|
280
|
+
const timer = setInterval(() => {
|
|
281
|
+
if (running)
|
|
282
|
+
return;
|
|
283
|
+
running = true;
|
|
284
|
+
void poll().catch(() => { }).finally(() => { running = false; });
|
|
285
|
+
}, intervalMs);
|
|
286
|
+
timer.unref();
|
|
287
|
+
return () => clearInterval(timer);
|
|
288
|
+
}
|
|
160
289
|
export async function runDaemon(options = {}) {
|
|
290
|
+
const directory = clientDir();
|
|
291
|
+
const shutdown = new AbortController();
|
|
292
|
+
const state = { snapshot: async () => ({ ...stoppedRuntime(), connection: 'connecting' }) };
|
|
293
|
+
const { host, identity } = await claimDaemonIdentity(directory, async (identity) => {
|
|
294
|
+
const config = identity.config;
|
|
295
|
+
const host = await openRuntimeHost({ directory, binding: { server: normalizeWorkstationServer(config.server), workstationId: config.workstationId, ...(config.ownerUserId ? { ownerUserId: config.ownerUserId } : {}) },
|
|
296
|
+
mode: options.supervisedByServiceManager ? 'launchd' : 'foreground', snapshot: () => state.snapshot(), shutdown: async () => { shutdown.abort(); },
|
|
297
|
+
update: async (release, operationId) => {
|
|
298
|
+
if (!state.update)
|
|
299
|
+
return { phase: 'BUSY', commandId: operationId };
|
|
300
|
+
void state.update.handle({ commandId: operationId, release, force: false }).catch(error => console.error('[update] local request failed', error));
|
|
301
|
+
return { phase: 'REQUESTED', commandId: operationId, version: release.version };
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
return { host, identity };
|
|
305
|
+
});
|
|
306
|
+
try {
|
|
307
|
+
await runOwnedDaemon(options, shutdown, state, identity);
|
|
308
|
+
}
|
|
309
|
+
finally {
|
|
310
|
+
await host.close();
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
async function runOwnedDaemon(options, shutdown, localState, identity) {
|
|
161
314
|
// Last line of defence for the whole process. Node's default for an unhandled rejection is to
|
|
162
315
|
// *exit*, and this daemon is the only thing keeping every session on this workstation alive —
|
|
163
316
|
// dying because one async callback rejected is strictly worse than logging and carrying on.
|
|
@@ -167,10 +320,27 @@ export async function runDaemon(options = {}) {
|
|
|
167
320
|
console.error(`[daemon] unhandled rejection (ignored, daemon stays up): ${detail}`);
|
|
168
321
|
});
|
|
169
322
|
const dir = clientDir();
|
|
170
|
-
const cfg =
|
|
171
|
-
|
|
323
|
+
const cfg = identity.config;
|
|
324
|
+
let activateRdAfterReadiness;
|
|
325
|
+
if (process.platform === 'linux') {
|
|
326
|
+
const { synchronizeRd } = await import('./rd/update.js');
|
|
327
|
+
activateRdAfterReadiness = await synchronizeRd(dir, cfg.server);
|
|
328
|
+
}
|
|
329
|
+
const { NativeProvider } = await import('./desktopControl/nativeProvider.js');
|
|
330
|
+
const { DeviceSockets } = await import('./desktopControl/deviceSockets.js');
|
|
331
|
+
const { DesktopSessionSupervisor } = await import('./desktopControl/supervisor.js');
|
|
332
|
+
const { MacProvider } = await import('./desktopControl/macProvider.js');
|
|
333
|
+
const { LinuxDesktopProvider } = await import('./rd/provider.js');
|
|
334
|
+
const legacyDesktop = cfg.desktopProvider ? new NativeProvider({ ...cfg.desktopProvider, validateSession: async () => {
|
|
335
|
+
const { readRdState } = await import('./rd/state.js');
|
|
336
|
+
if (await readRdState(dir))
|
|
337
|
+
throw new Error('Remote desktop configuration changed');
|
|
338
|
+
} }) : undefined;
|
|
339
|
+
const desktopProvider = options.desktopProvider ?? (process.platform === 'linux' ? new LinuxDesktopProvider(dir, cfg.server, legacyDesktop) :
|
|
340
|
+
legacyDesktop ?? (process.platform === 'darwin' && process.arch === 'arm64' ? new MacProvider(dir, cfg) : undefined));
|
|
341
|
+
const { clientKey, serverPub } = identity;
|
|
172
342
|
// Repair the first fixed release even when it was installed by an older ignore-scripts updater,
|
|
173
|
-
// before a newly started terminal
|
|
343
|
+
// before a newly started keeper-terminal snapshots native dependency availability.
|
|
174
344
|
await preflightOptionalPty(updateServiceContext(dir));
|
|
175
345
|
const tasks = new TaskRegistry();
|
|
176
346
|
// Attaches both the server-driven update path (onUpdate below) and axom update's manual path to
|
|
@@ -194,35 +364,223 @@ export async function runDaemon(options = {}) {
|
|
|
194
364
|
// invoked*, not on what the host merely supports.
|
|
195
365
|
serviceLifecycle: resolveServiceLifecycle(options),
|
|
196
366
|
});
|
|
367
|
+
localState.update = updates;
|
|
197
368
|
// One ACP client for every external agent; the roster it reports is the ACP agent registry, keyed
|
|
198
369
|
// by registry agent id (ruling R2) — the same key `agent_open.bridge` now carries.
|
|
199
|
-
|
|
200
|
-
|
|
370
|
+
// Live sessions run in keeper-acp (spec §5.3) — the adapters outlive the daemon there — or, when it cannot be reached, in the
|
|
371
|
+
// daemon itself through the local fallback (§5.7). Short-lived connections (list, delete, verify, login) stay direct children.
|
|
372
|
+
const acpLauncher = new KeeperLauncher({ configDir: dir, workstationId: cfg.workstationId, server: cfg.server });
|
|
373
|
+
const acpTransport = new KeeperTransport(new AcpKeeperClient({ launcher: acpLauncher }));
|
|
374
|
+
// Sessions keeper-acp still runs from a previous daemon (a restart, an upgrade, a crash) are adopted where they are — across
|
|
375
|
+
// keeper-acp generations (spec §5.5, §6.3) — before the agent channel's reconnect offer is answered.
|
|
376
|
+
const acpGenerations = keeperGenerations({ launcher: acpLauncher, current: acpTransport });
|
|
377
|
+
const acp = new AcpAgents({ cacheDir: dir, serverOrigin: cfg.server, transports: { keeper: acpTransport }, keeperGenerations: acpGenerations });
|
|
378
|
+
const agents = new AgentSessionManager(acp, null, path.join(dir, 'agents'), tasks);
|
|
379
|
+
void agents.adoptSessions();
|
|
201
380
|
let bridges = await acp.roster();
|
|
381
|
+
// A scan's roster (and a pushed one) also carries the adapter builds live sessions run and how many could move now (spec §6.4,
|
|
382
|
+
// Task 25): keeper-acp's process labels joined with this daemon's sessions. The hello's roster above does not — it is not a scan.
|
|
383
|
+
const scanRoster = () => acp.roster({ sessions: (session) => agents.rosterSessionState(session) });
|
|
202
384
|
let supervisor;
|
|
203
385
|
// Plaintext WebSockets are only for an explicitly opted-in local development fixture. Production
|
|
204
386
|
// ticket handshakes must be WSS because the ticket is the data-channel key derivation material.
|
|
205
387
|
const allowInsecureWs = process.env.AXOM_WORKSTATION_ALLOW_INSECURE_WS === '1';
|
|
206
|
-
|
|
207
|
-
|
|
388
|
+
// Files: one daemon-owned host process (files/host.ts) serves file_*_request frames and files.* tool requests.
|
|
389
|
+
const home = os.homedir();
|
|
390
|
+
const playgroundCwd = path.join(home, '.axom', 'playground');
|
|
391
|
+
await fs.mkdir(playgroundCwd, { recursive: true });
|
|
392
|
+
let roots = workspaceRoots(cfg, home);
|
|
393
|
+
let hostRoots = [];
|
|
394
|
+
const filesHost = new FilesHostClient({
|
|
395
|
+
configDir: dir,
|
|
396
|
+
options: async () => { hostRoots = await existingDirectories(roots); return { home, roots: hostRoots }; },
|
|
397
|
+
onActivityChanged: () => tasks.externalActivityChanged(),
|
|
398
|
+
log: (line) => console.error(`${new Date().toISOString()} ${line}`),
|
|
399
|
+
});
|
|
400
|
+
tasks.externalActivity = () => filesHost.activity();
|
|
401
|
+
filesHost.start();
|
|
402
|
+
const stopRootsPoll = startExclusivePoll(async () => {
|
|
403
|
+
try {
|
|
404
|
+
roots = workspaceRoots(await loadClientConfig(dir), home);
|
|
405
|
+
const current = await existingDirectories(roots);
|
|
406
|
+
if (JSON.stringify(current) !== JSON.stringify(hostRoots))
|
|
407
|
+
filesHost.reconfigure();
|
|
408
|
+
}
|
|
409
|
+
catch { /* an unreadable config keeps the current host */ }
|
|
410
|
+
}, FILES_ROOTS_POLL_MS);
|
|
411
|
+
// tools.info is aggregated here and starts no keeper: its terminal fields come from keeper-terminal's hello when that keeper
|
|
412
|
+
// already runs, else from the cached node-pty check. terminal.* go through the TerminalRouter (keeper-terminal, or a legacy
|
|
413
|
+
// tools keeper an older CLI left running with live PTYs: served until they end, then stopped). browser.* keep the
|
|
414
|
+
// browser-kind ToolsClient path. Nothing here constructs a terminal-kind ToolsClient: its request() would start the legacy keeper.
|
|
415
|
+
const toolsConfig = { configDir: dir, workstationId: cfg.workstationId, server: cfg.server, roots, browserBinding: cfg.browserBinding };
|
|
416
|
+
const browserTools = new ToolsClient(toolsConfig, 'browser');
|
|
417
|
+
const ptyStatus = cachedPtyStatus();
|
|
418
|
+
// keeper-terminal generations (Task 26): new work goes to the serving generation; PTYs and execs an older generation still runs
|
|
419
|
+
// after an upgrade are routed to it by owner until the replacement policy retires it.
|
|
420
|
+
const terminalLauncher = new KeeperLauncher({ configDir: dir, workstationId: cfg.workstationId, server: cfg.server });
|
|
421
|
+
const terminalKeepers = new TerminalGenerations({ launcher: terminalLauncher });
|
|
422
|
+
const legacyKeeper = new LegacyToolsKeeper({
|
|
423
|
+
tools: toolsConfig,
|
|
424
|
+
recoverUploads: (uploads) => filesHost.recoverUploads(uploads),
|
|
425
|
+
log: (line) => console.error(`${new Date().toISOString()} ${line}`),
|
|
426
|
+
});
|
|
427
|
+
void legacyKeeper.detect();
|
|
428
|
+
const stopLegacyPoll = startExclusivePoll(() => legacyKeeper.tick(), LEGACY_TICK_MS);
|
|
429
|
+
const terminals = new TerminalRouter({
|
|
430
|
+
kits: terminalKeepers,
|
|
431
|
+
legacy: legacyKeeper,
|
|
432
|
+
launch: async (cwd) => ({ cwd: await filesHost.terminalCwd(cwd), shell: terminalShell(), env: terminalEnv() }),
|
|
433
|
+
});
|
|
434
|
+
const workstationTools = new WorkstationTools({
|
|
435
|
+
files: filesHost,
|
|
436
|
+
terminal: (method, params) => terminals.request(method, params),
|
|
437
|
+
browser: (method, params) => browserTools.request(method, params),
|
|
438
|
+
info: () => aggregateToolsInfo({
|
|
439
|
+
files: async () => filesHost.info() ?? computeFilesInfo(home, roots),
|
|
440
|
+
terminal: () => terminalKeepers.terminalStatus(ptyStatus),
|
|
441
|
+
browser: (method, params) => browserTools.request(method, params),
|
|
442
|
+
}),
|
|
443
|
+
});
|
|
444
|
+
// exec_request: keeper-terminal (durable) when available, else in this process; argv/env/deadline/shaping decided here.
|
|
445
|
+
// Execs a previous daemon left in keeper-terminal are recovered first: wire ≤ 4 ones are stopped (their channel is gone), wire v5
|
|
446
|
+
// ones are followed for the server to re-attach (exec_attach on the next computer channel).
|
|
447
|
+
// Keeper replacement (spec §8.3, Task 26): what the heartbeat reports of each keeper generation, and what happens to an older one.
|
|
448
|
+
const drainKeeper = oneShotDrain((kind, gen) => (kind === 'acp' ? acpLauncher : terminalLauncher).pathsFor(kind, gen));
|
|
449
|
+
const keeperPolicy = new KeeperPolicy({
|
|
450
|
+
dir: path.join(dir, 'keepers'),
|
|
451
|
+
probe: oneShotProbe((kind, gen) => (kind === 'acp' ? acpLauncher : terminalLauncher).pathsFor(kind, gen)),
|
|
452
|
+
terminal: {
|
|
453
|
+
currentGen: () => terminalKeepers.currentGen(),
|
|
454
|
+
live: async () => (await terminalKeepers.running()).map((generation) => generation.gen),
|
|
455
|
+
manifest: () => terminalKeepers.manifest(),
|
|
456
|
+
stop: async (gen, mode) => {
|
|
457
|
+
const result = await terminalLauncher.stop('terminal', gen, mode);
|
|
458
|
+
if (result.stopped)
|
|
459
|
+
terminalKeepers.forget(gen);
|
|
460
|
+
return result;
|
|
461
|
+
},
|
|
462
|
+
reachable: async (gen) => (await terminalKeepers.client(gen)).runningHello(),
|
|
463
|
+
drain: (gen) => drainKeeper('terminal', gen),
|
|
464
|
+
},
|
|
465
|
+
legacy: legacyKeeper,
|
|
466
|
+
acp: {
|
|
467
|
+
currentGen: () => acpGenerations.current(),
|
|
468
|
+
live: () => acpGenerations.live(),
|
|
469
|
+
manifest: () => readKeeperManifest(path.join(defaultDistDir(), 'acp')),
|
|
470
|
+
stop: (gen, mode) => acpLauncher.stop('acp', gen, mode),
|
|
471
|
+
moveOff: (gens) => agents.moveOffOlderKeeperGenerations(undefined, gens),
|
|
472
|
+
cancelMoves: async (refs) => { for (const sessionRef of refs)
|
|
473
|
+
await agents.cancelSwitch({ sessionRef }); },
|
|
474
|
+
retireIdle: () => acp.retireIdleGenerations(),
|
|
475
|
+
session: (ref) => agents.sessionView(ref),
|
|
476
|
+
},
|
|
477
|
+
agentName: (agentId) => bridges.find((bridge) => bridge.type === agentId)?.displayName || agentId,
|
|
478
|
+
legacyShell: () => terminalShell(),
|
|
479
|
+
log: (line) => console.error(`${new Date().toISOString()} ${line}`),
|
|
480
|
+
});
|
|
481
|
+
terminalKeepers.picker = (current, older) => keeperPolicy.terminalServing(current, older);
|
|
482
|
+
const stopKeeperPolicy = startExclusivePoll(() => keeperPolicy.tick(), KEEPER_POLICY_TICK_MS);
|
|
483
|
+
// Spec §3.1: keeper sockets that fell back to /tmp (a long configDir path) are touched so a /tmp cleaner never reaps them.
|
|
484
|
+
const stopSocketTouch = startExclusivePoll(async () => {
|
|
485
|
+
await acpLauncher.touchSockets('acp');
|
|
486
|
+
await terminalLauncher.touchSockets('terminal');
|
|
487
|
+
}, KEEPER_POLICY_TICK_MS);
|
|
488
|
+
void keeperPolicy.tick();
|
|
489
|
+
const execPolicy = new ExecPolicy({ generations: terminalKeepers, log: (line) => console.error(`${new Date().toISOString()} ${line}`) });
|
|
490
|
+
void execPolicy.recover();
|
|
491
|
+
const computer = new ComputerChannel({ server: cfg.server, allowInsecureWs, taskRegistry: tasks, browserBinding: cfg.browserBinding,
|
|
492
|
+
runExec: (request, onOutput, signal, hooks) => execPolicy.run(request, onOutput, signal, hooks),
|
|
493
|
+
execs: execPolicy, log: (line) => console.error(`${new Date().toISOString()} ${line}`),
|
|
494
|
+
tools: (method, params) => workstationTools.request(method, params),
|
|
495
|
+
runFrame: (request, signal) => workstationTools.runFrame(request, signal),
|
|
208
496
|
});
|
|
209
497
|
const metrics = new SystemMetricsSampler(cfg.workspaces ?? []);
|
|
210
498
|
await metrics.start();
|
|
499
|
+
const toolchain = new ToolchainProbe(() => terminalShell(), () => terminalEnv());
|
|
500
|
+
toolchain.start();
|
|
501
|
+
const desktopSupervisor = desktopProvider && !options.onDesktopCommand ? new DesktopSessionSupervisor({ provider: desktopProvider,
|
|
502
|
+
sockets: new DeviceSockets(cfg.server, process.env.AXOM_DESKTOP_ALLOW_PRIVATE_WS === '1'),
|
|
503
|
+
report: (generation, frame) => management.sendDesktopState(generation, frame),
|
|
504
|
+
refreshCapabilities: generation => management.refreshDesktopCapabilities(generation), }) : undefined;
|
|
505
|
+
let rdGeneration;
|
|
506
|
+
const rdUpkeep = process.platform === 'linux' && desktopSupervisor ? new RdUpkeep({
|
|
507
|
+
synchronize: async () => {
|
|
508
|
+
if (!rdGeneration || shutdown.signal.aborted)
|
|
509
|
+
return;
|
|
510
|
+
const { synchronizeRd } = await import('./rd/update.js');
|
|
511
|
+
return synchronizeRd(dir, cfg.server, { deferActivation: true });
|
|
512
|
+
},
|
|
513
|
+
whenIdle: operation => desktopSupervisor.whenIdle(async () => {
|
|
514
|
+
if (!tasks.canUpdate() || !rdGeneration || shutdown.signal.aborted)
|
|
515
|
+
return;
|
|
516
|
+
await operation();
|
|
517
|
+
}),
|
|
518
|
+
refresh: () => { if (rdGeneration)
|
|
519
|
+
management.refreshDesktopCapabilities(rdGeneration); },
|
|
520
|
+
log: message => console.error(`${new Date().toISOString()} ${message}`),
|
|
521
|
+
}) : undefined;
|
|
522
|
+
const stopRdUpkeep = rdUpkeep ? startExclusivePoll(() => rdUpkeep.tick(), 60_000) : () => { };
|
|
211
523
|
const management = new ManagementChannel({
|
|
212
524
|
server: cfg.server, workstationId: cfg.workstationId, workstation: cfg.workstationId, clientKey, serverPub,
|
|
213
525
|
bridges: () => bridges,
|
|
214
526
|
workspaces: cfg.workspaces,
|
|
527
|
+
playgroundCwd,
|
|
215
528
|
metrics: () => metrics.snapshot(),
|
|
529
|
+
toolchain: () => toolchain.current(),
|
|
216
530
|
allowInsecureWs,
|
|
217
|
-
onReady: (ready) =>
|
|
531
|
+
onReady: async (ready) => {
|
|
532
|
+
await updates.managementReady(ready.release, ready.generation);
|
|
533
|
+
await activateRdAfterReadiness?.();
|
|
534
|
+
if (ready.selectedWire >= 9)
|
|
535
|
+
desktopSupervisor?.managementReady(ready.generation);
|
|
536
|
+
rdGeneration = ready.selectedWire >= 9 ? ready.generation : undefined;
|
|
537
|
+
void rdUpkeep?.tick();
|
|
538
|
+
},
|
|
539
|
+
desktopProvider,
|
|
540
|
+
onDesktopCommand: options.onDesktopCommand ?? ((frame, generation) => desktopSupervisor?.dispatch(frame, generation) ?? Promise.resolve()),
|
|
541
|
+
onClosed: generation => {
|
|
542
|
+
if (rdGeneration === generation)
|
|
543
|
+
rdGeneration = undefined;
|
|
544
|
+
desktopSupervisor?.managementClosed(generation);
|
|
545
|
+
options.onDesktopManagementClosed?.(generation);
|
|
546
|
+
},
|
|
218
547
|
onUpdate: (command) => updates.handle(command),
|
|
219
548
|
onExternalAgentsScan: async () => {
|
|
220
|
-
bridges = await
|
|
549
|
+
bridges = await scanRoster();
|
|
221
550
|
return bridges;
|
|
222
551
|
},
|
|
223
552
|
snapshot: () => buildManagementSnapshot(supervisor, tasks, updates),
|
|
553
|
+
keepers: () => buildKeeperStatuses(keeperPolicy, () => tasks.countedKeeperExecs),
|
|
554
|
+
onKeeperControl: (frame) => keeperPolicy.control(frame),
|
|
224
555
|
});
|
|
556
|
+
options.onDesktopManagement?.(management);
|
|
225
557
|
const agent = new AgentChannel({ server: cfg.server, agents, bridges: () => bridges, allowInsecureWs });
|
|
558
|
+
// Spec §6.2: first take over what an older CLI left (`_npx` copies, pre-build binary dirs) so agents
|
|
559
|
+
// that worked keep working; then §6.1: a version this CLI certifies (runtime.json/catalog pin) or a
|
|
560
|
+
// changed patch set moves an installed agent's current build forward by itself ("Updated by Axom").
|
|
561
|
+
// In the background: either may copy or download, an open of an agent not done yet waits on that
|
|
562
|
+
// agent's lock, and running sessions keep the build they were spawned from either way. A changed
|
|
563
|
+
// roster reaches the server the way a scan's does; offline, the next hello carries it.
|
|
564
|
+
void maintainAdapterBuilds(acp, async () => {
|
|
565
|
+
bridges = await scanRoster();
|
|
566
|
+
management.pushRoster(bridges);
|
|
567
|
+
});
|
|
568
|
+
// A quota capability learned mid-flight (this process's first spawn of an agent) changes what the
|
|
569
|
+
// roster would say; push it the way a build change does. Coalesced: several agents flipping
|
|
570
|
+
// around one burst of opens share a single scan, and a flip during that scan queues one more.
|
|
571
|
+
let rosterPushQueued = false;
|
|
572
|
+
acp.onRosterChanged(() => {
|
|
573
|
+
if (rosterPushQueued)
|
|
574
|
+
return;
|
|
575
|
+
rosterPushQueued = true;
|
|
576
|
+
void Promise.resolve().then(async () => {
|
|
577
|
+
rosterPushQueued = false;
|
|
578
|
+
bridges = await scanRoster();
|
|
579
|
+
management.pushRoster(bridges);
|
|
580
|
+
}).catch((error) => {
|
|
581
|
+
console.warn(`[acp-builds] roster push after capability change failed: ${String(error)}`);
|
|
582
|
+
});
|
|
583
|
+
});
|
|
226
584
|
supervisor = new ConnectionSupervisor({
|
|
227
585
|
management, computer, agent,
|
|
228
586
|
// Timestamped here rather than inside the supervisor: the daemon owns this log's shape. The
|
|
@@ -231,7 +589,15 @@ export async function runDaemon(options = {}) {
|
|
|
231
589
|
log: (message) => console.log(`${new Date().toISOString()} ${message}`),
|
|
232
590
|
});
|
|
233
591
|
const stopIdleSweep = startIdleSweep(agents);
|
|
234
|
-
const
|
|
592
|
+
const stopSwitchUpkeep = startSwitchUpkeep(agents, acp);
|
|
593
|
+
localState.snapshot = async () => {
|
|
594
|
+
const channels = supervisor.channelSnapshot();
|
|
595
|
+
const active = await buildManagementSnapshot(supervisor, tasks, updates);
|
|
596
|
+
const keepers = await buildKeeperStatuses(keeperPolicy, () => tasks.countedKeeperExecs);
|
|
597
|
+
const sum = (key) => keepers?.reduce((n, k) => n + (k.metrics[key] ?? 0), 0) ?? 0;
|
|
598
|
+
return { ...stoppedRuntime(), connection: channels.management === 'CONNECTED' ? 'online' : 'reconnecting',
|
|
599
|
+
activity: keepers === null ? null : { commands: Math.max(0, active.activeShellTasks + sum('execRunning') - sum('execCounted')), terminals: sum('ptyOpen'), agents: active.activeAgentTasks + sum('sessions'), transfers: 0 } };
|
|
600
|
+
};
|
|
235
601
|
const stop = () => shutdown.abort();
|
|
236
602
|
process.once('SIGINT', stop);
|
|
237
603
|
process.once('SIGTERM', stop);
|
|
@@ -239,9 +605,24 @@ export async function runDaemon(options = {}) {
|
|
|
239
605
|
await supervisor.run(shutdown.signal);
|
|
240
606
|
}
|
|
241
607
|
finally {
|
|
608
|
+
stopRdUpkeep();
|
|
609
|
+
rdUpkeep?.stop();
|
|
610
|
+
await desktopSupervisor?.close();
|
|
242
611
|
process.removeListener('SIGINT', stop);
|
|
243
612
|
process.removeListener('SIGTERM', stop);
|
|
244
613
|
stopIdleSweep();
|
|
614
|
+
stopSwitchUpkeep();
|
|
245
615
|
metrics.stop();
|
|
616
|
+
toolchain.stop();
|
|
617
|
+
stopRootsPoll();
|
|
618
|
+
stopLegacyPoll();
|
|
619
|
+
stopKeeperPolicy();
|
|
620
|
+
stopSocketTouch();
|
|
621
|
+
terminalKeepers.disconnect(); // only the connections: keeper-terminal and its PTYs outlive the daemon
|
|
622
|
+
// keeper-hosted sessions are detached (head checkpoint, adapter untouched, no failed turn); the others checkpoint (spec §5.5.5)
|
|
623
|
+
await agents.detachAll();
|
|
624
|
+
acp.closeTransports(); // keeper-acp's adapters outlive the daemon; local-fallback adapters die with it (spec §5.7)
|
|
625
|
+
// Bounded: a graceful host shutdown, then SIGKILL (a hung IO cannot hold the daemon's exit).
|
|
626
|
+
await filesHost.stop();
|
|
246
627
|
}
|
|
247
628
|
}
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
|
+
import { BindingStore } from './control/bindingStore.js';
|
|
3
|
+
import { stopRuntime } from './control/runtimeClient.js';
|
|
4
|
+
import { LocalControlError } from './control/protocol.js';
|
|
5
|
+
import { normalizeWorkstationServer } from './control/serverIdentity.js';
|
|
2
6
|
import path from 'node:path';
|
|
3
7
|
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import { clientDir, loadClientConfig } from './config.js';
|
|
8
|
+
import { clientDir, loadClientConfig, updateServiceContext } from './config.js';
|
|
5
9
|
import { createServiceManager } from './service/installer.js';
|
|
6
10
|
import { buildServicePath } from './service/types.js';
|
|
7
11
|
import { loginShellPath } from './service/loginShell.js';
|
|
12
|
+
import { KeeperLauncher } from './keepers/kit/launcher.js';
|
|
13
|
+
import { stopKeepers } from './keepers/stop.js';
|
|
14
|
+
import { assertRuntimeRestartSafe } from './control/restartGuard.js';
|
|
8
15
|
const __filename = fileURLToPath(import.meta.url);
|
|
9
16
|
/**
|
|
10
17
|
* The package's real npm bin entry (`package.json`'s `bin.axom`), given the directory a compiled
|
|
@@ -34,24 +41,41 @@ export function serviceContext() {
|
|
|
34
41
|
throw new Error('Axomwork Workstation user service supervision is supported only on macOS and Linux.');
|
|
35
42
|
}
|
|
36
43
|
const directory = path.dirname(__filename);
|
|
37
|
-
const
|
|
44
|
+
const managed = updateServiceContext();
|
|
45
|
+
const nodeDirectory = path.dirname(managed.nodePath);
|
|
38
46
|
return {
|
|
39
47
|
platform: process.platform,
|
|
40
48
|
username: os.userInfo().username,
|
|
41
49
|
userId: process.getuid?.() ?? os.userInfo().uid,
|
|
42
50
|
homeDir: os.homedir(),
|
|
43
|
-
nodePath:
|
|
51
|
+
nodePath: managed.nodePath,
|
|
44
52
|
cliPath: CLI_PATH,
|
|
45
|
-
npmPath:
|
|
53
|
+
npmPath: managed.npmPath,
|
|
46
54
|
// dist/workstation/ is nested under lib/node_modules/@platless/axom-cli; climb to npm's prefix.
|
|
47
|
-
npmPrefix:
|
|
55
|
+
npmPrefix: managed.npmPrefix,
|
|
48
56
|
configDir: clientDir(),
|
|
49
|
-
pathEnv: buildServicePath(process.env.PATH, nodeDirectory),
|
|
57
|
+
pathEnv: buildServicePath(managed.env?.PATH ?? process.env.PATH, nodeDirectory),
|
|
58
|
+
...(managed.managed ? { npmConfigPrefix: managed.managed.prefix } : {}),
|
|
50
59
|
};
|
|
51
60
|
}
|
|
61
|
+
/** The workstation's keepers stopped (see {@link DaemonControllerDeps.stopKeepers}); an unregistered client has none. */
|
|
62
|
+
async function stopRegisteredKeepers() {
|
|
63
|
+
const configDir = clientDir();
|
|
64
|
+
const config = await loadClientConfig(configDir).catch(() => null);
|
|
65
|
+
if (!config?.workstationId || !config.server)
|
|
66
|
+
return;
|
|
67
|
+
await stopKeepers(new KeeperLauncher({ configDir, workstationId: config.workstationId, server: config.server }), {
|
|
68
|
+
log: (line) => console.error(line),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
52
71
|
const defaultDeps = {
|
|
72
|
+
identity: () => new BindingStore(clientDir()).inspect(),
|
|
73
|
+
runtime: async () => (await (await import('./control/inspect.js')).inspectWorkstation(clientDir())).runtime,
|
|
74
|
+
shutdown: generation => stopRuntime(clientDir(), generation),
|
|
75
|
+
restartSafe: generation => assertRuntimeRestartSafe(clientDir(), generation),
|
|
53
76
|
service: (context) => createServiceManager(context ?? serviceContext()),
|
|
54
77
|
config: () => loadClientConfig(clientDir()),
|
|
78
|
+
stopKeepers: stopRegisteredKeepers,
|
|
55
79
|
};
|
|
56
80
|
/**
|
|
57
81
|
* User services own the daemon lifecycle. Deliberately no PID-file fallback remains: a stale
|
|
@@ -59,6 +83,22 @@ const defaultDeps = {
|
|
|
59
83
|
*/
|
|
60
84
|
export function createDaemonController(overrides = {}) {
|
|
61
85
|
const deps = { ...defaultDeps, ...overrides };
|
|
86
|
+
async function checked(options) {
|
|
87
|
+
const runtime = await deps.runtime();
|
|
88
|
+
if (runtime.state === 'unknown')
|
|
89
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'Cannot verify the running workstation');
|
|
90
|
+
if (options) {
|
|
91
|
+
const identity = await deps.identity();
|
|
92
|
+
if (identity.revision !== options.guard.revision || runtime.generation !== options.guard.runtimeGeneration)
|
|
93
|
+
throw new LocalControlError('BINDING_CONFLICT', 'Workstation changed; check it again');
|
|
94
|
+
}
|
|
95
|
+
if (runtime.state === 'running') {
|
|
96
|
+
const config = await deps.config();
|
|
97
|
+
if (!runtime.binding || runtime.binding.server !== normalizeWorkstationServer(config.server) || runtime.binding.workstationId !== config.workstationId)
|
|
98
|
+
throw new LocalControlError('INSTANCE_OCCUPIED', 'Another workstation binding is running');
|
|
99
|
+
}
|
|
100
|
+
return runtime;
|
|
101
|
+
}
|
|
62
102
|
return {
|
|
63
103
|
async install() {
|
|
64
104
|
// The definition is written from the login shell's PATH when one can be had, unioned with
|
|
@@ -73,16 +113,65 @@ export function createDaemonController(overrides = {}) {
|
|
|
73
113
|
: base;
|
|
74
114
|
await deps.service(context).install({ interactive: true });
|
|
75
115
|
},
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
async
|
|
79
|
-
|
|
116
|
+
// The workstation is stopped by the user: what its keepers host ends too (the daemon first, so nothing relaunches them).
|
|
117
|
+
// In a `finally`: a service that is not installed (or fails to stop) must not leave the keepers running.
|
|
118
|
+
async uninstall() { try {
|
|
119
|
+
await deps.service().uninstall();
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
await deps.stopKeepers();
|
|
123
|
+
} },
|
|
124
|
+
async start(options) { if ((await checked(options)).state === 'running')
|
|
125
|
+
return; await deps.service().start(); },
|
|
126
|
+
async stop(options) { const runtime = await checked(options); try {
|
|
127
|
+
if (runtime.mode === 'foreground' && runtime.generation)
|
|
128
|
+
await deps.shutdown(runtime.generation);
|
|
129
|
+
else
|
|
130
|
+
await deps.service().stop();
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
await deps.stopKeepers();
|
|
134
|
+
} },
|
|
135
|
+
// A restart keeps the keepers: terminals, commands and agent sessions go on under the next daemon.
|
|
136
|
+
async restart(options) {
|
|
137
|
+
const runtime = await checked(options);
|
|
138
|
+
if (runtime.state === 'running' && runtime.generation)
|
|
139
|
+
await deps.restartSafe(runtime.generation);
|
|
140
|
+
if (runtime.mode === 'foreground' && runtime.generation)
|
|
141
|
+
await deps.shutdown(runtime.generation);
|
|
142
|
+
await deps.service().restart();
|
|
143
|
+
},
|
|
144
|
+
async setEnabled(enabled, options) {
|
|
145
|
+
const runtime = await checked(options);
|
|
146
|
+
const service = deps.service();
|
|
147
|
+
if (!service.setEnabled)
|
|
148
|
+
throw new LocalControlError('UNSUPPORTED_PROTOCOL', 'Service enablement is unavailable');
|
|
149
|
+
if (enabled)
|
|
150
|
+
await service.setEnabled(true);
|
|
151
|
+
else {
|
|
152
|
+
try {
|
|
153
|
+
await service.setEnabled(false);
|
|
154
|
+
if (runtime.mode === 'foreground' && runtime.generation) {
|
|
155
|
+
const current = await deps.runtime();
|
|
156
|
+
if (current.state === 'running' && current.generation === runtime.generation)
|
|
157
|
+
await deps.shutdown(runtime.generation);
|
|
158
|
+
else if (current.state !== 'stopped')
|
|
159
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'The running workstation changed during shutdown');
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
await deps.stopKeepers();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
},
|
|
80
167
|
async status() {
|
|
168
|
+
const runtime = await deps.runtime();
|
|
169
|
+
if (runtime.state === 'running' && runtime.binding)
|
|
170
|
+
return { running: true, server: runtime.binding.server, workstationId: runtime.binding.workstationId };
|
|
171
|
+
if (runtime.state === 'unknown')
|
|
172
|
+
return { running: false, uncertain: true };
|
|
81
173
|
const state = await deps.service().status();
|
|
82
|
-
|
|
83
|
-
return { running: false };
|
|
84
|
-
const config = await deps.config().catch(() => null);
|
|
85
|
-
return { running: true, server: config?.server, workstationId: config?.workstationId };
|
|
174
|
+
return state.running ? { running: true, uncertain: true } : { running: false };
|
|
86
175
|
},
|
|
87
176
|
};
|
|
88
177
|
}
|