@platless/axom-cli 0.1.0 → 0.5.9
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 +1 -1
- package/dist/authScopes.js +19 -0
- package/dist/axom-patch/cli/README.md +132 -0
- 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 +120 -0
- package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +64 -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/commandcode-account-limits.json +30 -0
- package/dist/axom-patch/cli/nightly-task.md +152 -0
- package/dist/axom-patch/cli/pi-steering.json +37 -0
- package/dist/axom-patch/cli/pi-subagent-subwork.json +23 -0
- package/dist/axom-patch/cli/probes/claude-late-async-terminal.mjs +38 -0
- package/dist/axom-patch/cli/probes/pi-steering.mjs +80 -0
- package/dist/axom-patch/cli/probes/pi-subagent-subwork.mjs +211 -0
- package/dist/axom-patch/cli/probes/qoder-steering.mjs +89 -0
- package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +61 -0
- package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +257 -0
- package/dist/axom-patch/cli/qoder-background-terminal.md +82 -0
- package/dist/axom-patch/cli/qoder-foreground-output.md +42 -0
- package/dist/axom-patch/cli/qoder-steering.json +52 -0
- package/dist/axom-patch/cli/qoder-steering.md +30 -0
- package/dist/axom-patch/cli/qoder-subagent-origin.json +73 -0
- package/dist/axom-patch/cli/qoder-subagent-subwork.md +73 -0
- package/dist/axom-patch/cli/runtime.json +63 -0
- package/dist/axom-patch/cli/steering/claude-steering-js.patch +89 -0
- package/dist/axom-patch/cli/steering/claude-steering-types.patch +28 -0
- package/dist/axom-patch/cli/steering/codex-steering.patch +57 -0
- package/dist/axom-patch/cli/steering/pi-steering.js +79 -0
- package/dist/axom-patch/cli/zcode-account-limits.json +31 -0
- package/dist/axom-patch/cli/zcode-models.json +38 -0
- package/dist/axom-patch/cli/zcode-subagent-subwork.json +73 -0
- package/dist/cli.js +36 -1
- package/dist/commands/auth.js +4 -13
- package/dist/commands/execute.js +4 -4
- package/dist/commands/record.js +6 -6
- package/dist/commands/struct.js +4 -4
- package/dist/commands/workstation.js +2 -2
- package/dist/credentials.js +2 -2
- 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 +32 -14
- package/dist/runtime-deps/qoder-sdk/package-lock.json +1189 -0
- package/dist/runtime-deps/qoder-sdk/package.json +8 -0
- package/dist/test/keeperKit.js +67 -0
- package/dist/workstation/acp/acpAgents.js +2514 -681
- package/dist/workstation/acp/acpClient.js +95 -318
- package/dist/workstation/acp/activityBusy.js +43 -0
- package/dist/workstation/acp/activityReporting.js +54 -0
- package/dist/workstation/acp/adoption.js +28 -356
- package/dist/workstation/acp/authUrlSniffer.js +36 -0
- 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 +235 -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/delegationMcp.js +24 -0
- package/dist/workstation/acp/envSecrets.js +27 -0
- package/dist/workstation/acp/initializeResponse.js +285 -0
- package/dist/workstation/acp/installErrors.js +12 -2
- package/dist/workstation/acp/installProbe.js +1 -1
- package/dist/workstation/acp/managedPatches.js +144 -0
- package/dist/workstation/acp/outbox.js +330 -0
- package/dist/workstation/acp/patchAssets.js +80 -0
- package/dist/workstation/acp/permission.js +335 -37
- package/dist/workstation/acp/processor/checkpoint.js +87 -0
- package/dist/workstation/acp/processor/commands.js +35 -0
- package/dist/workstation/acp/processor/control.js +355 -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 +181 -0
- package/dist/workstation/acp/processor/sdkSchemas.js +61 -0
- package/dist/workstation/acp/processor/session.js +525 -0
- package/dist/workstation/acp/processor/sideChannel.js +80 -0
- package/dist/workstation/acp/processor/state.js +74 -0
- package/dist/workstation/acp/processor/step.js +459 -0
- package/dist/workstation/acp/processor/test/fixtureLog.js +65 -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 +364 -0
- package/dist/workstation/acp/processorSession.js +1512 -0
- package/dist/workstation/acp/qoderStartup.js +45 -0
- package/dist/workstation/acp/registry/catalog.js +385 -4
- package/dist/workstation/acp/registry/detect.js +55 -0
- package/dist/workstation/acp/registry/launch.js +61 -119
- package/dist/workstation/acp/sessionUpdateMapper.js +56 -27
- package/dist/workstation/acp/steering.js +58 -0
- package/dist/workstation/acp/switching.js +114 -0
- package/dist/workstation/acp/terminalLogin.js +171 -0
- 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 +185 -9
- package/dist/workstation/acp/test/keeperWorld.js +267 -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/qoderCaptures.js +125 -0
- package/dist/workstation/acp/test/raw/sanitize.js +344 -0
- package/dist/workstation/acp/test/raw/sources.js +249 -0
- package/dist/workstation/acp/test/realAdapter.js +377 -0
- package/dist/workstation/acp/test/scriptAgent.js +206 -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/acp/validate.js +33 -5
- package/dist/workstation/agentFlow.js +103 -0
- package/dist/workstation/agentSessions.js +1686 -348
- package/dist/workstation/channels/agent.js +14 -6
- package/dist/workstation/channels/computer.js +203 -7
- package/dist/workstation/channels/computerExecs.js +200 -0
- package/dist/workstation/channels/management.js +86 -6
- package/dist/workstation/config.js +3 -1
- package/dist/workstation/connection/encryptedChannel.js +88 -21
- package/dist/workstation/daemon.js +309 -6
- package/dist/workstation/daemonctl.js +28 -2
- package/dist/workstation/desktop/binding.js +36 -0
- package/dist/workstation/desktop/bridge.js +212 -0
- package/dist/workstation/desktop/flow-control.js +52 -0
- package/dist/workstation/desktop/focused-text.js +130 -0
- package/dist/workstation/desktop/protocol.js +35 -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/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/files/text.js +356 -0
- 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 +120 -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 +249 -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 +75 -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/oauth/deviceAuthorization.js +21 -12
- package/dist/workstation/protocol/accountLimits.js +47 -0
- package/dist/workstation/protocol/agent.js +201 -8
- package/dist/workstation/protocol/common.js +36 -0
- package/dist/workstation/protocol/computer.js +107 -7
- package/dist/workstation/protocol/management.js +50 -2
- package/dist/workstation/qoderAdapter/backgroundProcess.js +118 -0
- package/dist/workstation/qoderAdapter/backgroundTerminal.js +193 -0
- package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +372 -0
- package/dist/workstation/qoderAdapter/callbackContext.js +76 -0
- package/dist/workstation/qoderAdapter/completedContinuation.js +139 -0
- package/dist/workstation/qoderAdapter/contract.js +78 -0
- package/dist/workstation/qoderAdapter/foregroundOutput.js +36 -0
- package/dist/workstation/qoderAdapter/history.js +85 -0
- package/dist/workstation/qoderAdapter/identityIndex.js +191 -0
- package/dist/workstation/qoderAdapter/inputQueue.js +42 -0
- package/dist/workstation/qoderAdapter/main.js +95 -0
- package/dist/workstation/qoderAdapter/mapper.js +117 -0
- package/dist/workstation/qoderAdapter/observerPlugin.js +245 -0
- package/dist/workstation/qoderAdapter/observerReceiver.js +187 -0
- package/dist/workstation/qoderAdapter/permissions.js +61 -0
- package/dist/workstation/qoderAdapter/profile.js +93 -0
- package/dist/workstation/qoderAdapter/sdkDriver.js +48 -0
- package/dist/workstation/qoderAdapter/sdkRuntime.js +93 -0
- package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +145 -0
- package/dist/workstation/qoderAdapter/session.js +319 -0
- package/dist/workstation/qoderAdapter/subagentMeta.js +28 -0
- package/dist/workstation/qoderAdapter/subwork.js +85 -0
- package/dist/workstation/qoderAdapter/taskControl.js +38 -0
- package/dist/workstation/qoderAdapter/taskLedger.js +163 -0
- package/dist/workstation/qoderAdapter/types.js +1 -0
- package/dist/workstation/qoderAdapter/usage.js +37 -0
- package/dist/workstation/register.js +4 -0
- package/dist/workstation/systemMetrics.js +110 -1
- package/dist/workstation/tools/browser.js +394 -0
- package/dist/workstation/tools/client.js +277 -0
- package/dist/workstation/tools/info.js +50 -0
- package/dist/workstation/tools/keeperProcess.js +124 -0
- package/dist/workstation/tools/router.js +55 -0
- package/dist/workstation/tools/terminalRouter.js +222 -0
- package/dist/workstation/tools/validation.js +86 -0
- package/dist/workstation/update/coordinator.js +32 -0
- package/dist/workstation/update/npmInstaller.js +2 -0
- package/dist/workstation/update/ptyPreflight.js +45 -0
- package/dist/workstation/update/taskRegistry.js +141 -23
- package/dist/workstation/update/watchdog.js +66 -0
- package/package.json +20 -7
- package/skills/axom/SKILL.md +55 -17
- package/skills/axom-definition/SKILL.md +32 -32
- package/skills/axom-platform/SKILL.md +39 -39
- package/dist/workstation/acp/acpSession.js +0 -1047
- package/dist/workstation/acp/contract.js +0 -494
- package/dist/workstation/acp/draftInterceptor.js +0 -154
|
@@ -1,14 +1,15 @@
|
|
|
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, } 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
9
|
const protocols = {
|
|
10
10
|
MANAGEMENT: { min: MANAGEMENT_WIRE_MIN_VERSION, max: MANAGEMENT_WIRE_VERSION },
|
|
11
|
-
COMPUTER
|
|
11
|
+
// COMPUTER v5: durable exec frames (exec_started/attach/status/ack, offsets on exec_output; spec §4.3).
|
|
12
|
+
COMPUTER: { min: 1, max: 5 }, AGENT: { min: AGENT_WIRE_MIN_VERSION, max: AGENT_WIRE_VERSION },
|
|
12
13
|
};
|
|
13
14
|
/** Exported so tests can assert the wire frame's `clientPackage` without opening a socket. */
|
|
14
15
|
export function buildHelloFrame(options, clientNonce) {
|
|
@@ -24,7 +25,7 @@ export function buildHelloFrame(options, clientNonce) {
|
|
|
24
25
|
*/
|
|
25
26
|
function helloBridges(bridges) {
|
|
26
27
|
const roster = bridges?.();
|
|
27
|
-
return roster === undefined ? undefined : bridgesForWire(roster, 'management_hello');
|
|
28
|
+
return roster === undefined ? undefined : bridgesForWire(helloRoster(roster), 'management_hello');
|
|
28
29
|
}
|
|
29
30
|
/** Exported so the roster this answer carries can be asserted without driving a whole channel. */
|
|
30
31
|
export function buildScanResultFrame(commandId, bridges) {
|
|
@@ -57,6 +58,10 @@ export function metricsForManagementWire(metrics, serverManagementWire) {
|
|
|
57
58
|
if (!metrics)
|
|
58
59
|
return metrics;
|
|
59
60
|
const selectedWire = serverManagementWire ?? 0;
|
|
61
|
+
if (selectedWire < MANAGEMENT_WIRE_GPU_VERSION && metrics.gpus !== undefined) {
|
|
62
|
+
const { gpus: _gpus, ...withoutGpus } = metrics;
|
|
63
|
+
return metricsForManagementWire(withoutGpus, selectedWire);
|
|
64
|
+
}
|
|
60
65
|
if (selectedWire >= MANAGEMENT_WIRE_LOAD_AVERAGES_VERSION) {
|
|
61
66
|
const { cpuLoadNormalized: _cpuLoadNormalized, ...rawLoad } = metrics;
|
|
62
67
|
return rawLoad;
|
|
@@ -98,6 +103,51 @@ export function dispatchUpdateCommand(channel, command, onUpdate, serverManageme
|
|
|
98
103
|
})
|
|
99
104
|
.catch(() => { });
|
|
100
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* The most a heartbeat waits for its `keepers` sample. The channel builds the next heartbeat only after this one went out, so a sample
|
|
108
|
+
* that hangs (a wedged `ps`, a stuck file read) must never hold the frame: past this it goes out with `keepers: null`.
|
|
109
|
+
*/
|
|
110
|
+
export const KEEPERS_SAMPLE_CAP_MS = 1500;
|
|
111
|
+
/**
|
|
112
|
+
* Heartbeat `keepers` for the negotiated wire: absent below 8 (an older server does not know the field), the sampled list from 8 on,
|
|
113
|
+
* `null` when sampling failed or did not answer within `capMs`. Never rejects, never waits longer than `capMs`. Exported for tests.
|
|
114
|
+
*/
|
|
115
|
+
export async function keepersForManagementWire(sample, serverManagementWire, capMs = KEEPERS_SAMPLE_CAP_MS) {
|
|
116
|
+
if (!sample || (serverManagementWire ?? 0) < MANAGEMENT_WIRE_KEEPERS_VERSION)
|
|
117
|
+
return {};
|
|
118
|
+
let timer;
|
|
119
|
+
const capped = new Promise((resolve) => { timer = setTimeout(() => resolve(null), capMs); timer.unref?.(); });
|
|
120
|
+
try {
|
|
121
|
+
const sampled = Promise.resolve().then(sample);
|
|
122
|
+
sampled.catch(() => undefined); // a late rejection after the cap must not be unhandled
|
|
123
|
+
return { keepers: await Promise.race([sampled, capped]) };
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return { keepers: null };
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
clearTimeout(timer);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Runs one `keeper_control` and sends its result if the channel is still open. Detached from the frame loop (a replace-now stops
|
|
134
|
+
* keeper processes, which takes seconds). Exported for tests.
|
|
135
|
+
*/
|
|
136
|
+
export function dispatchKeeperControl(channel, frame, onKeeperControl) {
|
|
137
|
+
return onKeeperControl(frame)
|
|
138
|
+
.catch((error) => ({
|
|
139
|
+
t: 'keeper_control_result', commandId: frame.commandId, kind: frame.kind, action: frame.action, ok: false, affected: [],
|
|
140
|
+
error: error instanceof Error ? error.message : String(error),
|
|
141
|
+
}))
|
|
142
|
+
.then((result) => {
|
|
143
|
+
if (channel.state !== 'OPEN')
|
|
144
|
+
return;
|
|
145
|
+
try {
|
|
146
|
+
channel.send(result);
|
|
147
|
+
}
|
|
148
|
+
catch { /* the channel closed meanwhile; the server asks again with the same commandId */ }
|
|
149
|
+
});
|
|
150
|
+
}
|
|
101
151
|
/**
|
|
102
152
|
* The server nonce to derive this session's key from.
|
|
103
153
|
*
|
|
@@ -125,9 +175,29 @@ export class ManagementChannel {
|
|
|
125
175
|
options;
|
|
126
176
|
/** Undefined until this connection receives management_ready; reset for every reconnect. */
|
|
127
177
|
serverManagementWire;
|
|
178
|
+
/** The open channel of the current connection, for frames this client sends unprompted ({@link pushRoster}). */
|
|
179
|
+
channel;
|
|
128
180
|
constructor(options) {
|
|
129
181
|
this.options = options;
|
|
130
182
|
}
|
|
183
|
+
/**
|
|
184
|
+
* Tell the server about a roster that changed without a scan asking (adapter builds moved by Axom,
|
|
185
|
+
* spec §6.1): the same `external_agents_scan_result` frame a scan answers with, under a client-made
|
|
186
|
+
* command id no scan is waiting on — the server records the roster and completes nothing. `false`
|
|
187
|
+
* when there is no open connection; the next `management_hello` carries {@link ManagementChannelOptions.bridges} then.
|
|
188
|
+
*/
|
|
189
|
+
pushRoster(bridges) {
|
|
190
|
+
const channel = this.channel;
|
|
191
|
+
if (!channel || channel.state !== 'OPEN')
|
|
192
|
+
return false;
|
|
193
|
+
try {
|
|
194
|
+
channel.send(buildScanResultFrame(`roster-push-${randomBytes(8).toString('hex')}`, bridges));
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
131
201
|
async connect(signal) {
|
|
132
202
|
this.serverManagementWire = undefined;
|
|
133
203
|
const nonce = randomBytes(16);
|
|
@@ -161,6 +231,10 @@ export class ManagementChannel {
|
|
|
161
231
|
// is never driven by the local browser/device-authorization flow.
|
|
162
232
|
await this.options.onReady?.({ generation: ready.generation, release: ready.release });
|
|
163
233
|
const pending = new Map();
|
|
234
|
+
this.channel = channel;
|
|
235
|
+
void channel.closed.then(() => { if (this.channel === channel)
|
|
236
|
+
this.channel = undefined; }, () => { if (this.channel === channel)
|
|
237
|
+
this.channel = undefined; });
|
|
164
238
|
void this.routeMessages(channel, pending);
|
|
165
239
|
return {
|
|
166
240
|
generation: ready.generation,
|
|
@@ -180,10 +254,11 @@ export class ManagementChannel {
|
|
|
180
254
|
}),
|
|
181
255
|
};
|
|
182
256
|
}
|
|
183
|
-
heartbeat(seq, sentAtEpochMs) {
|
|
184
|
-
const snapshot = this.options.snapshot();
|
|
257
|
+
async heartbeat(seq, sentAtEpochMs) {
|
|
258
|
+
const snapshot = await this.options.snapshot();
|
|
185
259
|
return {
|
|
186
260
|
t: 'heartbeat', seq, sentAtEpochMs, clientVersion: CLIENT_VERSION,
|
|
261
|
+
...(this.options.playgroundCwd ? { playgroundCwd: this.options.playgroundCwd } : {}),
|
|
187
262
|
channels: {
|
|
188
263
|
MANAGEMENT: snapshot.channels.management,
|
|
189
264
|
COMPUTER: snapshot.channels.computer,
|
|
@@ -195,6 +270,7 @@ export class ManagementChannel {
|
|
|
195
270
|
? { metrics: metricsForManagementWire(this.options.metrics(), this.serverManagementWire) }
|
|
196
271
|
: {}),
|
|
197
272
|
...(snapshot.update ? { update: statusForManagementWire(snapshot.update, this.serverManagementWire) } : {}),
|
|
273
|
+
...await keepersForManagementWire(this.options.keepers, this.serverManagementWire, this.options.keepersCapMs),
|
|
198
274
|
};
|
|
199
275
|
}
|
|
200
276
|
async nextReady(channel) {
|
|
@@ -217,6 +293,10 @@ export class ManagementChannel {
|
|
|
217
293
|
void dispatchUpdateCommand(channel, frame, this.options.onUpdate, this.serverManagementWire);
|
|
218
294
|
continue;
|
|
219
295
|
}
|
|
296
|
+
if (frame.t === 'keeper_control' && this.options.onKeeperControl) {
|
|
297
|
+
void dispatchKeeperControl(channel, frame, this.options.onKeeperControl);
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
220
300
|
if (frame.t === 'external_agents_scan' && this.options.onExternalAgentsScan) {
|
|
221
301
|
channel.send(buildScanResultFrame(frame.commandId, await this.options.onExternalAgentsScan()));
|
|
222
302
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveBrowserBinding } from './desktop/binding.js';
|
|
1
2
|
import { promises as fs } from 'node:fs';
|
|
2
3
|
import os from 'node:os';
|
|
3
4
|
import path from 'node:path';
|
|
@@ -33,7 +34,8 @@ export async function loadClientConfig(dir) {
|
|
|
33
34
|
if (!parsed?.server || !parsed.workstationId) {
|
|
34
35
|
throw new Error('Invalid workstation config: server and workstationId are required; run axom auth again');
|
|
35
36
|
}
|
|
36
|
-
|
|
37
|
+
const browserBinding = parsed.browserBinding ? resolveBrowserBinding(parsed.browserBinding, {}) : undefined;
|
|
38
|
+
return { ...(browserBinding ? { browserBinding } : {}), server: parsed.server, workstationId: parsed.workstationId, ...(Array.isArray(parsed.workspaces) ? { workspaces: parsed.workspaces } : {}) };
|
|
37
39
|
}
|
|
38
40
|
export async function writeKeys(dir, clientKeyB64, serverPubB64) {
|
|
39
41
|
await fs.mkdir(dir, { recursive: true });
|
|
@@ -67,6 +67,8 @@ export function connectEncryptedChannel(options) {
|
|
|
67
67
|
let openTimer;
|
|
68
68
|
let nextSequence = 1;
|
|
69
69
|
const outstandingAcks = new Set();
|
|
70
|
+
let lastReceivedAt = timers.now();
|
|
71
|
+
let maxHeartbeatLagMs = 0;
|
|
70
72
|
let resolveClosed;
|
|
71
73
|
let removeAbortListener = () => { };
|
|
72
74
|
const closed = new Promise((resolve) => { resolveClosed = resolve; });
|
|
@@ -103,30 +105,87 @@ export function connectEncryptedChannel(options) {
|
|
|
103
105
|
socket.send(crypto.seal(sessionKey, Buffer.from(options.encode(frame))));
|
|
104
106
|
};
|
|
105
107
|
const scheduleHeartbeat = () => {
|
|
108
|
+
const dueAt = timers.now() + heartbeatMs;
|
|
106
109
|
heartbeatTimer = timers.setTimeout(() => {
|
|
107
110
|
if (settled)
|
|
108
111
|
return;
|
|
112
|
+
maxHeartbeatLagMs = Math.max(maxHeartbeatLagMs, timers.now() - dueAt);
|
|
109
113
|
const sequence = nextSequence++;
|
|
110
114
|
// Register before send: a synchronous server acknowledgement/work frame emitted from
|
|
111
|
-
// `send` must observe and satisfy this precise liveness period.
|
|
115
|
+
// `send` must observe and satisfy this precise liveness period. This happens synchronously,
|
|
116
|
+
// before `options.heartbeat` is even awaited below, so that guarantee holds regardless of
|
|
117
|
+
// how long building the frame takes.
|
|
112
118
|
outstandingAcks.add(sequence);
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
119
|
+
// `options.heartbeat` may be async (management's reconciles on-disk update state first), so
|
|
120
|
+
// the rest of this tick's work moves into a promise chain. There is deliberately no separate
|
|
121
|
+
// "did we close while the frame was being built" guard between the await and `send`: `settled`
|
|
122
|
+
// and `state` are only ever flipped together, synchronously, inside `finish()`, so `send`'s own
|
|
123
|
+
// `state !== 'OPEN'` check already covers every case such a guard could -- a second copy of the
|
|
124
|
+
// same condition here would be unreachable, untestable dead code, not defense in depth.
|
|
125
|
+
void (async () => {
|
|
126
|
+
let frame;
|
|
127
|
+
try {
|
|
128
|
+
frame = await options.heartbeat(sequence, timers.now());
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
// Unlike a `send` failure (below), this can come from `options.heartbeat` itself doing
|
|
132
|
+
// real work before it returns a frame -- management's reconciles on-disk update state
|
|
133
|
+
// first -- so a rejection here can be the *only* sign a whole heartbeat cycle silently
|
|
134
|
+
// gave up. Log it rather than let the channel simply close with no trace of why.
|
|
135
|
+
console.error(`[encrypted-channel] heartbeat frame build failed, closing: ${error instanceof Error ? error.message : String(error)}`);
|
|
136
|
+
outstandingAcks.delete(sequence);
|
|
137
|
+
close();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
send(frame);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
console.error(`[encrypted-channel] heartbeat send failed, closing: ${error instanceof Error ? error.message : String(error)}`);
|
|
145
|
+
outstandingAcks.delete(sequence);
|
|
146
|
+
close();
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
// `ws.send` can synchronously deliver close/error through a test double or a transport
|
|
150
|
+
// implementation. Never resurrect liveness state after that terminal callback.
|
|
151
|
+
if (settled)
|
|
152
|
+
return;
|
|
153
|
+
if (outstandingAcks.size >= missedAcks) {
|
|
154
|
+
console.warn(`${new Date(timers.now()).toISOString()} [encrypted-channel] ${options.url.pathname} closing after ` +
|
|
155
|
+
`${outstandingAcks.size} unconfirmed heartbeats; silentMs=${Math.max(0, timers.now() - lastReceivedAt)}, ` +
|
|
156
|
+
`timerLagMs=${maxHeartbeatLagMs}`);
|
|
157
|
+
close();
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
scheduleHeartbeat();
|
|
161
|
+
})().catch((error) => {
|
|
162
|
+
// Nothing above is expected to throw past its own try/catch except `close()`'s
|
|
163
|
+
// `socket.close()` (a caller-supplied `WebSocketLike`) and `scheduleHeartbeat`'s
|
|
164
|
+
// `timers.setTimeout` -- both call into code this module does not control. Before
|
|
165
|
+
// `options.heartbeat` could be async, the equivalent synchronous throw escaped this
|
|
166
|
+
// `setTimeout` callback as an uncaught exception, which Node turns into a process exit that
|
|
167
|
+
// the supervisor restarts: loud and recoverable. Now it is a promise rejection instead, and
|
|
168
|
+
// `daemon.ts` registers a process-wide `unhandledRejection` handler that deliberately logs
|
|
169
|
+
// and keeps the daemon alive -- exactly wrong here, because an uncaught throw from
|
|
170
|
+
// `scheduleHeartbeat` in particular leaves `settled` false and `state` stuck at `'OPEN'`
|
|
171
|
+
// with nothing left to ever arm another heartbeat: the channel reports healthy forever while
|
|
172
|
+
// it has silently stopped. Force it terminal by hand instead of trusting whichever call
|
|
173
|
+
// above almost got there. `finish()` alone is synchronous and safe to call unconditionally
|
|
174
|
+
// (and idempotent if `close()` already ran before throwing). It does call two
|
|
175
|
+
// caller-supplied functions -- `timers.clearTimeout` and `removeAbortListener` (which wraps
|
|
176
|
+
// `AbortSignal.removeEventListener` when `options.signal` was given) -- so it is not
|
|
177
|
+
// literally incapable of throwing; it is safe here because production wiring for both is a
|
|
178
|
+
// real timer API and a real `AbortSignal`, neither of which throws from clearing/removing.
|
|
179
|
+
// The socket close is only ever best-effort from here, specifically so
|
|
180
|
+
// this handler's own failure stays impossible even if the very thing that just threw was
|
|
181
|
+
// `socket.close()` itself.
|
|
182
|
+
console.error(`[encrypted-channel] heartbeat cycle failed unrecoverably: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}`);
|
|
183
|
+
finish();
|
|
184
|
+
try {
|
|
185
|
+
socket.close();
|
|
186
|
+
}
|
|
187
|
+
catch { /* best-effort; already reported above */ }
|
|
188
|
+
});
|
|
130
189
|
}, heartbeatMs);
|
|
131
190
|
};
|
|
132
191
|
return new Promise((resolve, reject) => {
|
|
@@ -183,7 +242,7 @@ export function connectEncryptedChannel(options) {
|
|
|
183
242
|
throw new Error('Encrypted channel needs a session key or handshake');
|
|
184
243
|
opened = true;
|
|
185
244
|
if (options.initialFrame)
|
|
186
|
-
send(options.initialFrame());
|
|
245
|
+
send(await options.initialFrame());
|
|
187
246
|
// An initial proof may synchronously trigger a socket close/error. `opened` permits the
|
|
188
247
|
// proof frame itself, but the public connection is not published until this guard passes.
|
|
189
248
|
if (settled || connectionSettled)
|
|
@@ -193,6 +252,7 @@ export function connectEncryptedChannel(options) {
|
|
|
193
252
|
disarmOpenTimer();
|
|
194
253
|
resolve({
|
|
195
254
|
send,
|
|
255
|
+
get bufferedAmount() { return socket.bufferedAmount ?? 0; },
|
|
196
256
|
messages,
|
|
197
257
|
close,
|
|
198
258
|
closed,
|
|
@@ -226,14 +286,20 @@ export function connectEncryptedChannel(options) {
|
|
|
226
286
|
// be the only server traffic while a task runs. Invalid frames take the catch path and never
|
|
227
287
|
// get to satisfy liveness.
|
|
228
288
|
outstandingAcks.clear();
|
|
289
|
+
lastReceivedAt = timers.now();
|
|
290
|
+
maxHeartbeatLagMs = 0;
|
|
229
291
|
if (!acknowledged)
|
|
230
292
|
messages.push(frame);
|
|
231
293
|
}
|
|
232
294
|
catch {
|
|
295
|
+
console.warn(`${new Date(timers.now()).toISOString()} [encrypted-channel] ${options.url.pathname} closing after an invalid encrypted frame`);
|
|
233
296
|
close();
|
|
234
297
|
}
|
|
235
298
|
});
|
|
236
|
-
socket.on('close', () => {
|
|
299
|
+
socket.on('close', (code, reason) => {
|
|
300
|
+
if (!settled)
|
|
301
|
+
console.warn(`${new Date(timers.now()).toISOString()} [encrypted-channel] ${options.url.pathname} peer closed; ` +
|
|
302
|
+
`code=${code ?? 'unknown'}, reason=${reason?.toString() || 'none'}`);
|
|
237
303
|
if (!connectionSettled) {
|
|
238
304
|
connectionSettled = true;
|
|
239
305
|
finish();
|
|
@@ -242,9 +308,10 @@ export function connectEncryptedChannel(options) {
|
|
|
242
308
|
}
|
|
243
309
|
finish();
|
|
244
310
|
});
|
|
245
|
-
socket.on('error', () => {
|
|
311
|
+
socket.on('error', (error) => {
|
|
246
312
|
if (settled)
|
|
247
313
|
return;
|
|
314
|
+
console.warn(`${new Date(timers.now()).toISOString()} [encrypted-channel] ${options.url.pathname} socket error: ${error?.message ?? 'unknown'}`);
|
|
248
315
|
const rejectOpening = !connectionSettled;
|
|
249
316
|
// Mark this before close(): ws and deterministic fakes can emit close synchronously.
|
|
250
317
|
if (rejectOpening)
|