@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,15 +1,22 @@
|
|
|
1
1
|
import { deriveSessionKey, randomBytes } from '../crypto.js';
|
|
2
2
|
import { connectEncryptedChannel } from '../connection/encryptedChannel.js';
|
|
3
3
|
import { CLIENT_PACKAGE_NAME, CLIENT_VERSION } from '../version.js';
|
|
4
|
-
import { decodeManagementFrame, encodeManagementFrame, MANAGEMENT_WIRE_DRAINING_VERSION, MANAGEMENT_WIRE_LOAD_AVERAGES_VERSION, MANAGEMENT_WIRE_METRICS_VERSION, MANAGEMENT_WIRE_MIN_VERSION, MANAGEMENT_WIRE_PRESSURE_VERSION, MANAGEMENT_WIRE_VERSION, } from '../protocol/management.js';
|
|
5
|
-
import { bridgesForWire } from '../protocol/common.js';
|
|
4
|
+
import { decodeManagementFrame, encodeManagementFrame, MANAGEMENT_WIRE_DRAINING_VERSION, MANAGEMENT_WIRE_GPU_VERSION, MANAGEMENT_WIRE_KEEPERS_VERSION, MANAGEMENT_WIRE_LOAD_AVERAGES_VERSION, MANAGEMENT_WIRE_METRICS_VERSION, MANAGEMENT_WIRE_MIN_VERSION, MANAGEMENT_WIRE_PRESSURE_VERSION, MANAGEMENT_WIRE_VERSION, ManagementFrameAdmission, } from '../protocol/management.js';
|
|
5
|
+
import { bridgesForWire, helloRoster } from '../protocol/common.js';
|
|
6
6
|
import { AGENT_WIRE_MIN_VERSION, AGENT_WIRE_VERSION } from '../protocol/agent.js';
|
|
7
7
|
import { osInfo } from '../osinfo.js';
|
|
8
8
|
import { workstationChannelUrl } from './url.js';
|
|
9
|
+
import { DesktopCapabilityReporter } from '../desktopControl/capabilities.js';
|
|
9
10
|
const protocols = {
|
|
10
11
|
MANAGEMENT: { min: MANAGEMENT_WIRE_MIN_VERSION, max: MANAGEMENT_WIRE_VERSION },
|
|
11
|
-
COMPUTER
|
|
12
|
+
// COMPUTER v5: durable exec frames (exec_started/attach/status/ack, offsets on exec_output; spec §4.3).
|
|
13
|
+
// COMPUTER v6: this client refuses credential storage to the agent file tools (files/credentials.ts); the server sends
|
|
14
|
+
// file frames only to v6 clients.
|
|
15
|
+
COMPUTER: { min: 1, max: 6 }, AGENT: { min: AGENT_WIRE_MIN_VERSION, max: AGENT_WIRE_VERSION },
|
|
12
16
|
};
|
|
17
|
+
function toolchainField(toolchain) {
|
|
18
|
+
return toolchain ? { toolchain: { python: toolchain.python, node: toolchain.node } } : {};
|
|
19
|
+
}
|
|
13
20
|
/** Exported so tests can assert the wire frame's `clientPackage` without opening a socket. */
|
|
14
21
|
export function buildHelloFrame(options, clientNonce) {
|
|
15
22
|
return {
|
|
@@ -24,7 +31,7 @@ export function buildHelloFrame(options, clientNonce) {
|
|
|
24
31
|
*/
|
|
25
32
|
function helloBridges(bridges) {
|
|
26
33
|
const roster = bridges?.();
|
|
27
|
-
return roster === undefined ? undefined : bridgesForWire(roster, 'management_hello');
|
|
34
|
+
return roster === undefined ? undefined : bridgesForWire(helloRoster(roster), 'management_hello');
|
|
28
35
|
}
|
|
29
36
|
/** Exported so the roster this answer carries can be asserted without driving a whole channel. */
|
|
30
37
|
export function buildScanResultFrame(commandId, bridges) {
|
|
@@ -57,6 +64,10 @@ export function metricsForManagementWire(metrics, serverManagementWire) {
|
|
|
57
64
|
if (!metrics)
|
|
58
65
|
return metrics;
|
|
59
66
|
const selectedWire = serverManagementWire ?? 0;
|
|
67
|
+
if (selectedWire < MANAGEMENT_WIRE_GPU_VERSION && metrics.gpus !== undefined) {
|
|
68
|
+
const { gpus: _gpus, ...withoutGpus } = metrics;
|
|
69
|
+
return metricsForManagementWire(withoutGpus, selectedWire);
|
|
70
|
+
}
|
|
60
71
|
if (selectedWire >= MANAGEMENT_WIRE_LOAD_AVERAGES_VERSION) {
|
|
61
72
|
const { cpuLoadNormalized: _cpuLoadNormalized, ...rawLoad } = metrics;
|
|
62
73
|
return rawLoad;
|
|
@@ -98,6 +109,51 @@ export function dispatchUpdateCommand(channel, command, onUpdate, serverManageme
|
|
|
98
109
|
})
|
|
99
110
|
.catch(() => { });
|
|
100
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* The most a heartbeat waits for its `keepers` sample. The channel builds the next heartbeat only after this one went out, so a sample
|
|
114
|
+
* that hangs (a wedged `ps`, a stuck file read) must never hold the frame: past this it goes out with `keepers: null`.
|
|
115
|
+
*/
|
|
116
|
+
export const KEEPERS_SAMPLE_CAP_MS = 1500;
|
|
117
|
+
/**
|
|
118
|
+
* Heartbeat `keepers` for the negotiated wire: absent below 8 (an older server does not know the field), the sampled list from 8 on,
|
|
119
|
+
* `null` when sampling failed or did not answer within `capMs`. Never rejects, never waits longer than `capMs`. Exported for tests.
|
|
120
|
+
*/
|
|
121
|
+
export async function keepersForManagementWire(sample, serverManagementWire, capMs = KEEPERS_SAMPLE_CAP_MS) {
|
|
122
|
+
if (!sample || (serverManagementWire ?? 0) < MANAGEMENT_WIRE_KEEPERS_VERSION)
|
|
123
|
+
return {};
|
|
124
|
+
let timer;
|
|
125
|
+
const capped = new Promise((resolve) => { timer = setTimeout(() => resolve(null), capMs); timer.unref?.(); });
|
|
126
|
+
try {
|
|
127
|
+
const sampled = Promise.resolve().then(sample);
|
|
128
|
+
sampled.catch(() => undefined); // a late rejection after the cap must not be unhandled
|
|
129
|
+
return { keepers: await Promise.race([sampled, capped]) };
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return { keepers: null };
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
clearTimeout(timer);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Runs one `keeper_control` and sends its result if the channel is still open. Detached from the frame loop (a replace-now stops
|
|
140
|
+
* keeper processes, which takes seconds). Exported for tests.
|
|
141
|
+
*/
|
|
142
|
+
export function dispatchKeeperControl(channel, frame, onKeeperControl) {
|
|
143
|
+
return onKeeperControl(frame)
|
|
144
|
+
.catch((error) => ({
|
|
145
|
+
t: 'keeper_control_result', commandId: frame.commandId, kind: frame.kind, action: frame.action, ok: false, affected: [],
|
|
146
|
+
error: error instanceof Error ? error.message : String(error),
|
|
147
|
+
}))
|
|
148
|
+
.then((result) => {
|
|
149
|
+
if (channel.state !== 'OPEN')
|
|
150
|
+
return;
|
|
151
|
+
try {
|
|
152
|
+
channel.send(result);
|
|
153
|
+
}
|
|
154
|
+
catch { /* the channel closed meanwhile; the server asks again with the same commandId */ }
|
|
155
|
+
});
|
|
156
|
+
}
|
|
101
157
|
/**
|
|
102
158
|
* The server nonce to derive this session's key from.
|
|
103
159
|
*
|
|
@@ -125,18 +181,65 @@ export class ManagementChannel {
|
|
|
125
181
|
options;
|
|
126
182
|
/** Undefined until this connection receives management_ready; reset for every reconnect. */
|
|
127
183
|
serverManagementWire;
|
|
184
|
+
/** The open channel of the current connection, for frames this client sends unprompted ({@link pushRoster}). */
|
|
185
|
+
channel;
|
|
186
|
+
desktopReporter;
|
|
187
|
+
connectionAttempt = 0;
|
|
188
|
+
generation;
|
|
128
189
|
constructor(options) {
|
|
129
190
|
this.options = options;
|
|
130
191
|
}
|
|
192
|
+
refreshDesktopCapabilities(generation) {
|
|
193
|
+
if (generation === undefined || generation === this.generation)
|
|
194
|
+
this.desktopReporter?.refresh();
|
|
195
|
+
}
|
|
196
|
+
/** T10 reports only on its authenticated current management socket. */
|
|
197
|
+
sendDesktopState(generation, frame) {
|
|
198
|
+
if (generation !== this.generation || (this.serverManagementWire ?? 0) < 9 || this.channel?.state !== 'OPEN')
|
|
199
|
+
return false;
|
|
200
|
+
try {
|
|
201
|
+
this.channel.send(frame);
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Tell the server about a roster that changed without a scan asking (adapter builds moved by Axom,
|
|
210
|
+
* spec §6.1): the same `external_agents_scan_result` frame a scan answers with, under a client-made
|
|
211
|
+
* command id no scan is waiting on — the server records the roster and completes nothing. `false`
|
|
212
|
+
* when there is no open connection; the next `management_hello` carries {@link ManagementChannelOptions.bridges} then.
|
|
213
|
+
*/
|
|
214
|
+
pushRoster(bridges) {
|
|
215
|
+
const channel = this.channel;
|
|
216
|
+
if (!channel || channel.state !== 'OPEN')
|
|
217
|
+
return false;
|
|
218
|
+
try {
|
|
219
|
+
channel.send(buildScanResultFrame(`roster-push-${randomBytes(8).toString('hex')}`, bridges));
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
131
226
|
async connect(signal) {
|
|
227
|
+
const attempt = ++this.connectionAttempt;
|
|
228
|
+
this.desktopReporter?.dispose();
|
|
229
|
+
this.desktopReporter = undefined;
|
|
230
|
+
this.generation = undefined;
|
|
231
|
+
this.channel = undefined;
|
|
132
232
|
this.serverManagementWire = undefined;
|
|
133
233
|
const nonce = randomBytes(16);
|
|
134
234
|
let initialHeartbeat = true;
|
|
235
|
+
const admission = new ManagementFrameAdmission(() => attempt === this.connectionAttempt && !signal?.aborted);
|
|
135
236
|
const channel = await connectEncryptedChannel({
|
|
136
237
|
url: workstationChannelUrl(this.options.server, 'management', this.options.allowInsecureWs),
|
|
137
238
|
signal,
|
|
138
239
|
encode: encodeManagementFrame,
|
|
139
240
|
decode: decodeManagementFrame,
|
|
241
|
+
decodeBytes: decodeManagementFrame,
|
|
242
|
+
admitFrame: frame => admission.admit(frame),
|
|
140
243
|
heartbeat: (seq, sentAtEpochMs) => this.heartbeat(seq, sentAtEpochMs),
|
|
141
244
|
initialFrame: () => {
|
|
142
245
|
initialHeartbeat = false;
|
|
@@ -156,10 +259,39 @@ export class ManagementChannel {
|
|
|
156
259
|
if (initialHeartbeat)
|
|
157
260
|
throw new Error('Management proof heartbeat was not sent');
|
|
158
261
|
const ready = await this.nextReady(channel);
|
|
262
|
+
if (attempt !== this.connectionAttempt || channel.state !== 'OPEN') {
|
|
263
|
+
channel.close();
|
|
264
|
+
throw new Error('Management connection superseded');
|
|
265
|
+
}
|
|
159
266
|
this.serverManagementWire = ready.protocols.MANAGEMENT;
|
|
267
|
+
this.generation = ready.generation;
|
|
268
|
+
this.channel = channel;
|
|
269
|
+
const reporter = new DesktopCapabilityReporter({ selectedWire: this.serverManagementWire, generation: ready.generation,
|
|
270
|
+
provider: this.options.desktopProvider, send: report => {
|
|
271
|
+
if (attempt === this.connectionAttempt && this.channel === channel && channel.state === 'OPEN')
|
|
272
|
+
channel.send({ t: 'workstation_capabilities', ...report });
|
|
273
|
+
} });
|
|
274
|
+
this.desktopReporter = reporter;
|
|
275
|
+
// Starts before the existing awaited update hook; heartbeat and reporting never await discovery.
|
|
276
|
+
reporter.refresh();
|
|
277
|
+
const retire = () => {
|
|
278
|
+
admission.dispose();
|
|
279
|
+
reporter.dispose();
|
|
280
|
+
try {
|
|
281
|
+
this.options.onClosed?.(ready.generation);
|
|
282
|
+
}
|
|
283
|
+
catch { /* transport retirement must complete */ }
|
|
284
|
+
if (this.channel === channel) {
|
|
285
|
+
this.channel = undefined;
|
|
286
|
+
this.generation = undefined;
|
|
287
|
+
this.desktopReporter = undefined;
|
|
288
|
+
this.serverManagementWire = undefined;
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
void channel.closed.then(retire, retire);
|
|
160
292
|
// A readiness record is evidence from this encrypted, proof-of-key management generation — it
|
|
161
293
|
// is never driven by the local browser/device-authorization flow.
|
|
162
|
-
await this.options.onReady?.({ generation: ready.generation, release: ready.release });
|
|
294
|
+
await this.options.onReady?.({ generation: ready.generation, selectedWire: ready.protocols.MANAGEMENT, release: ready.release });
|
|
163
295
|
const pending = new Map();
|
|
164
296
|
void this.routeMessages(channel, pending);
|
|
165
297
|
return {
|
|
@@ -181,9 +313,15 @@ export class ManagementChannel {
|
|
|
181
313
|
};
|
|
182
314
|
}
|
|
183
315
|
async heartbeat(seq, sentAtEpochMs) {
|
|
316
|
+
const selectedWire = this.serverManagementWire;
|
|
317
|
+
// Availability may change without restarting the CLI (Desktop sharing is process-owned).
|
|
318
|
+
// Discovery stays bounded and never delays the heartbeat or creates a capture session.
|
|
319
|
+
this.desktopReporter?.refresh();
|
|
320
|
+
const revision = this.desktopReporter?.revision;
|
|
184
321
|
const snapshot = await this.options.snapshot();
|
|
185
322
|
return {
|
|
186
323
|
t: 'heartbeat', seq, sentAtEpochMs, clientVersion: CLIENT_VERSION,
|
|
324
|
+
...(this.options.playgroundCwd ? { playgroundCwd: this.options.playgroundCwd } : {}),
|
|
187
325
|
channels: {
|
|
188
326
|
MANAGEMENT: snapshot.channels.management,
|
|
189
327
|
COMPUTER: snapshot.channels.computer,
|
|
@@ -191,10 +329,13 @@ export class ManagementChannel {
|
|
|
191
329
|
},
|
|
192
330
|
activeShellTasks: snapshot.activeShellTasks, activeAgentTasks: snapshot.activeAgentTasks,
|
|
193
331
|
protocols, os: osInfo(),
|
|
194
|
-
...(this.options.
|
|
195
|
-
|
|
332
|
+
...toolchainField(this.options.toolchain?.() ?? null),
|
|
333
|
+
...(this.options.metrics && (selectedWire ?? 0) >= MANAGEMENT_WIRE_METRICS_VERSION
|
|
334
|
+
? { metrics: metricsForManagementWire(this.options.metrics(), selectedWire) }
|
|
196
335
|
: {}),
|
|
197
|
-
...(snapshot.update ? { update: statusForManagementWire(snapshot.update,
|
|
336
|
+
...(snapshot.update ? { update: statusForManagementWire(snapshot.update, selectedWire) } : {}),
|
|
337
|
+
...await keepersForManagementWire(this.options.keepers, selectedWire, this.options.keepersCapMs),
|
|
338
|
+
...((selectedWire ?? 0) >= 9 && revision !== undefined ? { capabilitiesRevision: revision } : {}),
|
|
198
339
|
};
|
|
199
340
|
}
|
|
200
341
|
async nextReady(channel) {
|
|
@@ -206,6 +347,25 @@ export class ManagementChannel {
|
|
|
206
347
|
async routeMessages(channel, pending) {
|
|
207
348
|
try {
|
|
208
349
|
for await (const frame of channel.messages) {
|
|
350
|
+
if (channel !== this.channel)
|
|
351
|
+
break;
|
|
352
|
+
if (frame.t === 'workstation_capabilities_request') {
|
|
353
|
+
if ((this.serverManagementWire ?? 0) >= 9 && frame.generation === this.generation)
|
|
354
|
+
this.desktopReporter?.refresh();
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
if ((frame.t === 'desktop_background' || frame.t === 'desktop_lobby' || frame.t === 'desktop_prepare' || frame.t === 'desktop_cancel') && (this.serverManagementWire ?? 0) >= 9) {
|
|
358
|
+
if (frame.t !== 'desktop_cancel' && frame.deviceGeneration !== this.generation)
|
|
359
|
+
continue;
|
|
360
|
+
if (this.options.onDesktopCommand && this.generation) {
|
|
361
|
+
// Capture the authenticated generation now: later callbacks cannot borrow a reconnect.
|
|
362
|
+
try {
|
|
363
|
+
void this.options.onDesktopCommand(frame, this.generation).catch(() => undefined);
|
|
364
|
+
}
|
|
365
|
+
catch { /* bounded desktop admission failure */ }
|
|
366
|
+
}
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
209
369
|
if (frame.t === 'channel_ticket') {
|
|
210
370
|
const queue = pending.get(frame.channel);
|
|
211
371
|
queue?.shift()?.resolve(frame.ticket);
|
|
@@ -217,6 +377,10 @@ export class ManagementChannel {
|
|
|
217
377
|
void dispatchUpdateCommand(channel, frame, this.options.onUpdate, this.serverManagementWire);
|
|
218
378
|
continue;
|
|
219
379
|
}
|
|
380
|
+
if (frame.t === 'keeper_control' && this.options.onKeeperControl) {
|
|
381
|
+
void dispatchKeeperControl(channel, frame, this.options.onKeeperControl);
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
220
384
|
if (frame.t === 'external_agents_scan' && this.options.onExternalAgentsScan) {
|
|
221
385
|
channel.send(buildScanResultFrame(frame.commandId, await this.options.onExternalAgentsScan()));
|
|
222
386
|
}
|
|
@@ -11,7 +11,7 @@ function ipv4Octets(hostname) {
|
|
|
11
11
|
return octets.every((octet) => octet <= 255) ? octets : null;
|
|
12
12
|
}
|
|
13
13
|
/** Deliberately classifies literal names/IPs only: DNS answers must never downgrade transport security. */
|
|
14
|
-
function isTrustedPlaintextHost(rawHostname) {
|
|
14
|
+
export function isTrustedPlaintextHost(rawHostname) {
|
|
15
15
|
const hostname = rawHostname.toLowerCase().replace(/^\[|\]$/g, '');
|
|
16
16
|
if (TRUSTED_PLAINTEXT_NAMES.has(hostname) || hostname.endsWith('.localhost'))
|
|
17
17
|
return true;
|
|
@@ -1,20 +1,28 @@
|
|
|
1
|
+
import { loadManagedCliInstallSync, npmExecutionContext } from './managedInstall.js';
|
|
1
2
|
import { resolveBrowserBinding } from './desktop/binding.js';
|
|
2
3
|
import { promises as fs } from 'node:fs';
|
|
3
4
|
import os from 'node:os';
|
|
4
5
|
import path from 'node:path';
|
|
5
6
|
import { fileURLToPath } from 'node:url';
|
|
6
7
|
import yaml from 'js-yaml';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
const localPath = z.string().refine(p => path.isAbsolute(p) && !p.includes('\0'));
|
|
10
|
+
const nativeConfig = z.strictObject({ kind: z.literal('linux-native'), helperPath: localPath, runtimeDir: localPath });
|
|
7
11
|
export function updateServiceContext(configDir = clientDir(), overrides = {}) {
|
|
8
12
|
// config.ts lives in dist/workstation/ after packaging, so two levels up is the installed package root.
|
|
9
13
|
const packageRoot = overrides.packageRoot ?? path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
10
|
-
const
|
|
14
|
+
const managed = loadManagedCliInstallSync(packageRoot);
|
|
15
|
+
const nodePath = managed?.nodePath ?? overrides.nodePath ?? process.execPath;
|
|
16
|
+
const npmPrefix = managed?.prefix ?? overrides.npmPrefix ?? process.env.AXOM_WORKSTATION_NPM_PREFIX ?? path.resolve(packageRoot, '../../../..');
|
|
11
17
|
return {
|
|
12
18
|
configDir,
|
|
19
|
+
...(managed ? { managed } : {}),
|
|
20
|
+
env: npmExecutionContext({ prefix: npmPrefix, nodePath }, process.env),
|
|
13
21
|
platform: overrides.platform ?? (process.platform === 'darwin' ? 'darwin' : 'linux'),
|
|
14
22
|
nodePath,
|
|
15
|
-
npmPath: overrides.npmPath ?? process.env.AXOM_WORKSTATION_NPM_PATH ?? path.join(path.dirname(nodePath), 'npm'),
|
|
23
|
+
npmPath: managed?.npmPath ?? overrides.npmPath ?? process.env.AXOM_WORKSTATION_NPM_PATH ?? path.join(path.dirname(nodePath), 'npm'),
|
|
16
24
|
// packageRoot is under lib/node_modules/@platless; climb to npm's global prefix.
|
|
17
|
-
npmPrefix
|
|
25
|
+
npmPrefix,
|
|
18
26
|
packageRoot,
|
|
19
27
|
};
|
|
20
28
|
}
|
|
@@ -35,7 +43,11 @@ export async function loadClientConfig(dir) {
|
|
|
35
43
|
throw new Error('Invalid workstation config: server and workstationId are required; run axom auth again');
|
|
36
44
|
}
|
|
37
45
|
const browserBinding = parsed.browserBinding ? resolveBrowserBinding(parsed.browserBinding, {}) : undefined;
|
|
38
|
-
|
|
46
|
+
const desktopProvider = parsed.desktopProvider ? nativeConfig.parse({ ...parsed.desktopProvider,
|
|
47
|
+
helperPath: parsed.desktopProvider.helperPath === '/usr/local/bin/axom-desktop-helper' ? '/usr/local/bin/axom-desktop-keeper' : parsed.desktopProvider.helperPath,
|
|
48
|
+
}) : process.env.AXOM_DESKTOP_NATIVE_PROVIDER === '1' ?
|
|
49
|
+
nativeConfig.parse({ kind: 'linux-native', helperPath: '/usr/local/bin/axom-desktop-keeper', runtimeDir: '/run/axom-desktop' }) : undefined;
|
|
50
|
+
return { ...(browserBinding ? { browserBinding } : {}), ...(desktopProvider ? { desktopProvider } : {}), server: parsed.server, workstationId: parsed.workstationId, ...(typeof parsed.ownerUserId === 'string' && parsed.ownerUserId ? { ownerUserId: parsed.ownerUserId } : {}), ...(Array.isArray(parsed.workspaces) ? { workspaces: parsed.workspaces } : {}) };
|
|
39
51
|
}
|
|
40
52
|
export async function writeKeys(dir, clientKeyB64, serverPubB64) {
|
|
41
53
|
await fs.mkdir(dir, { recursive: true });
|
|
@@ -279,7 +279,10 @@ export function connectEncryptedChannel(options) {
|
|
|
279
279
|
if (!isBinary)
|
|
280
280
|
return;
|
|
281
281
|
try {
|
|
282
|
-
const
|
|
282
|
+
const plaintext = crypto.open(sessionKey, bytes);
|
|
283
|
+
const frame = options.decodeBytes ? options.decodeBytes(plaintext) : options.decode(plaintext.toString());
|
|
284
|
+
if (options.admitFrame && !options.admitFrame(frame))
|
|
285
|
+
return;
|
|
283
286
|
const acknowledged = [...outstandingAcks].some((sequence) => options.isHeartbeatAck(frame, sequence));
|
|
284
287
|
// A correctly encrypted and decoded frame proves the peer is still reachable. Heartbeat
|
|
285
288
|
// acknowledgements are one such frame, but computer streaming/agent events may legitimately
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { assertLockOwnership } from './locks.js';
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import { promises as fs } from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import yaml from 'js-yaml';
|
|
6
|
+
import { loadClientConfig } from '../config.js';
|
|
7
|
+
import { privateDirectory } from '../keepers/kit/paths.js';
|
|
8
|
+
import { bindingSchema, operationRecordSchema, LocalControlError } from './protocol.js';
|
|
9
|
+
import { normalizeWorkstationServer } from './serverIdentity.js';
|
|
10
|
+
const IDENTITY_FILES = ['client.key', 'server.pub', 'config.yml'];
|
|
11
|
+
const missing = (error) => error.code === 'ENOENT';
|
|
12
|
+
async function readPrivateFile(file) {
|
|
13
|
+
const stat = await fs.lstat(file);
|
|
14
|
+
if (!stat.isFile() || stat.isSymbolicLink() || (process.getuid && stat.uid !== process.getuid()) || stat.size > 1_048_576)
|
|
15
|
+
throw new Error('Unsafe workstation identity file');
|
|
16
|
+
return fs.readFile(file, 'utf8');
|
|
17
|
+
}
|
|
18
|
+
async function exists(file) {
|
|
19
|
+
try {
|
|
20
|
+
await fs.lstat(file);
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (missing(error))
|
|
25
|
+
return false;
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function atomicWrite(file, value) {
|
|
30
|
+
assertLockOwnership();
|
|
31
|
+
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
32
|
+
try {
|
|
33
|
+
const handle = await fs.open(temporary, 'wx', 0o600);
|
|
34
|
+
try {
|
|
35
|
+
await handle.writeFile(value);
|
|
36
|
+
await handle.sync();
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
await handle.close();
|
|
40
|
+
}
|
|
41
|
+
await fs.rename(temporary, file);
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
await fs.rm(temporary, { force: true });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function syncDirectory(directory) {
|
|
48
|
+
const handle = await fs.open(directory, 'r');
|
|
49
|
+
try {
|
|
50
|
+
await handle.sync();
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
await handle.close();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function validateKeys(keys) {
|
|
57
|
+
for (const value of [keys.clientKey, keys.serverPub]) {
|
|
58
|
+
if (!/^[A-Za-z0-9+/]{43}=$/.test(value) || Buffer.from(value, 'base64').length !== 32 || Buffer.from(value, 'base64').toString('base64') !== value) {
|
|
59
|
+
throw new LocalControlError('CORRUPT_IDENTITY', 'Workstation connection keys are invalid');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function bindingOf(config) {
|
|
64
|
+
return bindingSchema.parse({ server: normalizeWorkstationServer(config.server), workstationId: config.workstationId, ...(config.ownerUserId ? { ownerUserId: config.ownerUserId } : {}) });
|
|
65
|
+
}
|
|
66
|
+
/** Caller holds the shared operation lock for every mutating method. */
|
|
67
|
+
export class BindingStore {
|
|
68
|
+
directory;
|
|
69
|
+
pending;
|
|
70
|
+
constructor(directory) {
|
|
71
|
+
this.directory = directory;
|
|
72
|
+
this.pending = path.join(directory, 'identity.pending');
|
|
73
|
+
}
|
|
74
|
+
async inspect() {
|
|
75
|
+
const pending = await exists(this.pending);
|
|
76
|
+
const present = await Promise.all(IDENTITY_FILES.map(file => exists(path.join(this.directory, file))));
|
|
77
|
+
if (!present.some(Boolean))
|
|
78
|
+
return { binding: null, revision: 'unregistered', identityState: pending ? 'incomplete' : 'absent' };
|
|
79
|
+
let readableBinding = null;
|
|
80
|
+
let configDigest = 'corrupt';
|
|
81
|
+
try {
|
|
82
|
+
configDigest = createHash('sha256').update(await readPrivateFile(path.join(this.directory, 'config.yml'))).digest('hex');
|
|
83
|
+
await readPrivateFile(path.join(this.directory, 'config.yml'));
|
|
84
|
+
const binding = bindingOf(await loadClientConfig(this.directory));
|
|
85
|
+
readableBinding = binding;
|
|
86
|
+
const keys = { clientKey: (await readPrivateFile(path.join(this.directory, 'client.key'))).trim(), serverPub: (await readPrivateFile(path.join(this.directory, 'server.pub'))).trim() };
|
|
87
|
+
validateKeys(keys);
|
|
88
|
+
const revision = createHash('sha256').update(JSON.stringify(binding)).update('\0').update(keys.clientKey).update('\0').update(keys.serverPub).digest('hex');
|
|
89
|
+
return { binding, revision, identityState: pending ? 'incomplete' : 'ready' };
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return { binding: readableBinding, revision: configDigest, identityState: pending ? 'incomplete' : 'corrupt' };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async history() {
|
|
96
|
+
try {
|
|
97
|
+
const data = JSON.parse(await readPrivateFile(path.join(this.directory, 'binding-history.json')));
|
|
98
|
+
if (!Array.isArray(data))
|
|
99
|
+
throw new Error('Invalid binding history');
|
|
100
|
+
return data.map(value => bindingSchema.parse(value));
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
if (missing(error))
|
|
104
|
+
return [];
|
|
105
|
+
throw new LocalControlError('CORRUPT_IDENTITY', 'Workstation binding history is unreadable');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async recover() {
|
|
109
|
+
if (!await exists(this.pending))
|
|
110
|
+
return;
|
|
111
|
+
const stat = await fs.lstat(this.pending);
|
|
112
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || (process.getuid && stat.uid !== process.getuid()))
|
|
113
|
+
throw new LocalControlError('CORRUPT_IDENTITY', 'Unsafe pending workstation identity');
|
|
114
|
+
let marker;
|
|
115
|
+
try {
|
|
116
|
+
marker = await readPrivateFile(path.join(this.pending, 'ready'));
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'Registration needs verification: incomplete local transaction');
|
|
120
|
+
}
|
|
121
|
+
if (marker === 'clear') {
|
|
122
|
+
await this.finishClear();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (marker !== '1')
|
|
126
|
+
throw new LocalControlError('CORRUPT_IDENTITY', 'Unknown workstation identity transaction');
|
|
127
|
+
const configText = await readPrivateFile(path.join(this.pending, 'config.yml'));
|
|
128
|
+
const config = await loadClientConfig(this.pending);
|
|
129
|
+
bindingOf(config);
|
|
130
|
+
const keys = { clientKey: (await readPrivateFile(path.join(this.pending, 'client.key'))).trim(), serverPub: (await readPrivateFile(path.join(this.pending, 'server.pub'))).trim() };
|
|
131
|
+
validateKeys(keys);
|
|
132
|
+
const operation = operationRecordSchema.parse(JSON.parse(await readPrivateFile(path.join(this.pending, 'operation.json'))));
|
|
133
|
+
let history;
|
|
134
|
+
if (await exists(path.join(this.pending, 'binding-history.json'))) {
|
|
135
|
+
const staged = JSON.parse(await readPrivateFile(path.join(this.pending, 'binding-history.json')));
|
|
136
|
+
if (!Array.isArray(staged))
|
|
137
|
+
throw new LocalControlError('CORRUPT_IDENTITY', 'Invalid staged binding history');
|
|
138
|
+
history = staged.map(value => bindingSchema.parse(value));
|
|
139
|
+
}
|
|
140
|
+
// All staging is validated before any existing identity file changes.
|
|
141
|
+
for (const [name, value] of [['client.key', keys.clientKey], ['server.pub', keys.serverPub], ['config.yml', configText]])
|
|
142
|
+
await atomicWrite(path.join(this.directory, name), value);
|
|
143
|
+
if (history)
|
|
144
|
+
await atomicWrite(path.join(this.directory, 'binding-history.json'), JSON.stringify(history));
|
|
145
|
+
await atomicWrite(path.join(this.directory, 'operation.json'), JSON.stringify(operation));
|
|
146
|
+
await syncDirectory(this.directory);
|
|
147
|
+
await fs.rm(this.pending, { recursive: true });
|
|
148
|
+
await syncDirectory(this.directory);
|
|
149
|
+
}
|
|
150
|
+
async commit(config, keys, operation, repair = false) {
|
|
151
|
+
const binding = bindingOf(config);
|
|
152
|
+
validateKeys(keys);
|
|
153
|
+
const safeOperation = operationRecordSchema.parse(operation);
|
|
154
|
+
await privateDirectory(this.directory);
|
|
155
|
+
await this.recover();
|
|
156
|
+
const current = await this.inspect();
|
|
157
|
+
if (current.identityState === 'corrupt' && !(repair && current.binding && (!current.binding.ownerUserId || current.binding.ownerUserId === binding.ownerUserId) && current.binding.server === binding.server && current.binding.workstationId === binding.workstationId))
|
|
158
|
+
throw new LocalControlError('CORRUPT_IDENTITY', 'Existing workstation identity needs repair');
|
|
159
|
+
const history = await this.history();
|
|
160
|
+
for (const previous of [current.binding, binding])
|
|
161
|
+
if (previous) {
|
|
162
|
+
const i = history.findIndex(item => item.server === previous.server && item.ownerUserId === previous.ownerUserId);
|
|
163
|
+
if (i >= 0)
|
|
164
|
+
history.splice(i, 1);
|
|
165
|
+
history.push(previous);
|
|
166
|
+
}
|
|
167
|
+
await fs.mkdir(this.pending, { mode: 0o700 });
|
|
168
|
+
for (const [name, value] of Object.entries({
|
|
169
|
+
'config.yml': yaml.dump({ ...config, server: binding.server }), 'client.key': keys.clientKey, 'server.pub': keys.serverPub,
|
|
170
|
+
'operation.json': JSON.stringify(safeOperation), 'binding-history.json': JSON.stringify(history),
|
|
171
|
+
}))
|
|
172
|
+
await atomicWrite(path.join(this.pending, name), value);
|
|
173
|
+
await atomicWrite(path.join(this.pending, 'ready'), '1');
|
|
174
|
+
await syncDirectory(this.pending);
|
|
175
|
+
await syncDirectory(this.directory);
|
|
176
|
+
await this.recover();
|
|
177
|
+
}
|
|
178
|
+
async clear(expectedRevision) {
|
|
179
|
+
await this.recover();
|
|
180
|
+
const current = await this.inspect();
|
|
181
|
+
if (current.revision !== expectedRevision || current.identityState !== 'ready')
|
|
182
|
+
throw new LocalControlError('BINDING_CONFLICT', 'Workstation identity changed; check it again');
|
|
183
|
+
await fs.mkdir(this.pending, { mode: 0o700 });
|
|
184
|
+
await atomicWrite(path.join(this.pending, 'clear.json'), JSON.stringify(current.binding));
|
|
185
|
+
await atomicWrite(path.join(this.pending, 'ready'), 'clear');
|
|
186
|
+
await syncDirectory(this.pending);
|
|
187
|
+
await syncDirectory(this.directory);
|
|
188
|
+
await this.finishClear();
|
|
189
|
+
}
|
|
190
|
+
async finishClear() {
|
|
191
|
+
const removed = bindingSchema.parse(JSON.parse(await readPrivateFile(path.join(this.pending, 'clear.json'))));
|
|
192
|
+
const history = (await this.history()).filter(item => item.server !== removed.server || item.workstationId !== removed.workstationId);
|
|
193
|
+
await atomicWrite(path.join(this.directory, 'binding-history.json'), JSON.stringify(history));
|
|
194
|
+
for (const name of [...IDENTITY_FILES, 'operation.json'])
|
|
195
|
+
await fs.rm(path.join(this.directory, name), { force: true });
|
|
196
|
+
await syncDirectory(this.directory);
|
|
197
|
+
await fs.rm(this.pending, { recursive: true });
|
|
198
|
+
await syncDirectory(this.directory);
|
|
199
|
+
}
|
|
200
|
+
}
|