@platless/axom-cli 0.5.9 → 0.6.52
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 +25 -6
- package/dist/axom-patch/cli/README.md +129 -12
- package/dist/axom-patch/cli/account-limits/generate.py +15 -12
- package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +20 -3
- package/dist/axom-patch/cli/claude-account-limits.json +9 -9
- package/dist/axom-patch/cli/claude-replay-lifecycle.json +16 -0
- package/dist/axom-patch/cli/codex-account-limits.json +11 -11
- package/dist/axom-patch/cli/codex-terminal-output.json +16 -0
- package/dist/axom-patch/cli/commandcode-account-limits.json +9 -9
- 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 +55 -13
- package/dist/axom-patch/cli/probes/claude-replay-lifecycle.mjs +31 -0
- 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/zcode-env-account.mjs +81 -0
- package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +7 -0
- package/dist/axom-patch/cli/qoder/dist/axom-acp-compat.mjs +1687 -0
- package/dist/axom-patch/cli/qoder/releases/1.1.75.json +277 -0
- package/dist/axom-patch/cli/qoder-background-terminal.md +13 -6
- package/dist/axom-patch/cli/qoder-foreground-output.md +14 -8
- 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 +19 -19
- package/dist/axom-patch/cli/zcode/env-account.mjs +13 -0
- package/dist/axom-patch/cli/zcode/generate-env-account.py +52 -0
- package/dist/axom-patch/cli/zcode-account-limits.json +10 -11
- package/dist/axom-patch/cli/zcode-env-account.json +58 -0
- package/dist/axom-patch/cli/zcode-models.json +9 -10
- package/dist/axom-patch/cli/zcode-subagent-subwork.json +10 -11
- package/dist/cli.js +11 -45
- package/dist/commands/auth.js +4 -1
- package/dist/commands/knowledge.js +47 -0
- package/dist/commands/update.js +126 -3
- package/dist/commands/workstation.js +101 -164
- package/dist/commands/workstationRegistrations.js +116 -0
- package/dist/credentialBroker.js +13 -2
- package/dist/credentials.js +16 -0
- package/dist/install/claude/commands.js +15 -15
- package/dist/keepers/terminal/keeper.mjs +1089 -673
- package/dist/keepers/terminal/manifest.json +4 -4
- package/dist/mcp.js +1 -1
- package/dist/native/darwin-x64/axom-quic +0 -0
- package/dist/workstation/acp/acpAgents.js +113 -102
- package/dist/workstation/acp/acpClient.js +26 -0
- package/dist/workstation/acp/builds/install/npm.js +99 -10
- package/dist/workstation/acp/initializeResponse.js +6 -1
- package/dist/workstation/acp/managedPatches.js +189 -25
- package/dist/workstation/acp/managedRuntime.js +314 -0
- package/dist/workstation/acp/outbox.js +12 -5
- package/dist/workstation/acp/patchAssets.js +40 -0
- package/dist/workstation/acp/permission.js +2 -2
- package/dist/workstation/acp/processor/checkpoint.js +26 -34
- package/dist/workstation/acp/processor/control.js +7 -80
- package/dist/workstation/acp/processor/jsonrpc.js +8 -0
- package/dist/workstation/acp/processor/runtime.js +5 -81
- package/dist/workstation/acp/processor/session.js +54 -82
- package/dist/workstation/acp/processor/state.js +7 -13
- package/dist/workstation/acp/processor/step.js +3 -20
- package/dist/workstation/acp/processor/test/fixtureLog.js +9 -16
- package/dist/workstation/acp/processor/transport/local.js +19 -3
- package/dist/workstation/acp/processor/updates.js +156 -105
- package/dist/workstation/acp/processorSession.js +20 -173
- package/dist/workstation/acp/registry/catalog.js +65 -39
- package/dist/workstation/acp/registry/launch.js +16 -1
- package/dist/workstation/acp/switching.js +0 -3
- package/dist/workstation/acp/test/fakeAgent.js +13 -0
- package/dist/workstation/acp/test/keeperWorld.js +32 -3
- package/dist/workstation/acp/test/raw/sources.js +16 -130
- package/dist/workstation/acp/test/realAdapter.js +1 -47
- package/dist/workstation/acp/test/scriptAgent.js +11 -3
- package/dist/workstation/agentFlow.js +1 -1
- package/dist/workstation/agentSessions.js +114 -166
- package/dist/workstation/channels/agent.js +9 -1
- package/dist/workstation/channels/computer.js +27 -4
- package/dist/workstation/channels/management.js +130 -15
- package/dist/workstation/channels/url.js +1 -1
- package/dist/workstation/config.js +33 -9
- package/dist/workstation/connection/encryptedChannel.js +4 -1
- package/dist/workstation/control/bindingStore.js +204 -0
- package/dist/workstation/control/commands.js +125 -0
- package/dist/workstation/control/daemonIdentity.js +143 -0
- package/dist/workstation/control/deviceCommands.js +191 -0
- package/dist/workstation/control/deviceProtocol.js +68 -0
- package/dist/workstation/control/enrollment.js +106 -0
- package/dist/workstation/control/inspect.js +71 -0
- package/dist/workstation/control/locks.js +110 -0
- package/dist/workstation/control/protocol.js +40 -0
- package/dist/workstation/control/restartGuard.js +46 -0
- package/dist/workstation/control/rootFormat.js +16 -0
- package/dist/workstation/control/runtimeClient.js +152 -0
- package/dist/workstation/control/runtimeHost.js +235 -0
- package/dist/workstation/control/serverIdentity.js +35 -0
- package/dist/workstation/control/updateTransition.js +91 -0
- package/dist/workstation/daemon.js +266 -264
- package/dist/workstation/daemonctl.js +226 -25
- package/dist/workstation/desktopControl/audio.js +99 -0
- package/dist/workstation/desktopControl/automationDevice.js +219 -0
- package/dist/workstation/desktopControl/automationJson.js +74 -0
- package/dist/workstation/desktopControl/automationLocalState.js +23 -0
- package/dist/workstation/desktopControl/automationProtocol.js +253 -0
- package/dist/workstation/desktopControl/automationSchema.generated.js +1 -0
- package/dist/workstation/desktopControl/automationSession.js +81 -0
- package/dist/workstation/desktopControl/automationTransport.js +138 -0
- package/dist/workstation/desktopControl/capabilities.js +93 -0
- package/dist/workstation/desktopControl/captureModes.js +9 -0
- package/dist/workstation/desktopControl/captureTargetLabel.js +9 -0
- package/dist/workstation/desktopControl/deviceAdmission.js +229 -0
- package/dist/workstation/desktopControl/deviceOwnership.js +130 -0
- package/dist/workstation/desktopControl/deviceSockets.js +246 -0
- package/dist/workstation/desktopControl/helperProcess.js +602 -0
- package/dist/workstation/desktopControl/hostGrant.js +65 -0
- package/dist/workstation/desktopControl/lobby.js +131 -0
- package/dist/workstation/desktopControl/macProvider.js +156 -0
- package/dist/workstation/desktopControl/media.js +331 -0
- package/dist/workstation/desktopControl/nativeDeviceAdmission.js +207 -0
- package/dist/workstation/desktopControl/nativeProvider.js +1044 -0
- package/dist/workstation/desktopControl/networkEvidence.js +43 -0
- package/dist/workstation/desktopControl/overview.js +58 -0
- package/dist/workstation/desktopControl/ownershipProtocol.js +31 -0
- package/dist/workstation/desktopControl/protocol.js +311 -0
- package/dist/workstation/desktopControl/provider.js +1 -0
- package/dist/workstation/desktopControl/quicMedia.js +240 -0
- package/dist/workstation/desktopControl/rateController.js +226 -0
- package/dist/workstation/desktopControl/supervisor.js +1774 -0
- package/dist/workstation/desktopControl/transport.js +27 -0
- package/dist/workstation/desktopControl/wallpaper.js +98 -0
- package/dist/workstation/exec/policy.js +5 -0
- package/dist/workstation/files/client.js +10 -1
- package/dist/workstation/files/credentials.js +142 -0
- package/dist/workstation/files/host.js +9 -2
- package/dist/workstation/files/text.js +42 -8
- package/dist/workstation/journal.js +5 -2
- package/dist/workstation/keepers/acp/client.js +38 -2
- package/dist/workstation/keepers/kit/buildVersion.js +35 -11
- package/dist/workstation/keepers/kit/bundle.js +21 -17
- package/dist/workstation/keepers/kit/client.js +2 -2
- package/dist/workstation/keepers/kit/launcher.js +123 -7
- package/dist/workstation/keepers/kit/paths.js +2 -1
- package/dist/workstation/keepers/legacy.js +16 -2
- package/dist/workstation/keepers/policy.js +5 -34
- package/dist/workstation/keepers/terminal/client.js +115 -23
- package/dist/workstation/keepers/terminal/exec.js +6 -3
- package/dist/workstation/keepers/terminal/frames.js +73 -0
- package/dist/workstation/keepers/terminal/generations.js +5 -8
- package/dist/workstation/keepers/terminal/gridProtocol.js +16 -0
- package/dist/workstation/keepers/terminal/input.js +146 -0
- package/dist/workstation/keepers/terminal/keeper.js +22 -8
- package/dist/workstation/keepers/terminal/main.js +1 -1
- package/dist/workstation/keepers/terminal/metadata.js +138 -0
- package/dist/workstation/keepers/terminal/protocol.js +4 -28
- package/dist/workstation/keepers/terminal/pty.js +1 -330
- package/dist/workstation/keepers/terminal/screen.js +246 -0
- package/dist/workstation/keepers/terminal/sessions.js +526 -0
- package/dist/workstation/managedInstall.js +57 -0
- package/dist/workstation/oauth/deviceAuthorization.js +31 -17
- package/dist/workstation/protocol/agent.js +2 -15
- package/dist/workstation/protocol/management.js +113 -5
- package/dist/workstation/protocol/runtimeDiagnostics.js +15 -0
- package/dist/workstation/rd/artifact.js +120 -0
- package/dist/workstation/rd/control.js +117 -0
- package/dist/workstation/rd/display.js +29 -0
- package/dist/workstation/rd/host.js +213 -0
- package/dist/workstation/rd/migrationReadiness.js +46 -0
- package/dist/workstation/rd/provider.js +256 -0
- package/dist/workstation/rd/sessions.js +391 -0
- package/dist/workstation/rd/state.js +57 -0
- package/dist/workstation/rd/update.js +168 -0
- package/dist/workstation/rd/upkeep.js +80 -0
- package/dist/workstation/register.js +27 -37
- package/dist/workstation/registrations/enrollment.js +250 -0
- package/dist/workstation/registrations/enrollmentPending.js +361 -0
- package/dist/workstation/registrations/migration.js +356 -0
- package/dist/workstation/registrations/model.js +34 -0
- package/dist/workstation/registrations/removal.js +222 -0
- package/dist/workstation/registrations/store.js +737 -0
- package/dist/workstation/runtime/desktopBinding.js +20 -0
- package/dist/workstation/runtime/deviceDesktop.js +193 -0
- package/dist/workstation/runtime/deviceRuntime.js +568 -0
- package/dist/workstation/runtime/deviceServices.js +117 -0
- package/dist/workstation/runtime/events.js +20 -0
- package/dist/workstation/runtime/ownership.js +100 -0
- package/dist/workstation/runtime/registrationRuntime.js +575 -0
- package/dist/workstation/runtime/remoteDesktopAdapter.js +186 -0
- package/dist/workstation/runtime/removalActivity.js +184 -0
- package/dist/workstation/service/installer.js +197 -14
- package/dist/workstation/service/launchd.js +2 -0
- package/dist/workstation/service/systemd.js +1 -0
- package/dist/workstation/storageMetrics.js +118 -0
- package/dist/workstation/systemMetrics.js +47 -92
- package/dist/workstation/toolchain.js +94 -0
- package/dist/workstation/tools/browser.js +28 -4
- package/dist/workstation/tools/client.js +8 -27
- package/dist/workstation/tools/info.js +6 -13
- package/dist/workstation/tools/terminalRouter.js +17 -172
- package/dist/workstation/tools/validation.js +55 -1
- package/dist/workstation/update/archiveEvidence.js +206 -0
- package/dist/workstation/update/artifact.js +2 -1
- package/dist/workstation/update/coordinator.js +34 -13
- package/dist/workstation/update/deviceCompatibility.js +119 -0
- package/dist/workstation/update/deviceCoordinator.js +369 -0
- package/dist/workstation/update/deviceHealth.js +104 -0
- package/dist/workstation/update/deviceHealthProtocol.js +26 -0
- package/dist/workstation/update/deviceRecoveryLifecycle.js +46 -0
- package/dist/workstation/update/deviceRollback.js +78 -0
- package/dist/workstation/update/globalInstall.js +186 -0
- package/dist/workstation/update/legacyBridge.js +414 -0
- package/dist/workstation/update/legacyOwner.js +39 -0
- package/dist/workstation/update/npmInstaller.js +51 -25
- package/dist/workstation/update/privateDurability.js +61 -0
- package/dist/workstation/update/rdRollback.js +145 -0
- package/dist/workstation/update/recoveryPlan.js +124 -0
- package/dist/workstation/update/runUpdate.js +10 -3
- package/dist/workstation/update/taskRegistry.js +161 -3
- package/dist/workstation/update/watchdog.js +29806 -333
- package/dist/workstation/update/watchdogProcess.js +72 -0
- package/package.json +11 -7
- 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 -61
- 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/runtime-deps/qoder-sdk/package-lock.json +0 -1189
- package/dist/runtime-deps/qoder-sdk/package.json +0 -8
- package/dist/workstation/acp/delegationMcp.js +0 -24
- package/dist/workstation/acp/processor/sideChannel.js +0 -80
- package/dist/workstation/acp/qoderStartup.js +0 -45
- package/dist/workstation/acp/test/raw/qoderCaptures.js +0 -125
- package/dist/workstation/qoderAdapter/backgroundProcess.js +0 -118
- package/dist/workstation/qoderAdapter/backgroundTerminal.js +0 -193
- package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +0 -372
- package/dist/workstation/qoderAdapter/callbackContext.js +0 -76
- package/dist/workstation/qoderAdapter/completedContinuation.js +0 -139
- package/dist/workstation/qoderAdapter/contract.js +0 -78
- package/dist/workstation/qoderAdapter/foregroundOutput.js +0 -36
- package/dist/workstation/qoderAdapter/history.js +0 -85
- package/dist/workstation/qoderAdapter/identityIndex.js +0 -191
- package/dist/workstation/qoderAdapter/inputQueue.js +0 -42
- package/dist/workstation/qoderAdapter/main.js +0 -95
- 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 -48
- package/dist/workstation/qoderAdapter/sdkRuntime.js +0 -93
- package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +0 -145
- package/dist/workstation/qoderAdapter/session.js +0 -319
- package/dist/workstation/qoderAdapter/subagentMeta.js +0 -28
- package/dist/workstation/qoderAdapter/subwork.js +0 -85
- package/dist/workstation/qoderAdapter/taskControl.js +0 -38
- package/dist/workstation/qoderAdapter/taskLedger.js +0 -163
- package/dist/workstation/qoderAdapter/usage.js +0 -37
- /package/dist/workstation/{qoderAdapter/types.js → desktopControl/automationPreparation.js} +0 -0
|
@@ -1,12 +1,28 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
|
+
import { BindingStore } from './control/bindingStore.js';
|
|
3
|
+
import { inspectRuntime, stopRuntime } from './control/runtimeClient.js';
|
|
4
|
+
import { LocalControlError } from './control/protocol.js';
|
|
5
|
+
import { normalizeWorkstationServer } from './control/serverIdentity.js';
|
|
2
6
|
import path from 'node:path';
|
|
3
7
|
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import {
|
|
8
|
+
import { randomUUID } from 'node:crypto';
|
|
9
|
+
import { clientDir, loadClientConfig, updateServiceContext } from './config.js';
|
|
5
10
|
import { createServiceManager } from './service/installer.js';
|
|
6
11
|
import { buildServicePath } from './service/types.js';
|
|
7
12
|
import { loginShellPath } from './service/loginShell.js';
|
|
8
13
|
import { KeeperLauncher } from './keepers/kit/launcher.js';
|
|
9
14
|
import { stopKeepers } from './keepers/stop.js';
|
|
15
|
+
import { assertRuntimeRestartSafe } from './control/restartGuard.js';
|
|
16
|
+
import { promises as fs } from 'node:fs';
|
|
17
|
+
import { inspectDeviceRuntime, prepareDeviceRuntimeStop, stopDeviceRuntime } from './control/runtimeClient.js';
|
|
18
|
+
import { assertDeviceUpdateTransitionSafe } from './control/daemonIdentity.js';
|
|
19
|
+
import { RegistrationStore } from './registrations/store.js';
|
|
20
|
+
import { readLegacyRegistrationSources, assertRegistrationEnrollmentAdmission } from './registrations/migration.js';
|
|
21
|
+
import { assertDeviceGuard } from './control/deviceProtocol.js';
|
|
22
|
+
import { resolveRegistrationOwnership } from './runtime/ownership.js';
|
|
23
|
+
import { captureStartupPackageEvidence } from './update/deviceHealth.js';
|
|
24
|
+
import { CLIENT_VERSION } from './version.js';
|
|
25
|
+
import { readDeviceBootstrapRecovery } from './update/recoveryPlan.js';
|
|
10
26
|
const __filename = fileURLToPath(import.meta.url);
|
|
11
27
|
/**
|
|
12
28
|
* The package's real npm bin entry (`package.json`'s `bin.axom`), given the directory a compiled
|
|
@@ -36,21 +52,27 @@ export function serviceContext() {
|
|
|
36
52
|
throw new Error('Axomwork Workstation user service supervision is supported only on macOS and Linux.');
|
|
37
53
|
}
|
|
38
54
|
const directory = path.dirname(__filename);
|
|
39
|
-
const
|
|
55
|
+
const managed = updateServiceContext();
|
|
56
|
+
const nodeDirectory = path.dirname(managed.nodePath);
|
|
40
57
|
return {
|
|
41
58
|
platform: process.platform,
|
|
42
59
|
username: os.userInfo().username,
|
|
43
60
|
userId: process.getuid?.() ?? os.userInfo().uid,
|
|
44
61
|
homeDir: os.homedir(),
|
|
45
|
-
nodePath:
|
|
62
|
+
nodePath: managed.nodePath,
|
|
46
63
|
cliPath: CLI_PATH,
|
|
47
|
-
npmPath:
|
|
64
|
+
npmPath: managed.npmPath,
|
|
48
65
|
// dist/workstation/ is nested under lib/node_modules/@platless/axom-cli; climb to npm's prefix.
|
|
49
|
-
npmPrefix:
|
|
66
|
+
npmPrefix: managed.npmPrefix,
|
|
50
67
|
configDir: clientDir(),
|
|
51
|
-
pathEnv: buildServicePath(process.env.PATH, nodeDirectory),
|
|
68
|
+
pathEnv: buildServicePath(managed.env?.PATH ?? process.env.PATH, nodeDirectory),
|
|
69
|
+
...(managed.managed ? { npmConfigPrefix: managed.managed.prefix } : {}),
|
|
52
70
|
};
|
|
53
71
|
}
|
|
72
|
+
async function installationServiceContext(base, shellPath = loginShellPath) {
|
|
73
|
+
const loginPath = await shellPath();
|
|
74
|
+
return loginPath ? { ...base, pathEnv: buildServicePath(`${loginPath}:${process.env.PATH ?? ''}`, path.dirname(base.nodePath)) } : base;
|
|
75
|
+
}
|
|
54
76
|
/** The workstation's keepers stopped (see {@link DaemonControllerDeps.stopKeepers}); an unregistered client has none. */
|
|
55
77
|
async function stopRegisteredKeepers() {
|
|
56
78
|
const configDir = clientDir();
|
|
@@ -62,6 +84,10 @@ async function stopRegisteredKeepers() {
|
|
|
62
84
|
});
|
|
63
85
|
}
|
|
64
86
|
const defaultDeps = {
|
|
87
|
+
identity: () => new BindingStore(clientDir()).inspect(),
|
|
88
|
+
runtime: async () => (await (await import('./control/inspect.js')).inspectWorkstation(clientDir())).runtime,
|
|
89
|
+
shutdown: generation => stopRuntime(clientDir(), generation),
|
|
90
|
+
restartSafe: generation => assertRuntimeRestartSafe(clientDir(), generation),
|
|
65
91
|
service: (context) => createServiceManager(context ?? serviceContext()),
|
|
66
92
|
config: () => loadClientConfig(clientDir()),
|
|
67
93
|
stopKeepers: stopRegisteredKeepers,
|
|
@@ -72,6 +98,22 @@ const defaultDeps = {
|
|
|
72
98
|
*/
|
|
73
99
|
export function createDaemonController(overrides = {}) {
|
|
74
100
|
const deps = { ...defaultDeps, ...overrides };
|
|
101
|
+
async function checked(options) {
|
|
102
|
+
const runtime = await deps.runtime();
|
|
103
|
+
if (runtime.state === 'unknown')
|
|
104
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'Cannot verify the running workstation');
|
|
105
|
+
if (options) {
|
|
106
|
+
const identity = await deps.identity();
|
|
107
|
+
if (identity.revision !== options.guard.revision || runtime.generation !== options.guard.runtimeGeneration)
|
|
108
|
+
throw new LocalControlError('BINDING_CONFLICT', 'Workstation changed; check it again');
|
|
109
|
+
}
|
|
110
|
+
if (runtime.state === 'running') {
|
|
111
|
+
const config = await deps.config();
|
|
112
|
+
if (!runtime.binding || runtime.binding.server !== normalizeWorkstationServer(config.server) || runtime.binding.workstationId !== config.workstationId)
|
|
113
|
+
throw new LocalControlError('INSTANCE_OCCUPIED', 'Another workstation binding is running');
|
|
114
|
+
}
|
|
115
|
+
return runtime;
|
|
116
|
+
}
|
|
75
117
|
return {
|
|
76
118
|
async install() {
|
|
77
119
|
// The definition is written from the login shell's PATH when one can be had, unioned with
|
|
@@ -79,11 +121,7 @@ export function createDaemonController(overrides = {}) {
|
|
|
79
121
|
// itself after an update they are not, and only the login shell knows where the user's CLIs
|
|
80
122
|
// are — see loginShellPath for why reusing the service's own PATH would make a machine
|
|
81
123
|
// reinstall its own blind spot.
|
|
82
|
-
const
|
|
83
|
-
const base = serviceContext();
|
|
84
|
-
const context = shellPath
|
|
85
|
-
? { ...base, pathEnv: buildServicePath(`${shellPath}:${process.env.PATH ?? ''}`, path.dirname(base.nodePath)) }
|
|
86
|
-
: base;
|
|
124
|
+
const context = await installationServiceContext(serviceContext());
|
|
87
125
|
await deps.service(context).install({ interactive: true });
|
|
88
126
|
},
|
|
89
127
|
// The workstation is stopped by the user: what its keepers host ends too (the daemon first, so nothing relaunches them).
|
|
@@ -94,28 +132,191 @@ export function createDaemonController(overrides = {}) {
|
|
|
94
132
|
finally {
|
|
95
133
|
await deps.stopKeepers();
|
|
96
134
|
} },
|
|
97
|
-
async start() { await
|
|
98
|
-
|
|
99
|
-
|
|
135
|
+
async start(options) { if ((await checked(options)).state === 'running')
|
|
136
|
+
return; await deps.service().start(); },
|
|
137
|
+
async stop(options) { const runtime = await checked(options); try {
|
|
138
|
+
if (runtime.mode === 'foreground' && runtime.generation)
|
|
139
|
+
await deps.shutdown(runtime.generation);
|
|
140
|
+
else
|
|
141
|
+
await deps.service().stop();
|
|
100
142
|
}
|
|
101
143
|
finally {
|
|
102
144
|
await deps.stopKeepers();
|
|
103
145
|
} },
|
|
104
146
|
// A restart keeps the keepers: terminals, commands and agent sessions go on under the next daemon.
|
|
105
|
-
async restart() {
|
|
147
|
+
async restart(options) {
|
|
148
|
+
const runtime = await checked(options);
|
|
149
|
+
if (runtime.state === 'running' && runtime.generation)
|
|
150
|
+
await deps.restartSafe(runtime.generation);
|
|
151
|
+
if (runtime.mode === 'foreground' && runtime.generation)
|
|
152
|
+
await deps.shutdown(runtime.generation);
|
|
153
|
+
await deps.service().restart();
|
|
154
|
+
},
|
|
155
|
+
async setEnabled(enabled, options) {
|
|
156
|
+
const runtime = await checked(options);
|
|
157
|
+
const service = deps.service();
|
|
158
|
+
if (!service.setEnabled)
|
|
159
|
+
throw new LocalControlError('UNSUPPORTED_PROTOCOL', 'Service enablement is unavailable');
|
|
160
|
+
if (enabled)
|
|
161
|
+
await service.setEnabled(true);
|
|
162
|
+
else {
|
|
163
|
+
try {
|
|
164
|
+
await service.setEnabled(false);
|
|
165
|
+
if (runtime.mode === 'foreground' && runtime.generation) {
|
|
166
|
+
const current = await deps.runtime();
|
|
167
|
+
if (current.state === 'running' && current.generation === runtime.generation)
|
|
168
|
+
await deps.shutdown(runtime.generation);
|
|
169
|
+
else if (current.state !== 'stopped')
|
|
170
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'The running workstation changed during shutdown');
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
finally {
|
|
174
|
+
await deps.stopKeepers();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
},
|
|
106
178
|
async status() {
|
|
179
|
+
const runtime = await deps.runtime();
|
|
180
|
+
if (runtime.state === 'running' && runtime.binding)
|
|
181
|
+
return { running: true, server: runtime.binding.server, workstationId: runtime.binding.workstationId };
|
|
182
|
+
if (runtime.state === 'unknown')
|
|
183
|
+
return { running: false, uncertain: true };
|
|
107
184
|
const state = await deps.service().status();
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
185
|
+
return state.running ? { running: true, uncertain: true } : { running: false };
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
export function createDeviceDaemonController(directory, overrides = {}) {
|
|
190
|
+
const context = overrides.context ?? (() => ({ ...serviceContext(), configDir: directory }));
|
|
191
|
+
const service = overrides.service ?? ((input) => createServiceManager(input ?? context()));
|
|
192
|
+
const store = new RegistrationStore(directory);
|
|
193
|
+
const runtime = async () => {
|
|
194
|
+
try {
|
|
195
|
+
return (await inspectDeviceRuntime(directory)).runtime;
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
const legacy = await inspectRuntime(directory);
|
|
199
|
+
if (legacy.state !== 'stopped')
|
|
200
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'The existing owner is not a verified device daemon');
|
|
201
|
+
return { state: 'stopped', generation: null, mode: 'unknown' };
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
const checked = async (options) => {
|
|
205
|
+
if (options) {
|
|
206
|
+
const inspection = await inspectDeviceRuntime(directory);
|
|
207
|
+
assertDeviceGuard(options.guard, inspection);
|
|
208
|
+
return inspection.runtime;
|
|
209
|
+
}
|
|
210
|
+
return runtime();
|
|
211
|
+
};
|
|
212
|
+
const terminateKeepers = async () => {
|
|
213
|
+
const inspection = await store.inspect();
|
|
214
|
+
if (!inspection.known || inspection.entries.some(entry => entry.state !== 'ready'))
|
|
215
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'Cannot verify every owned keeper namespace');
|
|
216
|
+
for (const registration of await store.list()) {
|
|
217
|
+
const sources = await readLegacyRegistrationSources(directory, registration.id);
|
|
218
|
+
const ownership = await resolveRegistrationOwnership(registration, sources);
|
|
219
|
+
const launcher = new KeeperLauncher({ configDir: registration.keeperConfigDirectory, workstationId: registration.workstationId, server: sources?.keeperServer ?? registration.server, ownership });
|
|
220
|
+
const reports = await stopKeepers(launcher);
|
|
221
|
+
if (reports.some(report => !report.result.stopped))
|
|
222
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'A workstation keeper has not stopped');
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
const finishPrepared = async (generation, mode) => {
|
|
226
|
+
const current = await runtime();
|
|
227
|
+
if (current.state === 'stopped')
|
|
228
|
+
return;
|
|
229
|
+
if (current.generation !== generation)
|
|
230
|
+
throw new LocalControlError('BINDING_CONFLICT', 'A successor device daemon replaced the prepared owner');
|
|
231
|
+
await stopDeviceRuntime(directory, generation, mode);
|
|
232
|
+
};
|
|
233
|
+
return {
|
|
234
|
+
async install(options) {
|
|
235
|
+
const installation = await installationServiceContext(context(), overrides.loginShellPath);
|
|
236
|
+
if (!path.isAbsolute(installation.cliPath) || path.resolve(installation.cliPath) !== installation.cliPath || path.basename(installation.cliPath) !== 'cli.js' || path.basename(path.dirname(installation.cliPath)) !== 'dist')
|
|
237
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'Installed service entrypoint must identify its exact dist/cli.js package');
|
|
238
|
+
const startup = await captureStartupPackageEvidence(path.dirname(path.dirname(installation.cliPath)), CLIENT_VERSION);
|
|
239
|
+
await assertDeviceUpdateTransitionSafe(directory, startup);
|
|
240
|
+
if (options) {
|
|
241
|
+
const recovery = await readDeviceBootstrapRecovery(directory, startup);
|
|
242
|
+
const transaction = recovery?.transaction;
|
|
243
|
+
if (!transaction || transaction.lifecycle !== 'provision-and-arm' || transaction.phase !== 'RESTARTING' || transaction.installation !== 'started' || transaction.installedRollback || transaction.recoveryRequired || transaction.initiatingDaemonGeneration !== options.updateGeneration)
|
|
244
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'Service replacement requires the original active device update handoff');
|
|
245
|
+
const current = await inspectDeviceRuntime(directory);
|
|
246
|
+
if (current.runtime.state !== 'running' || current.runtime.generation !== options.updateGeneration)
|
|
247
|
+
throw new LocalControlError('BINDING_CONFLICT', 'The original device update owner changed before service replacement');
|
|
248
|
+
}
|
|
249
|
+
await service(installation).install({ interactive: true });
|
|
112
250
|
},
|
|
251
|
+
async start(options) { if ((await checked(options)).state === 'running')
|
|
252
|
+
return; await assertDeviceUpdateTransitionSafe(directory); await assertRegistrationEnrollmentAdmission(directory); await service().start(); },
|
|
253
|
+
async stop(options) {
|
|
254
|
+
const current = await checked(options);
|
|
255
|
+
const manager = service();
|
|
256
|
+
if (current.generation && current.mode === 'launchd') {
|
|
257
|
+
await prepareDeviceRuntimeStop(directory, current.generation, 'terminate', options?.operationId ?? randomUUID());
|
|
258
|
+
await manager.stop();
|
|
259
|
+
await finishPrepared(current.generation, 'terminate');
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
if (current.generation)
|
|
263
|
+
await stopDeviceRuntime(directory, current.generation, 'terminate');
|
|
264
|
+
await manager.stop();
|
|
265
|
+
}
|
|
266
|
+
if (!current.generation)
|
|
267
|
+
await terminateKeepers();
|
|
268
|
+
},
|
|
269
|
+
async uninstall(options) {
|
|
270
|
+
const current = await checked(options);
|
|
271
|
+
const manager = service();
|
|
272
|
+
if (current.generation && current.mode === 'launchd') {
|
|
273
|
+
await prepareDeviceRuntimeStop(directory, current.generation, 'terminate', options?.operationId ?? randomUUID());
|
|
274
|
+
await manager.uninstall();
|
|
275
|
+
await finishPrepared(current.generation, 'terminate');
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
if (current.generation)
|
|
279
|
+
await stopDeviceRuntime(directory, current.generation, 'terminate');
|
|
280
|
+
await manager.uninstall();
|
|
281
|
+
}
|
|
282
|
+
if (!current.generation)
|
|
283
|
+
await terminateKeepers();
|
|
284
|
+
},
|
|
285
|
+
async restart(options) {
|
|
286
|
+
const current = await checked(options);
|
|
287
|
+
const manager = service();
|
|
288
|
+
if (current.generation) {
|
|
289
|
+
await (overrides.restartSafe ?? (generation => assertRuntimeRestartSafe(directory, generation)))(current.generation);
|
|
290
|
+
if (current.mode === 'launchd') {
|
|
291
|
+
await prepareDeviceRuntimeStop(directory, current.generation, 'detach', options?.operationId ?? randomUUID());
|
|
292
|
+
await manager.stop();
|
|
293
|
+
await finishPrepared(current.generation, 'detach');
|
|
294
|
+
await manager.restart();
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
await stopDeviceRuntime(directory, current.generation, 'detach');
|
|
298
|
+
}
|
|
299
|
+
await manager.restart();
|
|
300
|
+
},
|
|
301
|
+
async status() { const current = await runtime(); return { running: current.state === 'running' }; },
|
|
113
302
|
};
|
|
114
303
|
}
|
|
304
|
+
async function deviceControllerSelected() {
|
|
305
|
+
for (const name of ['device.json', 'legacy-migration.json'])
|
|
306
|
+
try {
|
|
307
|
+
await fs.lstat(path.join(clientDir(), name));
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
catch (error) {
|
|
311
|
+
if (error.code !== 'ENOENT')
|
|
312
|
+
throw error;
|
|
313
|
+
}
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
115
316
|
const controller = createDaemonController();
|
|
116
|
-
export const install = () => controller.install();
|
|
117
|
-
export const uninstall = () => controller.uninstall();
|
|
118
|
-
export const start = () => controller.start();
|
|
119
|
-
export const stop = () => controller.stop();
|
|
120
|
-
export const restart = () => controller.restart();
|
|
121
|
-
export const status = () => controller.status();
|
|
317
|
+
export const install = async () => (await deviceControllerSelected() ? createDeviceDaemonController(clientDir()) : controller).install();
|
|
318
|
+
export const uninstall = async () => (await deviceControllerSelected() ? createDeviceDaemonController(clientDir()) : controller).uninstall();
|
|
319
|
+
export const start = async () => (await deviceControllerSelected() ? createDeviceDaemonController(clientDir()) : controller).start();
|
|
320
|
+
export const stop = async () => (await deviceControllerSelected() ? createDeviceDaemonController(clientDir()) : controller).stop();
|
|
321
|
+
export const restart = async () => (await deviceControllerSelected() ? createDeviceDaemonController(clientDir()) : controller).restart();
|
|
322
|
+
export const status = async () => (await deviceControllerSelected() ? createDeviceDaemonController(clientDir()) : controller).status();
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const audioMode = z.enum(['off', 'auto', 'high', 'low']);
|
|
3
|
+
export const AUDIO_HEADER_BYTES = 40;
|
|
4
|
+
export const AUDIO_MAX_PAYLOAD = 1000;
|
|
5
|
+
export const audioConfiguration = z.strictObject({
|
|
6
|
+
commandId: z.string().regex(/^[0-9a-f]{32}$/), audioEpoch: z.number().int().min(1).max(0xffffffff),
|
|
7
|
+
sampleRate: z.literal(48000), channels: z.literal(2), framesPerPacket: z.literal(960),
|
|
8
|
+
bitrate: z.number().int().min(64000).max(256000), lookaheadFrames: z.number().int().min(0).max(960),
|
|
9
|
+
});
|
|
10
|
+
function requireAudio(value) {
|
|
11
|
+
if (!value)
|
|
12
|
+
throw new Error('invalid_audio');
|
|
13
|
+
}
|
|
14
|
+
export function decodeAudio(bytes) {
|
|
15
|
+
requireAudio(bytes.length >= 41 && bytes.length <= 1040 && bytes.subarray(0, 4).equals(Buffer.from('AXDA')));
|
|
16
|
+
requireAudio(bytes[4] === 1 && bytes[5] === 0 && bytes.readUInt16BE(6) === 40 &&
|
|
17
|
+
bytes.readUInt16BE(32) === 960 && bytes[34] === 2 && bytes[35] === 0 &&
|
|
18
|
+
bytes.readUInt16BE(38) === 0 && bytes.readUInt16BE(36) === bytes.length - 40);
|
|
19
|
+
const packet = { epoch: bytes.readUInt32BE(8), sequence: bytes.readUInt32BE(12),
|
|
20
|
+
firstSample: Number(bytes.readBigUInt64BE(16)), captureTimestampUs: Number(bytes.readBigUInt64BE(24)),
|
|
21
|
+
payload: bytes.subarray(40) };
|
|
22
|
+
validate(packet);
|
|
23
|
+
return packet;
|
|
24
|
+
}
|
|
25
|
+
function validate(packet) {
|
|
26
|
+
requireAudio(Number.isInteger(packet.epoch) && packet.epoch >= 1 && packet.epoch <= 0xffffffff &&
|
|
27
|
+
Number.isInteger(packet.sequence) && packet.sequence >= 1 && packet.sequence <= 0xffffffff &&
|
|
28
|
+
Number.isSafeInteger(packet.firstSample) && packet.firstSample === (packet.sequence - 1) * 960 &&
|
|
29
|
+
Number.isSafeInteger(packet.captureTimestampUs) && packet.captureTimestampUs >= 0 &&
|
|
30
|
+
packet.payload.length >= 1 && packet.payload.length <= AUDIO_MAX_PAYLOAD);
|
|
31
|
+
}
|
|
32
|
+
export function encodeAudio(packet) {
|
|
33
|
+
validate(packet);
|
|
34
|
+
const bytes = Buffer.alloc(AUDIO_HEADER_BYTES + packet.payload.length);
|
|
35
|
+
bytes.write('AXDA');
|
|
36
|
+
bytes[4] = 1;
|
|
37
|
+
bytes.writeUInt16BE(40, 6);
|
|
38
|
+
bytes.writeUInt32BE(packet.epoch, 8);
|
|
39
|
+
bytes.writeUInt32BE(packet.sequence, 12);
|
|
40
|
+
bytes.writeBigUInt64BE(BigInt(packet.firstSample), 16);
|
|
41
|
+
bytes.writeBigUInt64BE(BigInt(packet.captureTimestampUs), 24);
|
|
42
|
+
bytes.writeUInt16BE(960, 32);
|
|
43
|
+
bytes[34] = 2;
|
|
44
|
+
bytes.writeUInt16BE(packet.payload.length, 36);
|
|
45
|
+
packet.payload.copy(bytes, 40);
|
|
46
|
+
return bytes;
|
|
47
|
+
}
|
|
48
|
+
export class AudioRateController {
|
|
49
|
+
mode;
|
|
50
|
+
ladder = [64000, 96000, 128000, 192000, 256000];
|
|
51
|
+
current = 128000;
|
|
52
|
+
bad = 0;
|
|
53
|
+
healthy = 0;
|
|
54
|
+
lastObservation = -Infinity;
|
|
55
|
+
lastDown = -Infinity;
|
|
56
|
+
lastUp = -Infinity;
|
|
57
|
+
constructor(mode = 'off') {
|
|
58
|
+
this.mode = mode;
|
|
59
|
+
}
|
|
60
|
+
get bitrate() {
|
|
61
|
+
return this.mode === 'off' ? 0 : this.mode === 'high' ? 256000 : this.mode === 'low' ? 64000 : this.current;
|
|
62
|
+
}
|
|
63
|
+
select(mode) {
|
|
64
|
+
audioMode.parse(mode);
|
|
65
|
+
if (mode !== this.mode) {
|
|
66
|
+
this.current = mode === 'auto' ? 128000 : this.current;
|
|
67
|
+
this.bad = this.healthy = 0;
|
|
68
|
+
this.lastObservation = this.lastDown = this.lastUp = -Infinity;
|
|
69
|
+
}
|
|
70
|
+
this.mode = mode;
|
|
71
|
+
return this.bitrate;
|
|
72
|
+
}
|
|
73
|
+
observe(now, feedback) {
|
|
74
|
+
if (this.mode !== 'auto' || !Number.isFinite(now) || now - this.lastObservation < 500)
|
|
75
|
+
return this.bitrate;
|
|
76
|
+
if (![feedback.queueMs, feedback.lateRatio, feedback.lostRatio, feedback.underruns].every(Number.isFinite) ||
|
|
77
|
+
feedback.queueMs < 0 || feedback.lateRatio < 0 || feedback.lateRatio > 1 ||
|
|
78
|
+
feedback.lostRatio < 0 || feedback.lostRatio > 1 || feedback.underruns < 0)
|
|
79
|
+
return this.bitrate;
|
|
80
|
+
this.lastObservation = now;
|
|
81
|
+
const adverse = feedback.congested || feedback.queueMs > 40 || feedback.lateRatio > 0.02 || feedback.lostRatio > 0.03;
|
|
82
|
+
const recovered = !adverse && feedback.headroom && feedback.queueMs < 15 &&
|
|
83
|
+
feedback.lateRatio < 0.005 && feedback.lostRatio < 0.005 && feedback.underruns === 0;
|
|
84
|
+
this.bad = adverse ? this.bad + 1 : 0;
|
|
85
|
+
this.healthy = recovered ? this.healthy + 1 : 0;
|
|
86
|
+
const index = this.ladder.indexOf(this.current);
|
|
87
|
+
if (this.bad >= 2 && now - this.lastDown >= 1000 && index > 0) {
|
|
88
|
+
this.current = this.ladder[index - 1];
|
|
89
|
+
this.bad = this.healthy = 0;
|
|
90
|
+
this.lastDown = now;
|
|
91
|
+
}
|
|
92
|
+
else if (this.healthy >= 10 && now - Math.max(this.lastUp, this.lastDown) >= 5000 && index < this.ladder.length - 1) {
|
|
93
|
+
this.current = this.ladder[index + 1];
|
|
94
|
+
this.bad = this.healthy = 0;
|
|
95
|
+
this.lastUp = now;
|
|
96
|
+
}
|
|
97
|
+
return this.bitrate;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import WebSocket from 'ws';
|
|
2
|
+
import { automationSchema, decodeAutomationRequest, decodeAutomationResponse, encodeAutomationBinary } from './automationProtocol.js';
|
|
3
|
+
import { parseAutomationJson } from './automationJson.js';
|
|
4
|
+
import { desktopSocketUrl } from './deviceSockets.js';
|
|
5
|
+
import { bounded, NativeCommandError } from './helperProcess.js';
|
|
6
|
+
import { managedAutomationBinding } from './automationLocalState.js';
|
|
7
|
+
export class AutomationDeviceConnections {
|
|
8
|
+
server;
|
|
9
|
+
provider;
|
|
10
|
+
allowPrivateWs;
|
|
11
|
+
localStateDirectory;
|
|
12
|
+
admission;
|
|
13
|
+
get activeCount() { return this.active.size; }
|
|
14
|
+
active = new Map();
|
|
15
|
+
generation;
|
|
16
|
+
constructor(server, provider, allowPrivateWs = false, localStateDirectory, admission) {
|
|
17
|
+
this.server = server;
|
|
18
|
+
this.provider = provider;
|
|
19
|
+
this.allowPrivateWs = allowPrivateWs;
|
|
20
|
+
this.localStateDirectory = localStateDirectory;
|
|
21
|
+
this.admission = admission;
|
|
22
|
+
}
|
|
23
|
+
ready(generation) { this.close(); this.generation = generation; }
|
|
24
|
+
retired(generation) { if (generation === this.generation) {
|
|
25
|
+
this.close();
|
|
26
|
+
this.generation = undefined;
|
|
27
|
+
} }
|
|
28
|
+
close() { for (const entry of this.active.values())
|
|
29
|
+
entry.close(); this.active.clear(); }
|
|
30
|
+
async prepare(grant, binding, generation) {
|
|
31
|
+
if (generation !== this.generation || binding.route.deviceGeneration !== generation || this.active.size >= 8 || this.active.has(grant))
|
|
32
|
+
throw new Error('stale_generation');
|
|
33
|
+
if (!this.localStateDirectory)
|
|
34
|
+
throw new Error('trusted_storage_unavailable');
|
|
35
|
+
const localBinding = await managedAutomationBinding(this.localStateDirectory, { ...binding, registrationId: this.admission?.registrationId ?? binding.registrationId });
|
|
36
|
+
if (generation !== this.generation || this.active.size >= 8 || this.active.has(grant))
|
|
37
|
+
throw new Error('stale_generation');
|
|
38
|
+
const socket = new WebSocket(desktopSocketUrl(this.server, 'automation', this.allowPrivateWs), {
|
|
39
|
+
headers: { 'X-Axom-Automation-Grant': grant, 'X-Axom-Automation-Features': 'capture_wait_v1' }, followRedirects: false, perMessageDeflate: false, maxPayload: 49152
|
|
40
|
+
});
|
|
41
|
+
let selectedWait = false;
|
|
42
|
+
socket.once('upgrade', response => {
|
|
43
|
+
const selected = response.headers['x-axom-automation-features'];
|
|
44
|
+
if (selected !== undefined && selected !== 'capture_wait_v1') {
|
|
45
|
+
socket.terminate();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
selectedWait = selected === 'capture_wait_v1';
|
|
49
|
+
});
|
|
50
|
+
let session, retired = false, leaseBusy = false;
|
|
51
|
+
let mode = 'live';
|
|
52
|
+
let statusRequest;
|
|
53
|
+
const pending = new Map();
|
|
54
|
+
let lease;
|
|
55
|
+
const close = () => {
|
|
56
|
+
if (retired)
|
|
57
|
+
return;
|
|
58
|
+
retired = true;
|
|
59
|
+
clearInterval(lease);
|
|
60
|
+
pending.forEach(controller => controller.abort());
|
|
61
|
+
pending.clear();
|
|
62
|
+
this.active.delete(grant);
|
|
63
|
+
socket.terminate();
|
|
64
|
+
void session?.dispose().catch(() => { });
|
|
65
|
+
};
|
|
66
|
+
const current = () => !retired && this.generation === generation;
|
|
67
|
+
this.active.set(grant, { generation, close });
|
|
68
|
+
socket.on('error', close);
|
|
69
|
+
socket.on('close', close);
|
|
70
|
+
const send = async (bytes) => {
|
|
71
|
+
if (!current() || socket.readyState !== WebSocket.OPEN || socket.bufferedAmount > 8388608)
|
|
72
|
+
throw new Error('transport_failed');
|
|
73
|
+
await bounded(new Promise((resolve, reject) => socket.send(bytes, error => error ? reject(error) : resolve())), 5000);
|
|
74
|
+
};
|
|
75
|
+
try {
|
|
76
|
+
await bounded(new Promise((resolve, reject) => { socket.once('open', resolve); socket.once('error', reject); socket.once('close', () => reject(new Error('closed'))); }), 3000);
|
|
77
|
+
if (!this.provider?.prepareAutomation) {
|
|
78
|
+
await send(JSON.stringify({ automationProtocol: 1, operations: [] }));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
session = await this.provider.prepareAutomation(localBinding);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
await send(JSON.stringify({ automationProtocol: 1, operations: [] }));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (!current()) {
|
|
89
|
+
await session.dispose();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
await send(JSON.stringify({ automationProtocol: 1, operations: session.operations.filter(method => Object.hasOwn(automationSchema.requests, method) && (method !== 'capture_wait_v1' || selectedWait)) }));
|
|
93
|
+
const backend = session;
|
|
94
|
+
lease = setInterval(() => {
|
|
95
|
+
if (leaseBusy || !current() || mode !== 'live')
|
|
96
|
+
return;
|
|
97
|
+
leaseBusy = true;
|
|
98
|
+
if (this.admission && !this.admission.current(localBinding)) {
|
|
99
|
+
close();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
void backend.renewAuthenticatedLease(3000).catch(() => { if (mode === 'live')
|
|
103
|
+
close(); }).finally(() => { leaseBusy = false; });
|
|
104
|
+
}, 1000);
|
|
105
|
+
socket.on('message', (bytes, binary) => {
|
|
106
|
+
if (!current() || binary) {
|
|
107
|
+
close();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
const text = bytes.toString(), raw = parseAutomationJson(text, 49152).value;
|
|
112
|
+
if (raw.command === 'cancel' && Object.keys(raw).length === 2) {
|
|
113
|
+
pending.get(raw.requestId)?.abort();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (raw.command === 'status_only' || raw.command === 'isolate') {
|
|
117
|
+
if (pending.size >= 64 || pending.has(raw.requestId)) {
|
|
118
|
+
close();
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
mode = 'detaching';
|
|
122
|
+
clearInterval(lease);
|
|
123
|
+
const controller = new AbortController();
|
|
124
|
+
pending.set(raw.requestId, controller);
|
|
125
|
+
const operation = raw.command === 'status_only' ? backend.detachApplicationStatus?.(raw.launchRequestId, raw.canonicalArgsDigest) : backend.isolate?.();
|
|
126
|
+
void (operation ?? Promise.reject(new Error('unsupported'))).then(async (result) => {
|
|
127
|
+
if (raw.command === 'status_only') {
|
|
128
|
+
mode = 'status_only';
|
|
129
|
+
statusRequest = raw.launchRequestId;
|
|
130
|
+
}
|
|
131
|
+
if (current() && !controller.signal.aborted)
|
|
132
|
+
await send(JSON.stringify({ control: raw.command, id: raw.requestId, result }));
|
|
133
|
+
}, async (error) => {
|
|
134
|
+
if (current() && !controller.signal.aborted)
|
|
135
|
+
await send(JSON.stringify({ control: 'prepare_failed', id: raw.requestId, code: error instanceof Error ? error.message : 'service_unavailable', detail: null }));
|
|
136
|
+
}).catch(close).finally(() => pending.delete(raw.requestId));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (raw.command === 'prepare' && Object.keys(raw).length === 3) {
|
|
140
|
+
if (this.admission && !this.admission.current(localBinding))
|
|
141
|
+
throw new Error('control_lost');
|
|
142
|
+
if (mode !== 'live') {
|
|
143
|
+
void send(JSON.stringify({ control: 'prepare_failed', id: raw.requestId, code: 'control_lost', detail: null })).catch(close);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (pending.size >= 64 || pending.has(raw.requestId)) {
|
|
147
|
+
close();
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const controller = new AbortController();
|
|
151
|
+
pending.set(raw.requestId, controller);
|
|
152
|
+
void backend.prepareAttachment(raw.preparation, { signal: controller.signal }).then(attachment => { if (current() && !controller.signal.aborted)
|
|
153
|
+
return send(JSON.stringify({ control: 'prepared', id: raw.requestId, attachment })); }, error => { if (current() && !controller.signal.aborted)
|
|
154
|
+
return send(JSON.stringify({ control: 'prepare_failed', id: raw.requestId, code: error instanceof Error ? error.message : 'service_unavailable', detail: error instanceof NativeCommandError ? error.detail : null })); }).catch(close).finally(() => pending.delete(raw.requestId));
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const request = decodeAutomationRequest(text);
|
|
158
|
+
if (this.admission && !this.admission.current(localBinding) && request.method !== 'close')
|
|
159
|
+
throw new Error('control_lost');
|
|
160
|
+
if (mode !== 'live' && (mode !== 'status_only' || request.method !== 'application_status' || request.payload.requestId !== statusRequest)) {
|
|
161
|
+
void send(JSON.stringify({ ...request, payload: { type: 'failure', code: 'control_lost', message: 'Input authority was retired', detail: null } })).catch(close);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (pending.size >= 64 || pending.has(request.requestId)) {
|
|
165
|
+
close();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (request.method === 'capture_wait_v1' && (!selectedWait || !backend.operations.includes(request.method))) {
|
|
169
|
+
void send(JSON.stringify({ ...request, payload: { type: 'failure', code: 'unsupported', message: 'Conditional observation was not negotiated', detail: null } })).catch(close);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const controller = new AbortController();
|
|
173
|
+
pending.set(request.requestId, controller);
|
|
174
|
+
void (async () => {
|
|
175
|
+
const reply = await backend.execute(request, { signal: controller.signal });
|
|
176
|
+
if (!current() || controller.signal.aborted)
|
|
177
|
+
return;
|
|
178
|
+
let response = decodeAutomationResponse(JSON.stringify(reply.response), request);
|
|
179
|
+
let binary;
|
|
180
|
+
if (reply.image) {
|
|
181
|
+
const data = response.payload.data, transfer = request.method === 'capture' ? data : request.method === 'capture_wait_v1' ? data.image : data.postImage;
|
|
182
|
+
binary = encodeAutomationBinary(transfer.header, reply.image);
|
|
183
|
+
const captureAgeMs = Date.now() - transfer.metadata.capturedAtEpochMs;
|
|
184
|
+
if (!Number.isSafeInteger(captureAgeMs) || captureAgeMs < 0 || captureAgeMs > 30000) {
|
|
185
|
+
if (request.method !== 'act' && request.method !== 'status')
|
|
186
|
+
throw new Error('stale_observation');
|
|
187
|
+
response = { ...response, payload: { type: 'success', data: { ...data, postImage: null, captureError: 'capture_failed' } } };
|
|
188
|
+
binary = undefined;
|
|
189
|
+
}
|
|
190
|
+
else
|
|
191
|
+
response = { ...response, captureAgeMs };
|
|
192
|
+
}
|
|
193
|
+
await send(JSON.stringify(response));
|
|
194
|
+
if (binary)
|
|
195
|
+
await send(binary);
|
|
196
|
+
})().catch(async (error) => {
|
|
197
|
+
if (!current() || controller.signal.aborted)
|
|
198
|
+
return;
|
|
199
|
+
const code = error instanceof Error && error.message === 'stale_observation' ? 'stale_observation'
|
|
200
|
+
: error instanceof Error && error.message === 'unsupported' ? 'unsupported' : 'service_unavailable';
|
|
201
|
+
try {
|
|
202
|
+
await send(JSON.stringify({ ...request, payload: { type: 'failure', code, message: error instanceof Error ? error.message : code, detail: null } }));
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
close();
|
|
206
|
+
}
|
|
207
|
+
}).finally(() => pending.delete(request.requestId));
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
close();
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
close();
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|