@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,7 +1,9 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
1
3
|
import path from 'node:path';
|
|
2
4
|
import { fileURLToPath } from 'node:url';
|
|
3
5
|
import { AGENT_WIRE_MIN_VERSION } from './protocol/agent.js';
|
|
4
|
-
import { clientDir, loadClientConfig, readKeys } from './config.js';
|
|
6
|
+
import { clientDir, loadClientConfig, readKeys, updateServiceContext } from './config.js';
|
|
5
7
|
import { AgentSessionManager } from './agentSessions.js';
|
|
6
8
|
import { AcpAgents } from './acp/acpAgents.js';
|
|
7
9
|
import { ConnectionSupervisor } from './connection/supervisor.js';
|
|
@@ -12,6 +14,21 @@ import { TaskRegistry } from './update/taskRegistry.js';
|
|
|
12
14
|
import { createUpdateCoordinator } from './update/runUpdate.js';
|
|
13
15
|
import { resolveCliPath } from './daemonctl.js';
|
|
14
16
|
import { SystemMetricsSampler } from './systemMetrics.js';
|
|
17
|
+
import { cachedPtyStatus, preflightOptionalPty } from './update/ptyPreflight.js';
|
|
18
|
+
import { FilesHostClient } from './files/client.js';
|
|
19
|
+
import { ToolsClient } from './tools/client.js';
|
|
20
|
+
import { WorkstationTools } from './tools/router.js';
|
|
21
|
+
import { aggregateToolsInfo, computeFilesInfo } from './tools/info.js';
|
|
22
|
+
import { defaultDistDir, KeeperLauncher } from './keepers/kit/launcher.js';
|
|
23
|
+
import { readKeeperManifest } from './keepers/kit/bundle.js';
|
|
24
|
+
import { KEEPER_POLICY_TICK_MS, KeeperPolicy, oneShotDrain, oneShotProbe } from './keepers/policy.js';
|
|
25
|
+
import { TerminalGenerations } from './keepers/terminal/generations.js';
|
|
26
|
+
import { AcpKeeperClient } from './keepers/acp/client.js';
|
|
27
|
+
import { KeeperTransport } from './acp/processor/transport/keeper.js';
|
|
28
|
+
import { keeperGenerations } from './acp/processor/transport/generations.js';
|
|
29
|
+
import { ExecPolicy } from './exec/policy.js';
|
|
30
|
+
import { LegacyToolsKeeper } from './keepers/legacy.js';
|
|
31
|
+
import { TerminalRouter, terminalEnv, terminalShell } from './tools/terminalRouter.js';
|
|
15
32
|
/**
|
|
16
33
|
* The path `runDaemon` hands `createUpdateCoordinator` as `cliPath`, used to re-provision the OS
|
|
17
34
|
* service after an update (see runUpdate.ts's `provisionService`).
|
|
@@ -105,6 +122,45 @@ export const IDLE_SWEEP_INTERVAL_MS = 10 * 60 * 1000;
|
|
|
105
122
|
* nobody awaits a timer callback, and an escaping rejection is an unhandled rejection, which Node
|
|
106
123
|
* answers by exiting.
|
|
107
124
|
*/
|
|
125
|
+
/**
|
|
126
|
+
* The daemon's start-time adapter build upkeep: legacy migration, then the certified reconcile; calls
|
|
127
|
+
* `onRosterChanged` once if either changed any agent's current build. Never rejects.
|
|
128
|
+
*/
|
|
129
|
+
export async function maintainAdapterBuilds(acp, onRosterChanged) {
|
|
130
|
+
try {
|
|
131
|
+
const migrated = await acp.migrateLegacyAdapters();
|
|
132
|
+
const moves = await acp.reconcileCertifiedBuilds();
|
|
133
|
+
if (migrated.length || moves.length)
|
|
134
|
+
await onRosterChanged();
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
console.warn(`[acp-builds] adapter build upkeep failed: ${String(error)}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/** How often held sessions are looked at for a pending switch (spec §6.3 step 7) or a lost adapter to reopen (Task 24). */
|
|
141
|
+
export const SWITCH_POLL_INTERVAL_MS = 5_000;
|
|
142
|
+
/** How often unused adapter builds are collected (spec §6.3 "GC"; the first run is one grace period after start). */
|
|
143
|
+
export const BUILD_GC_INTERVAL_MS = 10 * 60_000;
|
|
144
|
+
/**
|
|
145
|
+
* Task 24: the switch scheduler (every {@link SWITCH_POLL_INTERVAL_MS}: `agent_reopen_request` for idle sessions with a pending
|
|
146
|
+
* switch, and for keeper-hosted sessions whose adapter keeper-acp lost) and build GC (every {@link BUILD_GC_INTERVAL_MS}). Both
|
|
147
|
+
* timers are `unref`'d, and neither callback can reject. Returns the stop.
|
|
148
|
+
*/
|
|
149
|
+
export function startSwitchUpkeep(agents, acp, pollMs = SWITCH_POLL_INTERVAL_MS, gcMs = BUILD_GC_INTERVAL_MS) {
|
|
150
|
+
let polling = false;
|
|
151
|
+
const poll = setInterval(() => {
|
|
152
|
+
if (polling)
|
|
153
|
+
return;
|
|
154
|
+
polling = true;
|
|
155
|
+
void Promise.resolve().then(() => agents.pollSwitches()).catch((err) => {
|
|
156
|
+
console.error(`[agent] switch poll failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
157
|
+
}).finally(() => { polling = false; });
|
|
158
|
+
}, pollMs);
|
|
159
|
+
poll.unref();
|
|
160
|
+
const gc = setInterval(() => { void Promise.resolve().then(() => acp.collectBuilds()).catch(() => undefined); }, gcMs);
|
|
161
|
+
gc.unref();
|
|
162
|
+
return () => { clearInterval(poll); clearInterval(gc); };
|
|
163
|
+
}
|
|
108
164
|
export function startIdleSweep(agents, ttlMs = IDLE_SESSION_TTL_MS, intervalMs = IDLE_SWEEP_INTERVAL_MS) {
|
|
109
165
|
const timer = setInterval(() => {
|
|
110
166
|
void (async () => {
|
|
@@ -140,6 +196,90 @@ export function resolveServiceLifecycle(options) {
|
|
|
140
196
|
// provision-and-arm would hand that mode to a one-shot, unsupervised debugging invocation.
|
|
141
197
|
return options.supervision === 'container' ? 'container' : 'none';
|
|
142
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* Builds the object `ManagementChannelOptions.snapshot` reports on every management heartbeat.
|
|
201
|
+
*
|
|
202
|
+
* Lifted out of `runDaemon` into its own function -- rather than staying the closure literal
|
|
203
|
+
* passed inline to `new ManagementChannel({ snapshot: ... })` -- specifically so a test can drive
|
|
204
|
+
* it directly. `runDaemon` boots a whole daemon (real config, real keys, a real `ManagementChannel`
|
|
205
|
+
* with no seam for injecting a socket), so before this extraction the *only* thing that ever
|
|
206
|
+
* verified `update:` here was the boot-*reconciled* phase (`updates.snapshotAsync()`), rather than
|
|
207
|
+
* the raw on-disk one (`updates.snapshot()`), was `tsc` accepting the `await`. `tsc` cannot tell
|
|
208
|
+
* `snapshotAsync()` and `snapshot()` apart by behavior -- both type-check -- so a future edit that
|
|
209
|
+
* quietly swaps one for the other would compile clean and wedge the server's view of every
|
|
210
|
+
* workstation the next time one restarts mid-update.
|
|
211
|
+
*/
|
|
212
|
+
export async function buildManagementSnapshot(supervisor, tasks, updates) {
|
|
213
|
+
return { channels: supervisor.channelSnapshot(), ...tasks.snapshot(), update: await updates.snapshotAsync() };
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* The files host's configured roots: every workspace cwd, absolute (a relative one is taken relative to the home
|
|
217
|
+
* directory; FileTools refuses relative roots because they would resolve against whatever directory a pinned section
|
|
218
|
+
* holds).
|
|
219
|
+
*/
|
|
220
|
+
export function workspaceRoots(cfg, home = os.homedir()) {
|
|
221
|
+
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))];
|
|
222
|
+
}
|
|
223
|
+
/** The roots that exist as directories right now. A missing workspace must not keep the host (all of Files) down. */
|
|
224
|
+
export async function existingDirectories(roots) {
|
|
225
|
+
const existing = [];
|
|
226
|
+
for (const root of roots) {
|
|
227
|
+
try {
|
|
228
|
+
if ((await fs.stat(root)).isDirectory())
|
|
229
|
+
existing.push(root);
|
|
230
|
+
}
|
|
231
|
+
catch { /* left out until it exists */ }
|
|
232
|
+
}
|
|
233
|
+
return existing;
|
|
234
|
+
}
|
|
235
|
+
/** How often the daemon re-reads the workspaces config (and root existence) to restart the files host on a change. */
|
|
236
|
+
export const FILES_ROOTS_POLL_MS = 10_000;
|
|
237
|
+
/** How often the legacy tools keeper is re-probed and checked for retirement (keepers/legacy.ts). */
|
|
238
|
+
export const LEGACY_TICK_MS = 30_000;
|
|
239
|
+
/**
|
|
240
|
+
* Builds the object `ManagementChannelOptions.keepers` reports (Task 26): the policy's statuses, never a rejection (a failure is
|
|
241
|
+
* `null`, which the channel also guarantees). `countedExecs` (final review minor 7): keeper-terminal execs `activeShellTasks` also
|
|
242
|
+
* counts right now — reported as the current terminal keeper's `execCounted` metric (sampled with this heartbeat), so the
|
|
243
|
+
* Workstations page counts `activeShellTasks + execRunning − execCounted` commands, each once.
|
|
244
|
+
*/
|
|
245
|
+
export async function buildKeeperStatuses(policy, countedExecs = () => 0) {
|
|
246
|
+
let statuses;
|
|
247
|
+
try {
|
|
248
|
+
statuses = await policy.statuses();
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
const terminals = statuses.filter((status) => status.kind === 'terminal' && !status.legacy);
|
|
254
|
+
const target = terminals.find((status) => status.replacement === 'none') ?? terminals[0];
|
|
255
|
+
if (!target)
|
|
256
|
+
return statuses;
|
|
257
|
+
let counted = 0;
|
|
258
|
+
try {
|
|
259
|
+
const value = countedExecs();
|
|
260
|
+
counted = Number.isSafeInteger(value) && value > 0 ? value : 0;
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
counted = 0;
|
|
264
|
+
}
|
|
265
|
+
// A copy: the policy reuses its sample for a while.
|
|
266
|
+
return statuses.map((status) => (status === target ? { ...status, metrics: { ...status.metrics, execCounted: counted } } : status));
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Runs `poll` every `intervalMs`, skipping a tick while the previous run is still in flight (on a hung mount a stat
|
|
270
|
+
* may never return; overlapping runs would pile up behind it). The timer is unref'd. Returns the stop function.
|
|
271
|
+
*/
|
|
272
|
+
export function startExclusivePoll(poll, intervalMs) {
|
|
273
|
+
let running = false;
|
|
274
|
+
const timer = setInterval(() => {
|
|
275
|
+
if (running)
|
|
276
|
+
return;
|
|
277
|
+
running = true;
|
|
278
|
+
void poll().catch(() => { }).finally(() => { running = false; });
|
|
279
|
+
}, intervalMs);
|
|
280
|
+
timer.unref();
|
|
281
|
+
return () => clearInterval(timer);
|
|
282
|
+
}
|
|
143
283
|
export async function runDaemon(options = {}) {
|
|
144
284
|
// Last line of defence for the whole process. Node's default for an unhandled rejection is to
|
|
145
285
|
// *exit*, and this daemon is the only thing keeping every session on this workstation alive —
|
|
@@ -152,6 +292,9 @@ export async function runDaemon(options = {}) {
|
|
|
152
292
|
const dir = clientDir();
|
|
153
293
|
const cfg = await loadClientConfig(dir);
|
|
154
294
|
const { clientKey, serverPub } = await readKeys(dir);
|
|
295
|
+
// Repair the first fixed release even when it was installed by an older ignore-scripts updater,
|
|
296
|
+
// before a newly started keeper-terminal snapshots native dependency availability.
|
|
297
|
+
await preflightOptionalPty(updateServiceContext(dir));
|
|
155
298
|
const tasks = new TaskRegistry();
|
|
156
299
|
// Attaches both the server-driven update path (onUpdate below) and axom update's manual path to
|
|
157
300
|
// the same coordinator assembly — see runUpdate.ts.
|
|
@@ -176,31 +319,179 @@ export async function runDaemon(options = {}) {
|
|
|
176
319
|
});
|
|
177
320
|
// One ACP client for every external agent; the roster it reports is the ACP agent registry, keyed
|
|
178
321
|
// by registry agent id (ruling R2) — the same key `agent_open.bridge` now carries.
|
|
179
|
-
|
|
180
|
-
|
|
322
|
+
// Live sessions run in keeper-acp (spec §5.3) — the adapters outlive the daemon there — or, when it cannot be reached, in the
|
|
323
|
+
// daemon itself through the local fallback (§5.7). Short-lived connections (list, delete, verify, login) stay direct children.
|
|
324
|
+
const acpLauncher = new KeeperLauncher({ configDir: dir, workstationId: cfg.workstationId, server: cfg.server });
|
|
325
|
+
const acpTransport = new KeeperTransport(new AcpKeeperClient({ launcher: acpLauncher }));
|
|
326
|
+
// Sessions keeper-acp still runs from a previous daemon (a restart, an upgrade, a crash) are adopted where they are — across
|
|
327
|
+
// keeper-acp generations (spec §5.5, §6.3) — before the agent channel's reconnect offer is answered.
|
|
328
|
+
const acpGenerations = keeperGenerations({ launcher: acpLauncher, current: acpTransport });
|
|
329
|
+
const acp = new AcpAgents({ cacheDir: dir, transports: { keeper: acpTransport }, keeperGenerations: acpGenerations });
|
|
330
|
+
const agents = new AgentSessionManager(acp, null, path.join(dir, 'agents'), tasks);
|
|
331
|
+
void agents.adoptSessions();
|
|
181
332
|
let bridges = await acp.roster();
|
|
333
|
+
// 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,
|
|
334
|
+
// 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.
|
|
335
|
+
const scanRoster = () => acp.roster({ sessions: (session) => agents.rosterSessionState(session) });
|
|
182
336
|
let supervisor;
|
|
183
337
|
// Plaintext WebSockets are only for an explicitly opted-in local development fixture. Production
|
|
184
338
|
// ticket handshakes must be WSS because the ticket is the data-channel key derivation material.
|
|
185
339
|
const allowInsecureWs = process.env.AXOM_WORKSTATION_ALLOW_INSECURE_WS === '1';
|
|
186
|
-
|
|
340
|
+
// Files: one daemon-owned host process (files/host.ts) serves file_*_request frames and files.* tool requests.
|
|
341
|
+
const home = os.homedir();
|
|
342
|
+
const playgroundCwd = path.join(home, '.axom', 'playground');
|
|
343
|
+
await fs.mkdir(playgroundCwd, { recursive: true });
|
|
344
|
+
let roots = workspaceRoots(cfg, home);
|
|
345
|
+
let hostRoots = [];
|
|
346
|
+
const filesHost = new FilesHostClient({
|
|
347
|
+
configDir: dir,
|
|
348
|
+
options: async () => { hostRoots = await existingDirectories(roots); return { home, roots: hostRoots }; },
|
|
349
|
+
onActivityChanged: () => tasks.externalActivityChanged(),
|
|
350
|
+
log: (line) => console.error(`${new Date().toISOString()} ${line}`),
|
|
351
|
+
});
|
|
352
|
+
tasks.externalActivity = () => filesHost.activity();
|
|
353
|
+
filesHost.start();
|
|
354
|
+
const stopRootsPoll = startExclusivePoll(async () => {
|
|
355
|
+
try {
|
|
356
|
+
roots = workspaceRoots(await loadClientConfig(dir), home);
|
|
357
|
+
const current = await existingDirectories(roots);
|
|
358
|
+
if (JSON.stringify(current) !== JSON.stringify(hostRoots))
|
|
359
|
+
filesHost.reconfigure();
|
|
360
|
+
}
|
|
361
|
+
catch { /* an unreadable config keeps the current host */ }
|
|
362
|
+
}, FILES_ROOTS_POLL_MS);
|
|
363
|
+
// tools.info is aggregated here and starts no keeper: its terminal fields come from keeper-terminal's hello when that keeper
|
|
364
|
+
// already runs, else from the cached node-pty check. terminal.* go through the TerminalRouter (keeper-terminal, or a legacy
|
|
365
|
+
// tools keeper an older CLI left running with live PTYs: served until they end, then stopped). browser.* keep the
|
|
366
|
+
// browser-kind ToolsClient path. Nothing here constructs a terminal-kind ToolsClient: its request() would start the legacy keeper.
|
|
367
|
+
const toolsConfig = { configDir: dir, workstationId: cfg.workstationId, server: cfg.server, roots, browserBinding: cfg.browserBinding };
|
|
368
|
+
const browserTools = new ToolsClient(toolsConfig, 'browser');
|
|
369
|
+
const ptyStatus = cachedPtyStatus();
|
|
370
|
+
// keeper-terminal generations (Task 26): new work goes to the serving generation; PTYs and execs an older generation still runs
|
|
371
|
+
// after an upgrade are routed to it by owner until the replacement policy retires it.
|
|
372
|
+
const terminalLauncher = new KeeperLauncher({ configDir: dir, workstationId: cfg.workstationId, server: cfg.server });
|
|
373
|
+
const terminalKeepers = new TerminalGenerations({ launcher: terminalLauncher });
|
|
374
|
+
const legacyKeeper = new LegacyToolsKeeper({
|
|
375
|
+
tools: toolsConfig,
|
|
376
|
+
recoverUploads: (uploads) => filesHost.recoverUploads(uploads),
|
|
377
|
+
log: (line) => console.error(`${new Date().toISOString()} ${line}`),
|
|
378
|
+
});
|
|
379
|
+
void legacyKeeper.detect();
|
|
380
|
+
const stopLegacyPoll = startExclusivePoll(() => legacyKeeper.tick(), LEGACY_TICK_MS);
|
|
381
|
+
const terminals = new TerminalRouter({
|
|
382
|
+
kits: terminalKeepers,
|
|
383
|
+
legacy: legacyKeeper,
|
|
384
|
+
launch: async (cwd) => ({ cwd: await filesHost.terminalCwd(cwd), shell: terminalShell(), env: terminalEnv() }),
|
|
385
|
+
});
|
|
386
|
+
const workstationTools = new WorkstationTools({
|
|
387
|
+
files: filesHost,
|
|
388
|
+
terminal: (method, params) => terminals.request(method, params),
|
|
389
|
+
browser: (method, params) => browserTools.request(method, params),
|
|
390
|
+
info: () => aggregateToolsInfo({
|
|
391
|
+
files: async () => filesHost.info() ?? computeFilesInfo(home, roots),
|
|
392
|
+
terminal: () => terminalKeepers.terminalStatus(ptyStatus),
|
|
393
|
+
browser: (method, params) => browserTools.request(method, params),
|
|
394
|
+
}),
|
|
395
|
+
});
|
|
396
|
+
// exec_request: keeper-terminal (durable) when available, else in this process; argv/env/deadline/shaping decided here.
|
|
397
|
+
// Execs a previous daemon left in keeper-terminal are recovered first: wire ≤ 4 ones are stopped (their channel is gone), wire v5
|
|
398
|
+
// ones are followed for the server to re-attach (exec_attach on the next computer channel).
|
|
399
|
+
// Keeper replacement (spec §8.3, Task 26): what the heartbeat reports of each keeper generation, and what happens to an older one.
|
|
400
|
+
const drainKeeper = oneShotDrain((kind, gen) => (kind === 'acp' ? acpLauncher : terminalLauncher).pathsFor(kind, gen));
|
|
401
|
+
const keeperPolicy = new KeeperPolicy({
|
|
402
|
+
dir: path.join(dir, 'keepers'),
|
|
403
|
+
probe: oneShotProbe((kind, gen) => (kind === 'acp' ? acpLauncher : terminalLauncher).pathsFor(kind, gen)),
|
|
404
|
+
terminal: {
|
|
405
|
+
currentGen: () => terminalKeepers.currentGen(),
|
|
406
|
+
live: async () => (await terminalKeepers.running()).map((generation) => generation.gen),
|
|
407
|
+
manifest: () => terminalKeepers.manifest(),
|
|
408
|
+
stop: async (gen, mode) => {
|
|
409
|
+
const result = await terminalLauncher.stop('terminal', gen, mode);
|
|
410
|
+
if (result.stopped)
|
|
411
|
+
terminalKeepers.forget(gen);
|
|
412
|
+
return result;
|
|
413
|
+
},
|
|
414
|
+
reachable: async (gen) => (await terminalKeepers.client(gen)).runningHello(),
|
|
415
|
+
drain: (gen) => drainKeeper('terminal', gen),
|
|
416
|
+
},
|
|
417
|
+
legacy: legacyKeeper,
|
|
418
|
+
acp: {
|
|
419
|
+
currentGen: () => acpGenerations.current(),
|
|
420
|
+
live: () => acpGenerations.live(),
|
|
421
|
+
manifest: () => readKeeperManifest(path.join(defaultDistDir(), 'acp')),
|
|
422
|
+
stop: (gen, mode) => acpLauncher.stop('acp', gen, mode),
|
|
423
|
+
moveOff: (gens) => agents.moveOffOlderKeeperGenerations(undefined, gens),
|
|
424
|
+
cancelMoves: async (refs) => { for (const sessionRef of refs)
|
|
425
|
+
await agents.cancelSwitch({ sessionRef }); },
|
|
426
|
+
retireIdle: () => acp.retireIdleGenerations(),
|
|
427
|
+
session: (ref) => agents.sessionView(ref),
|
|
428
|
+
},
|
|
429
|
+
agentName: (agentId) => bridges.find((bridge) => bridge.type === agentId)?.displayName || agentId,
|
|
430
|
+
legacyShell: () => terminalShell(),
|
|
431
|
+
log: (line) => console.error(`${new Date().toISOString()} ${line}`),
|
|
432
|
+
});
|
|
433
|
+
terminalKeepers.picker = (current, older) => keeperPolicy.terminalServing(current, older);
|
|
434
|
+
const stopKeeperPolicy = startExclusivePoll(() => keeperPolicy.tick(), KEEPER_POLICY_TICK_MS);
|
|
435
|
+
// Spec §3.1: keeper sockets that fell back to /tmp (a long configDir path) are touched so a /tmp cleaner never reaps them.
|
|
436
|
+
const stopSocketTouch = startExclusivePoll(async () => {
|
|
437
|
+
await acpLauncher.touchSockets('acp');
|
|
438
|
+
await terminalLauncher.touchSockets('terminal');
|
|
439
|
+
}, KEEPER_POLICY_TICK_MS);
|
|
440
|
+
void keeperPolicy.tick();
|
|
441
|
+
const execPolicy = new ExecPolicy({ generations: terminalKeepers, log: (line) => console.error(`${new Date().toISOString()} ${line}`) });
|
|
442
|
+
void execPolicy.recover();
|
|
443
|
+
const computer = new ComputerChannel({ server: cfg.server, allowInsecureWs, taskRegistry: tasks, browserBinding: cfg.browserBinding,
|
|
444
|
+
runExec: (request, onOutput, signal, hooks) => execPolicy.run(request, onOutput, signal, hooks),
|
|
445
|
+
execs: execPolicy, log: (line) => console.error(`${new Date().toISOString()} ${line}`),
|
|
446
|
+
tools: (method, params) => workstationTools.request(method, params),
|
|
447
|
+
runFrame: (request, signal) => workstationTools.runFrame(request, signal),
|
|
448
|
+
});
|
|
187
449
|
const metrics = new SystemMetricsSampler(cfg.workspaces ?? []);
|
|
188
450
|
await metrics.start();
|
|
189
451
|
const management = new ManagementChannel({
|
|
190
452
|
server: cfg.server, workstationId: cfg.workstationId, workstation: cfg.workstationId, clientKey, serverPub,
|
|
191
453
|
bridges: () => bridges,
|
|
192
454
|
workspaces: cfg.workspaces,
|
|
455
|
+
playgroundCwd,
|
|
193
456
|
metrics: () => metrics.snapshot(),
|
|
194
457
|
allowInsecureWs,
|
|
195
458
|
onReady: (ready) => updates.managementReady(ready.release, ready.generation),
|
|
196
459
|
onUpdate: (command) => updates.handle(command),
|
|
197
460
|
onExternalAgentsScan: async () => {
|
|
198
|
-
bridges = await
|
|
461
|
+
bridges = await scanRoster();
|
|
199
462
|
return bridges;
|
|
200
463
|
},
|
|
201
|
-
snapshot: () => (
|
|
464
|
+
snapshot: () => buildManagementSnapshot(supervisor, tasks, updates),
|
|
465
|
+
keepers: () => buildKeeperStatuses(keeperPolicy, () => tasks.countedKeeperExecs),
|
|
466
|
+
onKeeperControl: (frame) => keeperPolicy.control(frame),
|
|
202
467
|
});
|
|
203
468
|
const agent = new AgentChannel({ server: cfg.server, agents, bridges: () => bridges, allowInsecureWs });
|
|
469
|
+
// Spec §6.2: first take over what an older CLI left (`_npx` copies, pre-build binary dirs) so agents
|
|
470
|
+
// that worked keep working; then §6.1: a version this CLI certifies (runtime.json/catalog pin) or a
|
|
471
|
+
// changed patch set moves an installed agent's current build forward by itself ("Updated by Axom").
|
|
472
|
+
// In the background: either may copy or download, an open of an agent not done yet waits on that
|
|
473
|
+
// agent's lock, and running sessions keep the build they were spawned from either way. A changed
|
|
474
|
+
// roster reaches the server the way a scan's does; offline, the next hello carries it.
|
|
475
|
+
void maintainAdapterBuilds(acp, async () => {
|
|
476
|
+
bridges = await scanRoster();
|
|
477
|
+
management.pushRoster(bridges);
|
|
478
|
+
});
|
|
479
|
+
// A quota capability learned mid-flight (this process's first spawn of an agent) changes what the
|
|
480
|
+
// roster would say; push it the way a build change does. Coalesced: several agents flipping
|
|
481
|
+
// around one burst of opens share a single scan, and a flip during that scan queues one more.
|
|
482
|
+
let rosterPushQueued = false;
|
|
483
|
+
acp.onRosterChanged(() => {
|
|
484
|
+
if (rosterPushQueued)
|
|
485
|
+
return;
|
|
486
|
+
rosterPushQueued = true;
|
|
487
|
+
void Promise.resolve().then(async () => {
|
|
488
|
+
rosterPushQueued = false;
|
|
489
|
+
bridges = await scanRoster();
|
|
490
|
+
management.pushRoster(bridges);
|
|
491
|
+
}).catch((error) => {
|
|
492
|
+
console.warn(`[acp-builds] roster push after capability change failed: ${String(error)}`);
|
|
493
|
+
});
|
|
494
|
+
});
|
|
204
495
|
supervisor = new ConnectionSupervisor({
|
|
205
496
|
management, computer, agent,
|
|
206
497
|
// Timestamped here rather than inside the supervisor: the daemon owns this log's shape. The
|
|
@@ -209,6 +500,7 @@ export async function runDaemon(options = {}) {
|
|
|
209
500
|
log: (message) => console.log(`${new Date().toISOString()} ${message}`),
|
|
210
501
|
});
|
|
211
502
|
const stopIdleSweep = startIdleSweep(agents);
|
|
503
|
+
const stopSwitchUpkeep = startSwitchUpkeep(agents, acp);
|
|
212
504
|
const shutdown = new AbortController();
|
|
213
505
|
const stop = () => shutdown.abort();
|
|
214
506
|
process.once('SIGINT', stop);
|
|
@@ -220,6 +512,17 @@ export async function runDaemon(options = {}) {
|
|
|
220
512
|
process.removeListener('SIGINT', stop);
|
|
221
513
|
process.removeListener('SIGTERM', stop);
|
|
222
514
|
stopIdleSweep();
|
|
515
|
+
stopSwitchUpkeep();
|
|
223
516
|
metrics.stop();
|
|
517
|
+
stopRootsPoll();
|
|
518
|
+
stopLegacyPoll();
|
|
519
|
+
stopKeeperPolicy();
|
|
520
|
+
stopSocketTouch();
|
|
521
|
+
terminalKeepers.disconnect(); // only the connections: keeper-terminal and its PTYs outlive the daemon
|
|
522
|
+
// keeper-hosted sessions are detached (head checkpoint, adapter untouched, no failed turn); the others checkpoint (spec §5.5.5)
|
|
523
|
+
await agents.detachAll();
|
|
524
|
+
acp.closeTransports(); // keeper-acp's adapters outlive the daemon; local-fallback adapters die with it (spec §5.7)
|
|
525
|
+
// Bounded: a graceful host shutdown, then SIGKILL (a hung IO cannot hold the daemon's exit).
|
|
526
|
+
await filesHost.stop();
|
|
224
527
|
}
|
|
225
528
|
}
|
|
@@ -5,6 +5,8 @@ import { clientDir, loadClientConfig } from './config.js';
|
|
|
5
5
|
import { createServiceManager } from './service/installer.js';
|
|
6
6
|
import { buildServicePath } from './service/types.js';
|
|
7
7
|
import { loginShellPath } from './service/loginShell.js';
|
|
8
|
+
import { KeeperLauncher } from './keepers/kit/launcher.js';
|
|
9
|
+
import { stopKeepers } from './keepers/stop.js';
|
|
8
10
|
const __filename = fileURLToPath(import.meta.url);
|
|
9
11
|
/**
|
|
10
12
|
* The package's real npm bin entry (`package.json`'s `bin.axom`), given the directory a compiled
|
|
@@ -49,9 +51,20 @@ export function serviceContext() {
|
|
|
49
51
|
pathEnv: buildServicePath(process.env.PATH, nodeDirectory),
|
|
50
52
|
};
|
|
51
53
|
}
|
|
54
|
+
/** The workstation's keepers stopped (see {@link DaemonControllerDeps.stopKeepers}); an unregistered client has none. */
|
|
55
|
+
async function stopRegisteredKeepers() {
|
|
56
|
+
const configDir = clientDir();
|
|
57
|
+
const config = await loadClientConfig(configDir).catch(() => null);
|
|
58
|
+
if (!config?.workstationId || !config.server)
|
|
59
|
+
return;
|
|
60
|
+
await stopKeepers(new KeeperLauncher({ configDir, workstationId: config.workstationId, server: config.server }), {
|
|
61
|
+
log: (line) => console.error(line),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
52
64
|
const defaultDeps = {
|
|
53
65
|
service: (context) => createServiceManager(context ?? serviceContext()),
|
|
54
66
|
config: () => loadClientConfig(clientDir()),
|
|
67
|
+
stopKeepers: stopRegisteredKeepers,
|
|
55
68
|
};
|
|
56
69
|
/**
|
|
57
70
|
* User services own the daemon lifecycle. Deliberately no PID-file fallback remains: a stale
|
|
@@ -73,9 +86,22 @@ export function createDaemonController(overrides = {}) {
|
|
|
73
86
|
: base;
|
|
74
87
|
await deps.service(context).install({ interactive: true });
|
|
75
88
|
},
|
|
76
|
-
|
|
89
|
+
// The workstation is stopped by the user: what its keepers host ends too (the daemon first, so nothing relaunches them).
|
|
90
|
+
// In a `finally`: a service that is not installed (or fails to stop) must not leave the keepers running.
|
|
91
|
+
async uninstall() { try {
|
|
92
|
+
await deps.service().uninstall();
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
await deps.stopKeepers();
|
|
96
|
+
} },
|
|
77
97
|
async start() { await deps.service().start(); },
|
|
78
|
-
async stop() {
|
|
98
|
+
async stop() { try {
|
|
99
|
+
await deps.service().stop();
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
await deps.stopKeepers();
|
|
103
|
+
} },
|
|
104
|
+
// A restart keeps the keepers: terminals, commands and agent sessions go on under the next daemon.
|
|
79
105
|
async restart() { await deps.service().restart(); },
|
|
80
106
|
async status() {
|
|
81
107
|
const state = await deps.service().status();
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
function endpoint(text, kind) {
|
|
3
|
+
let url;
|
|
4
|
+
try {
|
|
5
|
+
url = new URL(text);
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
throw new Error('Invalid browser endpoint');
|
|
9
|
+
}
|
|
10
|
+
// Require literal loopback authorities, before WHATWG canonicalizes short/hex IPv4.
|
|
11
|
+
if (!/^(?:tcp|ws|wss|http|https):\/\/(?:127\.0\.0\.1|\[::1\]):\d+(?:\/|$)/.test(text) || url.username || url.password || url.search || url.hash || !url.port || Number(url.port) < 1 || Number(url.port) > 65535)
|
|
12
|
+
throw new Error('Browser endpoints require an explicit loopback host and port without credentials');
|
|
13
|
+
if (kind === 'vnc' ? !['tcp:', 'ws:', 'wss:'].includes(url.protocol) : !['http:', 'https:'].includes(url.protocol))
|
|
14
|
+
throw new Error('Unsupported browser endpoint transport');
|
|
15
|
+
if (url.protocol === 'tcp:' && url.pathname && url.pathname !== '/')
|
|
16
|
+
throw new Error('TCP endpoint cannot have a path');
|
|
17
|
+
return url.toString();
|
|
18
|
+
}
|
|
19
|
+
export function resolveBrowserBinding(config, env = process.env) {
|
|
20
|
+
if (config && (typeof config !== 'object' || typeof config.vncEndpoint !== 'string' || !config.vncEndpoint))
|
|
21
|
+
throw new Error('Invalid browser binding config');
|
|
22
|
+
const vnc = env.AXOM_BROWSER_VNC_URL ?? config?.vncEndpoint;
|
|
23
|
+
if (!vnc)
|
|
24
|
+
return undefined;
|
|
25
|
+
const binding = { bindingId: config?.bindingId ?? 'default', displayName: config?.displayName ?? 'Shared browser', vncEndpoint: endpoint(vnc, 'vnc'), cdpEndpoint: env.AXOM_BROWSER_CDP_URL ?? config?.cdpEndpoint, downloadsRoot: config?.downloadsRoot };
|
|
26
|
+
if (typeof binding.bindingId !== 'string' || !binding.bindingId || binding.bindingId.length > 128 || typeof binding.displayName !== 'string' || !binding.displayName || binding.displayName.length > 128)
|
|
27
|
+
throw new Error('Invalid browser binding metadata');
|
|
28
|
+
if (binding.cdpEndpoint)
|
|
29
|
+
binding.cdpEndpoint = endpoint(binding.cdpEndpoint, 'cdp');
|
|
30
|
+
if (binding.downloadsRoot !== undefined && (typeof binding.downloadsRoot !== 'string' || !path.isAbsolute(binding.downloadsRoot) || binding.downloadsRoot.includes('\0')))
|
|
31
|
+
throw new Error('Browser downloadsRoot must be an explicit absolute directory');
|
|
32
|
+
return binding;
|
|
33
|
+
}
|
|
34
|
+
export function browserCapability(binding) {
|
|
35
|
+
return { bindingId: binding?.bindingId ?? null, displayName: binding?.displayName ?? null, configured: !!binding, cdp: !!binding?.cdpEndpoint };
|
|
36
|
+
}
|