@platless/axom-cli 0.6.36 → 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 +7 -1
- package/dist/axom-patch/cli/README.md +31 -2
- package/dist/axom-patch/cli/account-limits/generate.py +2 -2
- package/dist/axom-patch/cli/claude-replay-lifecycle.json +16 -0
- package/dist/axom-patch/cli/codex-terminal-output.json +2 -2
- package/dist/axom-patch/cli/probes/claude-replay-lifecycle.mjs +31 -0
- package/dist/axom-patch/cli/probes/codex-terminal-output.mjs +1 -1
- 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 +137 -41
- package/dist/axom-patch/cli/qoder/releases/1.1.75.json +5 -88
- package/dist/axom-patch/cli/qoder-native-acp.json +23 -23
- package/dist/axom-patch/cli/runtime.json +3 -1
- 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 +7 -7
- package/dist/axom-patch/cli/zcode-env-account.json +58 -0
- package/dist/axom-patch/cli/zcode-models.json +9 -9
- package/dist/axom-patch/cli/zcode-subagent-subwork.json +10 -10
- package/dist/cli.js +9 -11
- package/dist/commands/auth.js +4 -1
- package/dist/commands/update.js +68 -7
- package/dist/commands/workstation.js +94 -223
- 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 +2 -2
- package/dist/keepers/terminal/keeper.mjs +1089 -673
- package/dist/keepers/terminal/manifest.json +4 -4
- package/dist/native/darwin-x64/axom-quic +0 -0
- package/dist/workstation/acp/acpAgents.js +11 -4
- package/dist/workstation/acp/outbox.js +12 -5
- package/dist/workstation/acp/permission.js +2 -2
- package/dist/workstation/acp/processor/checkpoint.js +4 -3
- package/dist/workstation/acp/processor/control.js +2 -2
- package/dist/workstation/acp/processor/jsonrpc.js +8 -0
- package/dist/workstation/acp/processor/session.js +3 -2
- package/dist/workstation/acp/processor/state.js +1 -0
- package/dist/workstation/acp/processor/transport/local.js +19 -3
- package/dist/workstation/acp/processor/updates.js +25 -0
- package/dist/workstation/acp/processorSession.js +12 -1
- package/dist/workstation/acp/registry/catalog.js +6 -6
- package/dist/workstation/acp/test/keeperWorld.js +4 -3
- package/dist/workstation/agentSessions.js +76 -23
- package/dist/workstation/channels/agent.js +9 -1
- package/dist/workstation/channels/computer.js +27 -4
- package/dist/workstation/channels/management.js +36 -5
- package/dist/workstation/config.js +18 -6
- package/dist/workstation/control/bindingStore.js +4 -0
- package/dist/workstation/control/commands.js +1 -0
- package/dist/workstation/control/daemonIdentity.js +90 -0
- package/dist/workstation/control/deviceCommands.js +191 -0
- package/dist/workstation/control/deviceProtocol.js +68 -0
- package/dist/workstation/control/locks.js +51 -4
- package/dist/workstation/control/rootFormat.js +16 -0
- package/dist/workstation/control/runtimeClient.js +60 -5
- package/dist/workstation/control/runtimeHost.js +96 -9
- package/dist/workstation/control/serverIdentity.js +2 -2
- package/dist/workstation/control/updateTransition.js +91 -0
- package/dist/workstation/daemon.js +262 -360
- package/dist/workstation/daemonctl.js +150 -12
- 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/automationPreparation.js +1 -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 +2 -1
- package/dist/workstation/desktopControl/deviceAdmission.js +229 -0
- package/dist/workstation/desktopControl/deviceOwnership.js +130 -0
- package/dist/workstation/desktopControl/deviceSockets.js +2 -2
- package/dist/workstation/desktopControl/helperProcess.js +250 -1
- package/dist/workstation/desktopControl/hostGrant.js +65 -0
- package/dist/workstation/desktopControl/lobby.js +4 -2
- package/dist/workstation/desktopControl/macProvider.js +41 -40
- package/dist/workstation/desktopControl/nativeDeviceAdmission.js +207 -0
- package/dist/workstation/desktopControl/nativeProvider.js +31 -5
- package/dist/workstation/desktopControl/ownershipProtocol.js +31 -0
- package/dist/workstation/desktopControl/protocol.js +15 -6
- package/dist/workstation/desktopControl/supervisor.js +110 -17
- package/dist/workstation/desktopControl/wallpaper.js +26 -6
- package/dist/workstation/exec/policy.js +5 -0
- package/dist/workstation/files/client.js +10 -1
- package/dist/workstation/files/host.js +9 -2
- 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/client.js +2 -2
- package/dist/workstation/keepers/kit/launcher.js +80 -4
- 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/oauth/deviceAuthorization.js +31 -17
- package/dist/workstation/protocol/management.js +23 -4
- package/dist/workstation/rd/artifact.js +4 -3
- package/dist/workstation/rd/control.js +3 -5
- package/dist/workstation/rd/host.js +213 -0
- package/dist/workstation/rd/migrationReadiness.js +46 -0
- package/dist/workstation/rd/provider.js +44 -10
- package/dist/workstation/rd/state.js +14 -2
- package/dist/workstation/rd/update.js +5 -8
- package/dist/workstation/rd/upkeep.js +44 -2
- 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 +93 -1
- package/dist/workstation/service/launchd.js +1 -0
- package/dist/workstation/service/systemd.js +1 -0
- package/dist/workstation/systemMetrics.js +3 -0
- 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/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 +80 -10
- package/dist/workstation/update/legacyBridge.js +414 -0
- package/dist/workstation/update/legacyOwner.js +39 -0
- package/dist/workstation/update/npmInstaller.js +2 -2
- package/dist/workstation/update/privateDurability.js +61 -0
- package/dist/workstation/update/recoveryPlan.js +124 -0
- package/dist/workstation/update/runUpdate.js +7 -1
- package/dist/workstation/update/taskRegistry.js +161 -3
- package/dist/workstation/update/watchdog.js +27427 -14410
- package/dist/workstation/update/watchdogProcess.js +72 -0
- package/package.json +7 -3
- package/dist/native/darwin-arm64/axom-quic +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { decodeDesktopManagementFrame, parseDesktopJson, DesktopProtocolError } from '../desktopControl/protocol.js';
|
|
2
|
+
import { parseAutomationJson } from '../desktopControl/automationJson.js';
|
|
2
3
|
import { decodeChannelFrame, hasOptional, hasRequired, isBoolean, isBridgeInfoArray, isNullableString, isNumber, isObject, isString, } from './common.js';
|
|
3
4
|
export const MANAGEMENT_WIRE_MIN_VERSION = 1;
|
|
4
5
|
/** Adds the `DRAINING` value to ClientUpdatePhase. */
|
|
@@ -14,7 +15,8 @@ export const MANAGEMENT_WIRE_GPU_VERSION = 7;
|
|
|
14
15
|
/** Adds `keepers` to heartbeats and the `keeper_control` / `keeper_control_result` pair (spec §8.1, Task 26). */
|
|
15
16
|
export const MANAGEMENT_WIRE_KEEPERS_VERSION = 8;
|
|
16
17
|
export const MANAGEMENT_WIRE_DESKTOP_VERSION = 9;
|
|
17
|
-
export const
|
|
18
|
+
export const MANAGEMENT_WIRE_DEVICE_OWNERSHIP_VERSION = 10;
|
|
19
|
+
export const MANAGEMENT_WIRE_VERSION = MANAGEMENT_WIRE_DEVICE_OWNERSHIP_VERSION;
|
|
18
20
|
/** Bounds of one heartbeat's `keepers` (spec §8.1). */
|
|
19
21
|
export const KEEPER_STATUS_MAX_ITEMS = 32;
|
|
20
22
|
export const KEEPER_LABEL_MAX_CHARS = 64;
|
|
@@ -59,6 +61,7 @@ function isUpdateStatus(value) {
|
|
|
59
61
|
&& hasOptional(value, {
|
|
60
62
|
commandId: isString, version: isString, releaseSha512: isString, force: isBoolean,
|
|
61
63
|
activeShellTasks: isNumber, activeAgentTasks: isNumber, error: isString,
|
|
64
|
+
remoteForcePolicy: policy => policy === 'drain-only',
|
|
62
65
|
});
|
|
63
66
|
}
|
|
64
67
|
function isOsInfo(value) {
|
|
@@ -130,7 +133,7 @@ function isManagementFrame(value) {
|
|
|
130
133
|
}) && hasOptional(value, { bridges: isBridgeInfoArray });
|
|
131
134
|
case 'management_ready': return hasRequired(value, {
|
|
132
135
|
serverNonce: isString, generation: isString, protocols: isSelectedProtocolMap,
|
|
133
|
-
}) && hasOptional(value, { release: isNullable(isRelease) });
|
|
136
|
+
}) && hasOptional(value, { release: isNullable(isRelease), ownerUserId: isString });
|
|
134
137
|
case 'heartbeat': return hasRequired(value, {
|
|
135
138
|
seq: isNumber, sentAtEpochMs: isNumber, clientVersion: isString,
|
|
136
139
|
channels: (channels) => isObject(channels) && Object.entries(channels).every(([kind, state]) => CHANNEL_KINDS.has(kind) && isChannelState(state)),
|
|
@@ -148,7 +151,7 @@ function isManagementFrame(value) {
|
|
|
148
151
|
case 'heartbeat_ack': return hasRequired(value, { seq: isNumber });
|
|
149
152
|
case 'channel_ticket_request': return hasRequired(value, { channel: isChannelKind });
|
|
150
153
|
case 'channel_ticket': return hasRequired(value, { channel: isChannelKind, ticket: isString, expiresAtEpochMs: isNumber });
|
|
151
|
-
case 'client_update': return hasRequired(value, { commandId: isString, release: isRelease, force: isBoolean });
|
|
154
|
+
case 'client_update': return hasRequired(value, { commandId: isString, release: isRelease, force: isBoolean }) && hasOptional(value, { remoteForcePolicy: value => value === 'drain-only' });
|
|
152
155
|
case 'client_update_status': return hasRequired(value, { status: isUpdateStatus });
|
|
153
156
|
case 'external_agents_scan': return hasRequired(value, { commandId: isString });
|
|
154
157
|
case 'external_agents_scan_result': return hasRequired(value, { commandId: isString, bridges: isBridgeInfoArray });
|
|
@@ -162,6 +165,8 @@ function isManagementFrame(value) {
|
|
|
162
165
|
}
|
|
163
166
|
export function encodeManagementFrame(frame) {
|
|
164
167
|
const text = JSON.stringify(frame);
|
|
168
|
+
if (frame.t === 'desktop_automation_prepare')
|
|
169
|
+
decodeAutomationPrepare(text);
|
|
165
170
|
if (isDesktopManagementType(frame.t))
|
|
166
171
|
decodeDesktopManagementFrame(text);
|
|
167
172
|
return text;
|
|
@@ -170,12 +175,22 @@ export function decodeManagementFrame(source) {
|
|
|
170
175
|
const text = typeof source === 'string' ? source : new TextDecoder('utf-8', { fatal: true }).decode(source);
|
|
171
176
|
// Desktop extensions get the strict lexical/shape guard before any lossy object parsing.
|
|
172
177
|
const parsed = JSON.parse(text);
|
|
178
|
+
if (isObject(parsed) && parsed.t === 'desktop_automation_prepare')
|
|
179
|
+
return decodeAutomationPrepare(text);
|
|
173
180
|
if (isObject(parsed) && isDesktopManagementType(parsed.t))
|
|
174
181
|
return decodeDesktopManagementFrame(text);
|
|
175
182
|
if (isObject(parsed) && parsed.t === 'heartbeat' && Object.hasOwn(parsed, 'capabilitiesRevision'))
|
|
176
183
|
validateHeartbeatRevision(text);
|
|
177
184
|
return decodeChannelFrame(text, DISCRIMINATORS, isManagementFrame);
|
|
178
185
|
}
|
|
186
|
+
function decodeAutomationPrepare(text) {
|
|
187
|
+
const value = parseAutomationJson(text, 49152).value;
|
|
188
|
+
if (Object.keys(value).sort().join(',') !== 'automationProtocol,binding,deviceGeneration,grant,t' || value.automationProtocol !== 1 ||
|
|
189
|
+
typeof value.grant !== 'string' || !/^[A-Za-z0-9_-]{43}$/.test(value.grant) ||
|
|
190
|
+
value.binding?.route?.deviceGeneration !== value.deviceGeneration || !value.binding?.native || !value.binding?.preparation || typeof value.binding.executionId !== 'string')
|
|
191
|
+
throw new DesktopProtocolError('Invalid automation preparation');
|
|
192
|
+
return value;
|
|
193
|
+
}
|
|
179
194
|
/** Select only the new root field. JSON.parse already validates syntax; legacy nesting, size,
|
|
180
195
|
* unknown fields and duplicate legacy keys keep their established decoder behavior. */
|
|
181
196
|
function validateHeartbeatRevision(text) {
|
|
@@ -215,7 +230,7 @@ function validateHeartbeatRevision(text) {
|
|
|
215
230
|
}
|
|
216
231
|
function isDesktopManagementType(type) {
|
|
217
232
|
return typeof type === 'string' && ['desktop_background', 'desktop_lobby', 'desktop_lobby_result', 'workstation_capabilities', 'workstation_capabilities_request', 'desktop_prepare',
|
|
218
|
-
'desktop_prepare_result', 'desktop_cancel', 'desktop_session_state'].includes(type);
|
|
233
|
+
'desktop_prepare_result', 'desktop_cancel', 'desktop_isolation_result', 'desktop_session_state', 'desktop_ownership', 'desktop_ownership_result'].includes(type);
|
|
219
234
|
}
|
|
220
235
|
export class ManagementFrameAdmission {
|
|
221
236
|
isCurrent;
|
|
@@ -236,6 +251,10 @@ export class ManagementFrameAdmission {
|
|
|
236
251
|
}
|
|
237
252
|
if (frame.t === 'heartbeat' && frame.capabilitiesRevision !== undefined)
|
|
238
253
|
return false;
|
|
254
|
+
if (frame.t === 'desktop_ownership')
|
|
255
|
+
return !!this.ready && this.ready.protocols.MANAGEMENT >= 10 && frame.deviceGeneration === this.ready.generation;
|
|
256
|
+
if (frame.t === 'desktop_automation_prepare')
|
|
257
|
+
return !!this.ready && this.ready.protocols.MANAGEMENT >= MANAGEMENT_WIRE_DESKTOP_VERSION && frame.deviceGeneration === this.ready.generation;
|
|
239
258
|
if (!isDesktopManagementType(frame.t))
|
|
240
259
|
return true;
|
|
241
260
|
if (!this.ready || this.ready.protocols.MANAGEMENT < MANAGEMENT_WIRE_DESKTOP_VERSION)
|
|
@@ -7,6 +7,7 @@ import { deploymentDownloadURL } from '../control/serverIdentity.js';
|
|
|
7
7
|
import { downloadVerified } from '../update/artifact.js';
|
|
8
8
|
import { privateDirectory } from '../keepers/kit/paths.js';
|
|
9
9
|
const legacyRdFiles = ['bin/axom-desktop-keeper', 'bin/axom-input-guardian', 'bin/axom-quic', 'xorg.conf'];
|
|
10
|
+
export const RD_DEVICE_HOST_PLATFORM = 'linux-x64-debian-13-device-host';
|
|
10
11
|
export const rdFiles = [...legacyRdFiles, 'bin/axom-display', 'bin/axom-desktop-session'];
|
|
11
12
|
const sha256 = z.string().regex(/^[a-f0-9]{64}$/);
|
|
12
13
|
export const rdArtifactSchema = z.object({ version: z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._+-]{0,127}$/),
|
|
@@ -16,15 +17,15 @@ export const rdArtifactSchema = z.object({ version: z.string().regex(/^[a-zA-Z0-
|
|
|
16
17
|
files: z.record(z.string(), sha256),
|
|
17
18
|
}).refine(artifact => {
|
|
18
19
|
const files = artifact.ipcVersion === 1 ? legacyRdFiles : rdFiles;
|
|
19
|
-
const
|
|
20
|
-
return Object.keys(artifact.files).length === files.length +
|
|
20
|
+
const licenses = artifact.ipcVersion === 2 ? ['opus-LICENSE', 'webp-LICENSE'].filter(file => file in artifact.files) : [];
|
|
21
|
+
return Object.keys(artifact.files).length === files.length + licenses.length && files.every(file => file in artifact.files);
|
|
21
22
|
}, 'RD artifact files do not match its runtime version');
|
|
22
23
|
export function rdPlatform(osRelease, architecture) {
|
|
23
24
|
const values = Object.fromEntries(osRelease.split('\n').map(line => /^([A-Z_]+)=["']?([^"']*)["']?$/.exec(line)).filter(Boolean).map(match => [match[1], match[2]]));
|
|
24
25
|
if (values.ID !== 'debian' || values.VERSION_ID !== '13' || architecture !== 'x64') {
|
|
25
26
|
throw new Error('Remote desktop currently supports Debian 13 amd64; this distribution/architecture has no qualified RD package');
|
|
26
27
|
}
|
|
27
|
-
return
|
|
28
|
+
return RD_DEVICE_HOST_PLATFORM;
|
|
28
29
|
}
|
|
29
30
|
export function unpackRd(bytes, artifact) {
|
|
30
31
|
const archive = gunzipSync(bytes, { maxOutputLength: artifact.unpackedSize + 65536 });
|
|
@@ -38,16 +38,14 @@ export async function configureRd(directory, options) {
|
|
|
38
38
|
});
|
|
39
39
|
const server = normalizeWorkstationServer(options.server ?? registered?.server ?? resolveServer(await loadConfig()));
|
|
40
40
|
if (options.disableRd) {
|
|
41
|
-
await writeRdState(directory, previous ? { ...previous, enabled: false } : { schemaVersion:
|
|
41
|
+
await writeRdState(directory, previous ? { ...previous, enabled: false } : { schemaVersion: 2, enabled: false, artifactSource: server, session: 'auto' });
|
|
42
42
|
return;
|
|
43
43
|
}
|
|
44
|
-
if (registered && normalizeWorkstationServer(registered.server) !== server)
|
|
45
|
-
throw new Error('This workstation is already registered to another server; change its registration before enabling RD');
|
|
46
44
|
const { session, display } = rdSessionSelection(previous, options);
|
|
47
45
|
const platform = rdPlatform(await fs.readFile('/etc/os-release', 'utf8'), process.arch);
|
|
48
46
|
const installed = await installRd(directory, server, platform);
|
|
49
47
|
await ensureDependencies(installed.directory, session, display, installed.artifact);
|
|
50
|
-
await writeRdState(directory, { schemaVersion:
|
|
48
|
+
await writeRdState(directory, { schemaVersion: 2, enabled: true, artifactSource: server, session, ...(display ? { display } : {}),
|
|
51
49
|
installation: installed.directory, artifact: installed.artifact });
|
|
52
50
|
console.log(`Remote desktop enabled (${session}, ${installed.artifact.version}); capture starts only when an authorized viewer connects`);
|
|
53
51
|
});
|
|
@@ -114,6 +112,6 @@ export async function rdStatus(directory) {
|
|
|
114
112
|
if (error.code !== 'ENOENT')
|
|
115
113
|
throw error;
|
|
116
114
|
}
|
|
117
|
-
return { enabled: state.enabled, installed: state.installation !== undefined, version: state.artifact?.version, selection: state.session,
|
|
115
|
+
return { enabled: state.enabled, installed: state.installation !== undefined, version: state.artifact?.version, selection: state.session, artifactSource: state.artifactSource,
|
|
118
116
|
...(state.display ? { requestedDisplay: state.display } : {}), ...(state.enabled ? { observed, update } : {}) };
|
|
119
117
|
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { randomBytes, createHash, randomUUID } from 'node:crypto';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import { createConnection } from 'node:net';
|
|
6
|
+
import { NativeDeviceAdmission } from '../desktopControl/nativeDeviceAdmission.js';
|
|
7
|
+
import { bounded } from '../desktopControl/helperProcess.js';
|
|
8
|
+
import { privateDirectory } from '../keepers/kit/paths.js';
|
|
9
|
+
import { readDeviceHostDescriptor, deviceHostGrantSchema } from '../desktopControl/hostGrant.js';
|
|
10
|
+
export class LinuxDeviceHost {
|
|
11
|
+
directory;
|
|
12
|
+
cleanupDescriptor;
|
|
13
|
+
static pending = new Map();
|
|
14
|
+
child;
|
|
15
|
+
owner;
|
|
16
|
+
exit;
|
|
17
|
+
stopCommandId = randomUUID();
|
|
18
|
+
admission;
|
|
19
|
+
grant;
|
|
20
|
+
bootstrapGrant;
|
|
21
|
+
enableAttempted = false;
|
|
22
|
+
published = false;
|
|
23
|
+
failed = false;
|
|
24
|
+
exitObserved = false;
|
|
25
|
+
constructor(directory, cleanupDescriptor) {
|
|
26
|
+
this.directory = directory;
|
|
27
|
+
this.cleanupDescriptor = cleanupDescriptor;
|
|
28
|
+
}
|
|
29
|
+
static async recoverPending(root) {
|
|
30
|
+
const host = this.pending.get(root);
|
|
31
|
+
if (!host)
|
|
32
|
+
return;
|
|
33
|
+
if (host.failed && !host.enableAttempted && !host.published && host.exitObserved) {
|
|
34
|
+
host.owner?.destroy();
|
|
35
|
+
await fs.rm(path.join(host.directory, 'launch.json'), { force: true });
|
|
36
|
+
await fs.rm(path.join(host.directory, 'host.sock'), { force: true });
|
|
37
|
+
await fs.rmdir(host.directory).catch(error => { if (error.code !== 'ENOENT')
|
|
38
|
+
throw error; });
|
|
39
|
+
this.pending.delete(root);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (!host.admission) {
|
|
43
|
+
const original = host.bootstrapGrant;
|
|
44
|
+
if (!original)
|
|
45
|
+
throw Error('native_host_launch_unconfirmed');
|
|
46
|
+
host.admission = await NativeDeviceAdmission.connect(original);
|
|
47
|
+
host.grant = { ...original, hostGeneration: host.admission.hostLifetimeId };
|
|
48
|
+
}
|
|
49
|
+
return host;
|
|
50
|
+
}
|
|
51
|
+
static async launch(root, executable, environment) {
|
|
52
|
+
if (this.pending.has(root))
|
|
53
|
+
throw Error('original_native_host_unconfirmed');
|
|
54
|
+
const cleanup = path.join(root, 'desktop-cleanup');
|
|
55
|
+
await privateDirectory(cleanup);
|
|
56
|
+
const directory = await fs.mkdtemp('/tmp/axom-device-host-');
|
|
57
|
+
await fs.chmod(directory, 0o700);
|
|
58
|
+
const host = new LinuxDeviceHost(directory, path.join(cleanup, randomBytes(16).toString('hex') + '.json'));
|
|
59
|
+
const binary = await fs.realpath(executable), sha256 = createHash('sha256').update(await fs.readFile(binary)).digest('hex');
|
|
60
|
+
const endpoint = { socket: path.join(directory, 'host.sock'), token: randomBytes(32).toString('hex') };
|
|
61
|
+
host.bootstrapGrant = { schemaVersion: 2, executable: binary, sha256, ...endpoint };
|
|
62
|
+
const bootstrap = path.join(directory, 'launch.json');
|
|
63
|
+
await fs.writeFile(bootstrap, JSON.stringify({ ...endpoint, parentPid: process.pid, cleanupDescriptor: host.cleanupDescriptor, executable: binary, sha256 }), { flag: 'wx', mode: 0o600 });
|
|
64
|
+
this.pending.set(root, host);
|
|
65
|
+
const child = spawn(binary, ['--device-host', bootstrap], { env: { ...environment, AXOM_ACCESS_TOKEN: undefined }, stdio: 'ignore' });
|
|
66
|
+
host.child = child;
|
|
67
|
+
host.exit = new Promise((resolve, reject) => { child.once('exit', () => { host.exitObserved = true; resolve(); }); child.once('error', reject); });
|
|
68
|
+
void host.exit.catch(() => { });
|
|
69
|
+
try {
|
|
70
|
+
const deadline = Date.now() + 5000;
|
|
71
|
+
while (Date.now() < deadline) {
|
|
72
|
+
if (child.exitCode !== null)
|
|
73
|
+
throw new Error('native_host_launch_failed');
|
|
74
|
+
try {
|
|
75
|
+
await fs.lstat(endpoint.socket);
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
if (error.code !== 'ENOENT')
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
83
|
+
}
|
|
84
|
+
const owner = createConnection(endpoint.socket);
|
|
85
|
+
host.owner = owner;
|
|
86
|
+
await bounded(new Promise((resolve, reject) => {
|
|
87
|
+
owner.once('error', reject);
|
|
88
|
+
owner.once('connect', () => owner.write(endpoint.token));
|
|
89
|
+
owner.once('data', bytes => { if (bytes.length !== 1 || bytes[0] !== 1) {
|
|
90
|
+
reject(new Error('native_host_authentication_failed'));
|
|
91
|
+
return;
|
|
92
|
+
} owner.write('owner\n'); resolve(); });
|
|
93
|
+
owner.once('end', () => reject(new Error('native_host_owner_closed')));
|
|
94
|
+
}), 5000);
|
|
95
|
+
host.admission = await NativeDeviceAdmission.connect(endpoint);
|
|
96
|
+
host.grant = { schemaVersion: 2, hostGeneration: host.admission.hostLifetimeId, executable: binary, sha256, ...endpoint };
|
|
97
|
+
host.published = true;
|
|
98
|
+
await host.persistOriginal();
|
|
99
|
+
await new Promise((resolve, reject) => {
|
|
100
|
+
const peer = createConnection(endpoint.socket);
|
|
101
|
+
const timer = setTimeout(() => { peer.destroy(); reject(new Error('native_host_enable_unconfirmed')); }, 5000);
|
|
102
|
+
let authenticated = false, bytes = Buffer.alloc(0);
|
|
103
|
+
peer.once('connect', () => peer.write(endpoint.token));
|
|
104
|
+
peer.once('error', error => { clearTimeout(timer); reject(error); });
|
|
105
|
+
peer.on('data', chunk => { bytes = Buffer.concat([bytes, chunk]); if (!authenticated) {
|
|
106
|
+
if (bytes[0] !== 1) {
|
|
107
|
+
clearTimeout(timer);
|
|
108
|
+
peer.destroy();
|
|
109
|
+
reject(new Error('native_host_authentication_failed'));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
authenticated = true;
|
|
113
|
+
bytes = bytes.subarray(1);
|
|
114
|
+
host.enableAttempted = true;
|
|
115
|
+
peer.write('enable\n');
|
|
116
|
+
} if (bytes.includes(10)) {
|
|
117
|
+
clearTimeout(timer);
|
|
118
|
+
peer.destroy();
|
|
119
|
+
try {
|
|
120
|
+
if (JSON.stringify(JSON.parse(bytes.subarray(0, bytes.indexOf(10)).toString())) !== '{}')
|
|
121
|
+
throw Error();
|
|
122
|
+
resolve();
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
reject(new Error('native_host_enable_unconfirmed'));
|
|
126
|
+
}
|
|
127
|
+
} });
|
|
128
|
+
});
|
|
129
|
+
this.pending.delete(root);
|
|
130
|
+
return host;
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
host.failed = true;
|
|
134
|
+
host.owner?.destroy();
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async persistOriginal() {
|
|
139
|
+
const expected = deviceHostGrantSchema.parse(this.grant);
|
|
140
|
+
try {
|
|
141
|
+
if (JSON.stringify(await readDeviceHostDescriptor(this.cleanupDescriptor)) !== JSON.stringify(expected))
|
|
142
|
+
throw Error('original_native_host_unconfirmed');
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
if (error.code !== 'ENOENT')
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
const staging = this.cleanupDescriptor + '.writing-' + randomUUID();
|
|
150
|
+
const file = await fs.open(staging, 'wx', 0o600);
|
|
151
|
+
try {
|
|
152
|
+
await file.writeFile(JSON.stringify(expected));
|
|
153
|
+
await file.sync();
|
|
154
|
+
await file.close();
|
|
155
|
+
try {
|
|
156
|
+
await fs.link(staging, this.cleanupDescriptor);
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
if (error.code !== 'EEXIST')
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
if (JSON.stringify(await readDeviceHostDescriptor(this.cleanupDescriptor)) !== JSON.stringify(expected))
|
|
163
|
+
throw Error('original_native_host_unconfirmed');
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
await file.close();
|
|
167
|
+
await fs.unlink(staging);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async stopOriginal() {
|
|
171
|
+
let failure;
|
|
172
|
+
try {
|
|
173
|
+
await this.persistOriginal();
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
failure = error;
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
await this.admission.stopHost(this.stopCommandId);
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
failure ??= error;
|
|
183
|
+
}
|
|
184
|
+
if (failure)
|
|
185
|
+
throw failure;
|
|
186
|
+
}
|
|
187
|
+
async finalizeOriginal() {
|
|
188
|
+
this.owner?.destroy();
|
|
189
|
+
if (this.exit)
|
|
190
|
+
try {
|
|
191
|
+
await bounded(this.exit, 5000);
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
this.child?.kill('SIGTERM');
|
|
195
|
+
await bounded(this.exit, 5000);
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
const descriptor = await readDeviceHostDescriptor(this.cleanupDescriptor);
|
|
199
|
+
if (descriptor.hostGeneration === this.grant.hostGeneration && descriptor.token === this.grant.token && descriptor.socket === this.grant.socket)
|
|
200
|
+
await fs.unlink(this.cleanupDescriptor);
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
if (error.code !== 'ENOENT')
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
await fs.rm(path.join(this.directory, 'launch.json'), { force: true });
|
|
207
|
+
await fs.rmdir(this.directory).catch(error => { if (error.code !== 'ENOENT')
|
|
208
|
+
throw error; });
|
|
209
|
+
for (const [root, original] of LinuxDeviceHost.pending)
|
|
210
|
+
if (original === this)
|
|
211
|
+
LinuxDeviceHost.pending.delete(root);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import { execFile } from 'node:child_process';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { installRd, rdPlatform, verifyRdInstallation } from './artifact.js';
|
|
7
|
+
import { readRdState } from './state.js';
|
|
8
|
+
import { withPrivateLock } from '../control/locks.js';
|
|
9
|
+
const execute = promisify(execFile);
|
|
10
|
+
const capabilities = z.strictObject({ deviceAdmissionProtocol: z.literal(1), deviceHost: z.literal(true) });
|
|
11
|
+
/** Stage only. The caller must retain the legacy physical fence: qualification is not retirement
|
|
12
|
+
* of predecessor capture/input authority, and must never activate this state on its own. */
|
|
13
|
+
export async function prepareLegacyRdReadiness(directory, options = {}) {
|
|
14
|
+
return withPrivateLock(path.join(directory, 'rd', 'install'), async () => {
|
|
15
|
+
const previous = await readRdState(directory);
|
|
16
|
+
if (!previous?.enabled)
|
|
17
|
+
return { kind: 'not-enabled' };
|
|
18
|
+
const qualify = async (installation) => {
|
|
19
|
+
await verifyRdInstallation(installation.directory, installation.artifact);
|
|
20
|
+
const result = await (options.execute ?? execute)(path.join(installation.directory, 'bin/axom-desktop-keeper'), ['--device-capabilities'], {
|
|
21
|
+
timeout: 5000, maxBuffer: 8192,
|
|
22
|
+
env: { ...process.env, AXOM_ACCESS_TOKEN: undefined, AXOM_DEVICE_SCOPE: undefined, AXOM_DEVICE_CONTEXT: undefined,
|
|
23
|
+
AXOM_KEEPER_SOCKET: undefined, AXOM_KEEPER_TOKEN: undefined },
|
|
24
|
+
});
|
|
25
|
+
capabilities.parse(JSON.parse(result.stdout));
|
|
26
|
+
};
|
|
27
|
+
try {
|
|
28
|
+
await qualify({ directory: previous.installation, artifact: previous.artifact });
|
|
29
|
+
return { kind: 'ready', state: previous };
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// A legacy --self-test or IPC version says nothing about device admission support.
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const platform = options.platform ?? rdPlatform(await fs.readFile('/etc/os-release', 'utf8'), process.arch);
|
|
36
|
+
const candidate = await (options.install ?? installRd)(directory, previous.artifactSource, platform);
|
|
37
|
+
await qualify(candidate);
|
|
38
|
+
if (JSON.stringify(await readRdState(directory)) !== JSON.stringify(previous))
|
|
39
|
+
throw Error('Remote desktop selection changed during migration staging');
|
|
40
|
+
return { kind: 'staged', state: { ...previous, installation: candidate.directory, artifact: candidate.artifact } };
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
return { kind: 'unavailable', reason: `Remote desktop requires a qualified device host: ${error instanceof Error ? error.message : String(error)}` };
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
@@ -7,12 +7,12 @@ import { NativeProvider } from '../desktopControl/nativeProvider.js';
|
|
|
7
7
|
import { HelperProcess, NativeCommandError } from '../desktopControl/helperProcess.js';
|
|
8
8
|
import { desktopCapabilitiesSchema } from '../desktopControl/protocol.js';
|
|
9
9
|
import { captureTargetLabel } from '../desktopControl/captureTargetLabel.js';
|
|
10
|
-
import { normalizeWorkstationServer } from '../control/serverIdentity.js';
|
|
11
10
|
import { privateDirectory } from '../keepers/kit/paths.js';
|
|
12
11
|
import { verifyRdInstallation } from './artifact.js';
|
|
13
12
|
import { readRdState } from './state.js';
|
|
14
13
|
import { chooseSession, discoverSessions, ensureManagedSession, sessionAlive } from './sessions.js';
|
|
15
14
|
import { desktopDisplayControl, parseXrandr } from './display.js';
|
|
15
|
+
import { LinuxDeviceHost } from './host.js';
|
|
16
16
|
const execute = promisify(execFile);
|
|
17
17
|
const sourceAxis = z.number().int().min(2).max(4096);
|
|
18
18
|
const geometrySchema = z.object({ width: sourceAxis, height: sourceAxis, audio: z.boolean().optional() });
|
|
@@ -21,6 +21,33 @@ export class LinuxDesktopProvider {
|
|
|
21
21
|
directory;
|
|
22
22
|
server;
|
|
23
23
|
legacy;
|
|
24
|
+
host;
|
|
25
|
+
get deviceAdmission() { return this.host?.admission; }
|
|
26
|
+
static async connect(directory) {
|
|
27
|
+
if (!(await readRdState(directory))?.enabled)
|
|
28
|
+
return;
|
|
29
|
+
const provider = new LinuxDesktopProvider(directory, '');
|
|
30
|
+
await provider.initialize();
|
|
31
|
+
if (!provider.host)
|
|
32
|
+
throw new NativeCommandError('unavailable');
|
|
33
|
+
return provider;
|
|
34
|
+
}
|
|
35
|
+
async stopOriginal() { if (this.host)
|
|
36
|
+
await this.host.stopOriginal(); }
|
|
37
|
+
async finalizeOriginal() { if (this.host)
|
|
38
|
+
await this.host.finalizeOriginal(); }
|
|
39
|
+
async retirementRequired() {
|
|
40
|
+
const current = await readRdState(this.directory);
|
|
41
|
+
return !current?.enabled || !this.state || current.artifact.sha512 !== this.state.artifact.sha512 || current.session !== this.state.session || current.display !== this.state.display || !this.session || !await sessionAlive(this.session);
|
|
42
|
+
}
|
|
43
|
+
async prepareAutomation(binding) {
|
|
44
|
+
await this.initialize();
|
|
45
|
+
if (!this.usingLegacy)
|
|
46
|
+
await this.validate();
|
|
47
|
+
if (!this.provider?.prepareAutomation)
|
|
48
|
+
throw new NativeCommandError('unsupported_automation_protocol');
|
|
49
|
+
return this.provider.prepareAutomation(binding);
|
|
50
|
+
}
|
|
24
51
|
session;
|
|
25
52
|
provider;
|
|
26
53
|
state;
|
|
@@ -47,7 +74,7 @@ export class LinuxDesktopProvider {
|
|
|
47
74
|
const current = await readRdState(this.directory);
|
|
48
75
|
if (!current?.enabled || !this.state || current.artifact.sha512 !== this.state.artifact.sha512 ||
|
|
49
76
|
current.session !== this.state.session || current.display !== this.state.display ||
|
|
50
|
-
|
|
77
|
+
!session || this.session !== session || !await sessionAlive(session)) {
|
|
51
78
|
throw new NativeCommandError('unavailable');
|
|
52
79
|
}
|
|
53
80
|
}
|
|
@@ -75,8 +102,11 @@ export class LinuxDesktopProvider {
|
|
|
75
102
|
this.state = undefined;
|
|
76
103
|
return;
|
|
77
104
|
}
|
|
78
|
-
if (
|
|
79
|
-
|
|
105
|
+
if (this.host) {
|
|
106
|
+
if (await this.retirementRequired())
|
|
107
|
+
throw new NativeCommandError('unavailable');
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
80
110
|
if (this.provider && this.state?.artifact.sha512 === state.artifact.sha512 && this.state.session === state.session && this.state.display === state.display && this.session && await sessionAlive(this.session))
|
|
81
111
|
return;
|
|
82
112
|
this.provider = undefined;
|
|
@@ -91,11 +121,15 @@ export class LinuxDesktopProvider {
|
|
|
91
121
|
session.environment.AXOM_DESKTOP_RUNTIME_DIR : path.join(runtimeRoot, session.identity);
|
|
92
122
|
await privateDirectory(this.runtime);
|
|
93
123
|
const executable = path.join(state.installation, 'bin/axom-desktop-keeper');
|
|
124
|
+
const originalHost = await LinuxDeviceHost.launch(this.directory, executable, this.environment());
|
|
125
|
+
this.host = originalHost;
|
|
126
|
+
const originalEnvironment = { ...this.environment(), AXOM_KEEPER_SOCKET: originalHost.grant.socket, AXOM_KEEPER_TOKEN: originalHost.grant.token };
|
|
94
127
|
this.provider = new NativeProvider({ helperPath: executable, runtimeDir: this.runtime,
|
|
95
|
-
|
|
128
|
+
deviceAdmission: originalHost.admission, environment: originalEnvironment, enumerate: () => this.capabilities(), validateSession: () => this.validate(session),
|
|
96
129
|
sessionHelperFactory: async (binding) => {
|
|
97
130
|
await this.validate(session);
|
|
98
|
-
const helper = await HelperProcess.launch(executable, { ...
|
|
131
|
+
const helper = await HelperProcess.launch(executable, { ...originalEnvironment, AXOM_KEEPER_TARGET: binding.displayId,
|
|
132
|
+
AXOM_DEVICE_CONTEXT: originalHost.admission.context(binding.deviceGrant?.registrationId ?? '', binding.deviceGeneration, binding.sessionId, binding.deviceGrant) });
|
|
99
133
|
return helper;
|
|
100
134
|
} });
|
|
101
135
|
process.env.AXOM_QUIC_BINARY = path.join(state.installation, 'bin/axom-quic');
|
|
@@ -197,19 +231,19 @@ export class LinuxDesktopProvider {
|
|
|
197
231
|
throw error;
|
|
198
232
|
}
|
|
199
233
|
}
|
|
200
|
-
async prepare(selection, binding) {
|
|
234
|
+
async prepare(selection, binding, current) {
|
|
201
235
|
await this.initialize();
|
|
202
236
|
if (!this.provider)
|
|
203
237
|
throw new NativeCommandError('unavailable');
|
|
204
|
-
return this.provider.prepare(selection, binding);
|
|
238
|
+
return this.provider.prepare(selection, binding, current);
|
|
205
239
|
}
|
|
206
|
-
async wallpaper(targetId) {
|
|
240
|
+
async wallpaper(targetId, pixels = { width: 1920, height: 1080 }) {
|
|
207
241
|
await this.initialize();
|
|
208
242
|
if (!this.usingLegacy)
|
|
209
243
|
await this.validate();
|
|
210
244
|
if (!this.provider)
|
|
211
245
|
throw new NativeCommandError('unavailable');
|
|
212
|
-
return this.provider.wallpaper(targetId);
|
|
246
|
+
return this.provider.wallpaper(targetId, pixels);
|
|
213
247
|
}
|
|
214
248
|
async recoverDisplay(binding, reservation) {
|
|
215
249
|
await this.initialize();
|
|
@@ -4,13 +4,25 @@ import { randomUUID } from 'node:crypto';
|
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { rdArtifactSchema } from './artifact.js';
|
|
6
6
|
import { privateDirectory } from '../keepers/kit/paths.js';
|
|
7
|
-
const selection = { schemaVersion: z.literal(
|
|
7
|
+
const selection = { schemaVersion: z.literal(2), artifactSource: z.string().url(),
|
|
8
8
|
session: z.enum(['auto', 'existing', 'virtual']), display: z.string().regex(/^:\d+(?:\.\d+)?$/).optional(),
|
|
9
9
|
};
|
|
10
|
-
|
|
10
|
+
const currentRdStateSchema = z.discriminatedUnion('enabled', [
|
|
11
11
|
z.strictObject({ ...selection, enabled: z.literal(true), installation: z.string().startsWith('/'), artifact: rdArtifactSchema }),
|
|
12
12
|
z.strictObject({ ...selection, enabled: z.literal(false), installation: z.string().startsWith('/').optional(), artifact: rdArtifactSchema.optional() }),
|
|
13
13
|
]);
|
|
14
|
+
const legacySelection = { schemaVersion: z.literal(1), server: z.string().url(), session: selection.session, display: selection.display };
|
|
15
|
+
const legacyRdStateSchema = z.discriminatedUnion('enabled', [
|
|
16
|
+
z.strictObject({ ...legacySelection, enabled: z.literal(true), installation: z.string().startsWith('/'), artifact: rdArtifactSchema }),
|
|
17
|
+
z.strictObject({ ...legacySelection, enabled: z.literal(false), installation: z.string().startsWith('/').optional(), artifact: rdArtifactSchema.optional() }),
|
|
18
|
+
]);
|
|
19
|
+
export const rdStateSchema = z.preprocess(value => {
|
|
20
|
+
const legacy = legacyRdStateSchema.safeParse(value);
|
|
21
|
+
if (!legacy.success)
|
|
22
|
+
return value;
|
|
23
|
+
const { server, ...state } = legacy.data;
|
|
24
|
+
return { ...state, schemaVersion: 2, artifactSource: server };
|
|
25
|
+
}, currentRdStateSchema);
|
|
14
26
|
export async function readRdState(directory) {
|
|
15
27
|
let file;
|
|
16
28
|
try {
|
|
@@ -3,10 +3,9 @@ import path from 'node:path';
|
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import { execFile } from 'node:child_process';
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
|
-
import { installRd, rdArtifactSchema, rdPlatform, verifyRdInstallation } from './artifact.js';
|
|
6
|
+
import { installRd, rdArtifactSchema, rdPlatform, verifyRdInstallation, RD_DEVICE_HOST_PLATFORM } from './artifact.js';
|
|
7
7
|
import { readRdState, writeRdState } from './state.js';
|
|
8
8
|
import { assertLockOwnership, withPrivateLock } from '../control/locks.js';
|
|
9
|
-
import { normalizeWorkstationServer } from '../control/serverIdentity.js';
|
|
10
9
|
import { privateDirectory } from '../keepers/kit/paths.js';
|
|
11
10
|
import { z } from 'zod';
|
|
12
11
|
import { CLIENT_VERSION } from '../version.js';
|
|
@@ -34,9 +33,9 @@ async function adoptBundle(directory, server, options) {
|
|
|
34
33
|
if (!bundle)
|
|
35
34
|
return;
|
|
36
35
|
const catalog = z.object({ schemaVersion: z.literal(1), artifacts: z.record(z.string(), rdArtifactSchema) }).parse(JSON.parse(await fs.readFile(bundle.manifest, 'utf8')));
|
|
37
|
-
const artifact = catalog.artifacts[
|
|
36
|
+
const artifact = catalog.artifacts[RD_DEVICE_HOST_PLATFORM];
|
|
38
37
|
if (!artifact || artifact.ipcVersion !== 2)
|
|
39
|
-
throw new Error('Bundled RD requires the IPC 2 runtime');
|
|
38
|
+
throw new Error('Bundled RD requires the device-host platform and IPC 2 runtime');
|
|
40
39
|
if (!bundle.display || !/^:\d+(?:\.\d+)?$/.test(bundle.display))
|
|
41
40
|
throw new Error('Bundled RD requires a local X11 DISPLAY');
|
|
42
41
|
if (!path.isAbsolute(bundle.root))
|
|
@@ -44,7 +43,7 @@ async function adoptBundle(directory, server, options) {
|
|
|
44
43
|
await verifyRdInstallation(bundle.root, artifact);
|
|
45
44
|
await selfTest(bundle.root, artifact, options);
|
|
46
45
|
assertLockOwnership();
|
|
47
|
-
await writeRdState(directory, { schemaVersion:
|
|
46
|
+
await writeRdState(directory, { schemaVersion: 2, enabled: true, artifactSource: server, session: 'existing', display: bundle.display, installation: bundle.root, artifact });
|
|
48
47
|
}
|
|
49
48
|
function requireSupportedIpc(artifact) {
|
|
50
49
|
if (artifact.ipcVersion !== 1 && artifact.ipcVersion !== 2)
|
|
@@ -73,8 +72,6 @@ export async function synchronizeRd(directory, server, options = {}) {
|
|
|
73
72
|
if (!previous?.enabled)
|
|
74
73
|
return;
|
|
75
74
|
requireSupportedIpc(previous.artifact);
|
|
76
|
-
if (normalizeWorkstationServer(previous.server) !== normalizeWorkstationServer(server))
|
|
77
|
-
throw new Error('RD belongs to a different workstation server');
|
|
78
75
|
}
|
|
79
76
|
catch (error) {
|
|
80
77
|
await writeStatus(directory, { state: 'blocked', reason: reason(error) });
|
|
@@ -95,7 +92,7 @@ export async function synchronizeRd(directory, server, options = {}) {
|
|
|
95
92
|
let installed;
|
|
96
93
|
try {
|
|
97
94
|
const platform = options.platform ?? rdPlatform(await fs.readFile('/etc/os-release', 'utf8'), process.arch);
|
|
98
|
-
installed = await (options.install ?? installRd)(directory,
|
|
95
|
+
installed = await (options.install ?? installRd)(directory, previous.artifactSource, platform, options.fetch ?? globalThis.fetch);
|
|
99
96
|
requireSupportedIpc(installed.artifact);
|
|
100
97
|
await verifyRdInstallation(installed.directory, installed.artifact);
|
|
101
98
|
await selfTest(installed.directory, installed.artifact, options);
|