@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
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { promises as fs, constants } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
export const deviceHostGrantSchema = z.strictObject({ schemaVersion: z.literal(2), hostGeneration: z.string().min(1).max(128),
|
|
6
|
+
executable: z.string().startsWith('/').max(4096), sha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
7
|
+
socket: z.string().startsWith('/').max(103), token: z.string().regex(/^[a-f0-9]{64}$/) });
|
|
8
|
+
async function verifiedSocketPath(socket, checks) {
|
|
9
|
+
const realpath = checks.realpath ?? ((file) => fs.realpath(file)), canonical = await realpath(socket);
|
|
10
|
+
if (canonical === socket)
|
|
11
|
+
return true;
|
|
12
|
+
// Older macOS producers stored /tmp while the OS resolves its root-owned system alias.
|
|
13
|
+
// Exact prefix substitution rejects every additional symlink or path normalization.
|
|
14
|
+
if ((checks.platform ?? process.platform) !== 'darwin' || !socket.startsWith('/tmp/') || canonical !== '/private' + socket)
|
|
15
|
+
return false;
|
|
16
|
+
try {
|
|
17
|
+
const lstat = checks.systemLstat ?? ((file) => fs.lstat(file));
|
|
18
|
+
const link = await lstat('/tmp'), target = await lstat('/private/tmp');
|
|
19
|
+
const destination = await (checks.systemReadlink ?? ((file) => fs.readlink(file)))('/tmp');
|
|
20
|
+
return link.isSymbolicLink() && link.uid === 0 && target.isDirectory() && target.uid === 0 && !!(target.mode & 0o1000) &&
|
|
21
|
+
(destination === 'private/tmp' || destination === '/private/tmp') && await realpath('/tmp') === '/private/tmp' && await realpath('/private/tmp') === '/private/tmp';
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export async function readDeviceHostDescriptor(fileName) {
|
|
28
|
+
const parent = await fs.lstat(path.dirname(fileName));
|
|
29
|
+
if (!parent.isDirectory() || parent.uid !== process.getuid?.() || parent.mode & 0o077)
|
|
30
|
+
throw new Error('unsafe_native_host');
|
|
31
|
+
const file = await fs.open(fileName, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
32
|
+
let grant;
|
|
33
|
+
try {
|
|
34
|
+
const stat = await file.stat();
|
|
35
|
+
if (!stat.isFile() || stat.uid !== process.getuid?.() || stat.mode & 0o077 || stat.size > 16384)
|
|
36
|
+
throw new Error('unsafe_native_host');
|
|
37
|
+
grant = deviceHostGrantSchema.parse(JSON.parse(await file.readFile('utf8')));
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
await file.close();
|
|
41
|
+
}
|
|
42
|
+
return grant;
|
|
43
|
+
}
|
|
44
|
+
export async function readDeviceHostGrant(fileName, allowRootExecutable = false, checks = {}) {
|
|
45
|
+
const grant = await readDeviceHostDescriptor(fileName);
|
|
46
|
+
const parent = await fs.lstat(path.dirname(grant.socket)), socket = await fs.lstat(grant.socket);
|
|
47
|
+
if (!parent.isDirectory() || parent.uid !== process.getuid?.() || parent.mode & 0o077 || !socket.isSocket() || socket.uid !== process.getuid?.() || socket.mode & 0o077 ||
|
|
48
|
+
!await verifiedSocketPath(grant.socket, checks) || await (checks.realpath ?? ((file) => fs.realpath(file)))(grant.executable) !== grant.executable)
|
|
49
|
+
throw new Error('unsafe_native_host');
|
|
50
|
+
const executable = await fs.open(grant.executable, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
51
|
+
try {
|
|
52
|
+
const stat = await executable.stat();
|
|
53
|
+
if (!stat.isFile() || stat.uid !== process.getuid?.() && !(allowRootExecutable && stat.uid === 0) || stat.mode & 0o022 || stat.size > 32 * 1024 * 1024)
|
|
54
|
+
throw new Error('unsafe_native_host');
|
|
55
|
+
const hash = createHash('sha256');
|
|
56
|
+
for await (const bytes of executable.createReadStream({ autoClose: false }))
|
|
57
|
+
hash.update(bytes);
|
|
58
|
+
if (hash.digest('hex') !== grant.sha256)
|
|
59
|
+
throw new Error('unsafe_native_host');
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
await executable.close();
|
|
63
|
+
}
|
|
64
|
+
return grant;
|
|
65
|
+
}
|
|
@@ -14,7 +14,8 @@ const resolutionSchema = z.discriminatedUnion('mode', [z.strictObject({ mode: z.
|
|
|
14
14
|
export const lobbyRequestSchema = z.strictObject({ requestId: identifier, revision: safe, viewport: viewportSchema,
|
|
15
15
|
resolution: resolutionSchema, dpi: z.number().int().min(64).max(384).nullable(),
|
|
16
16
|
transports: z.array(z.enum(['quic', 'wss'])).min(1).max(2).refine(values => new Set(values).size === values.length),
|
|
17
|
-
targetId: identifier.nullable().optional(), viewer: z.unknown().nullable().optional()
|
|
17
|
+
targetId: identifier.nullable().optional(), viewer: z.unknown().nullable().optional(),
|
|
18
|
+
backgroundVersion: z.string().regex(/^[A-Za-z0-9_-]{1,128}$/).nullish() });
|
|
18
19
|
const rangeSchema = z.strictObject({ minWidth: axis, minHeight: axis, maxWidth: axis, maxHeight: axis });
|
|
19
20
|
export const lobbyPlanSchema = z.strictObject({ lobbyId: identifier, planId: identifier, revision: safe, targetId: identifier,
|
|
20
21
|
desktop: sourceModeSchema, dpi: z.number().int().min(64).max(384).nullable(), resize: z.enum(['none', 'modes', 'dynamic']),
|
|
@@ -40,8 +41,9 @@ export const wallpaperAssetSchema = z.strictObject({ assetId: z.string().min(1).
|
|
|
40
41
|
anchorX: z.number().finite().min(0).max(1).default(0.5), anchorY: z.number().finite().min(0).max(1).default(0.5),
|
|
41
42
|
background: z.string().regex(/^#[a-fA-F0-9]{6}$/).default('#000000'),
|
|
42
43
|
intrinsicScale: z.number().finite().min(0.125).max(16).default(1),
|
|
44
|
+
mimeType: z.enum(['image/jpeg', 'image/webp']).default('image/jpeg'),
|
|
43
45
|
fidelity: z.enum(['exact', 'approximate']).default('exact'), colorSpace: z.literal('srgb').default('srgb')
|
|
44
|
-
}).refine(asset => asset.width * asset.height <=
|
|
46
|
+
}).refine(asset => asset.width * asset.height <= 8_388_608 && (asset.coverage !== 'composited' || asset.compatibleDesktop != null));
|
|
45
47
|
export function composeWallpaper(value, selected) {
|
|
46
48
|
const asset = wallpaperAssetSchema.parse(value);
|
|
47
49
|
const desktop = sourceModeSchema.parse(selected);
|
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import { promises as fs
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { createHash } from 'node:crypto';
|
|
4
3
|
import { execFile } from 'node:child_process';
|
|
5
4
|
import { promisify } from 'node:util';
|
|
6
5
|
import { createConnection } from 'node:net';
|
|
7
6
|
import { z } from 'zod';
|
|
8
7
|
import { NativeProvider } from './nativeProvider.js';
|
|
9
8
|
import { HelperProcess, bounded, NativeCommandError } from './helperProcess.js';
|
|
9
|
+
import { readDeviceHostGrant } from './hostGrant.js';
|
|
10
10
|
import { desktopCapabilitiesSchema } from './protocol.js';
|
|
11
|
-
import {
|
|
11
|
+
import { NativeDeviceAdmission } from './nativeDeviceAdmission.js';
|
|
12
12
|
import { captureTargetLabel } from './captureTargetLabel.js';
|
|
13
13
|
import { decodeWallpaper } from './wallpaper.js';
|
|
14
14
|
import { captureSamplingModes as macCaptureModes } from './captureModes.js';
|
|
15
15
|
const execute = promisify(execFile);
|
|
16
|
-
export const macDesktopGrantSchema = z.strictObject({ schemaVersion: z.literal(
|
|
17
|
-
|
|
16
|
+
export const macDesktopGrantSchema = z.strictObject({ schemaVersion: z.literal(2),
|
|
17
|
+
hostGeneration: z.string().min(1).max(128),
|
|
18
18
|
executable: z.string().startsWith('/').max(4096), sha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
19
19
|
socket: z.string().startsWith('/').max(103), token: z.string().regex(/^[a-f0-9]{64}$/) });
|
|
20
20
|
const target = z.object({ id: z.string().regex(/^[0-9a-f]{32}$/), kind: z.enum(['display', 'window']), label: z.string(), displayIdentity: z.string().min(1).max(256), capturePixelScale: z.number().min(1).max(16).optional(),
|
|
@@ -39,45 +39,36 @@ export async function checkMacHost(grant) {
|
|
|
39
39
|
/** Discovery can report permissions but cannot prompt, capture, encode or inject input. */
|
|
40
40
|
export class MacProvider {
|
|
41
41
|
directory;
|
|
42
|
-
|
|
42
|
+
sourceFile;
|
|
43
|
+
async prepareAutomation(binding) {
|
|
44
|
+
const provider = await this.liveProvider();
|
|
45
|
+
if (!provider)
|
|
46
|
+
throw new NativeCommandError('unavailable');
|
|
47
|
+
return provider.prepareAutomation(binding);
|
|
48
|
+
}
|
|
49
|
+
admission;
|
|
50
|
+
original;
|
|
51
|
+
get deviceAdmission() { return this.admission; }
|
|
52
|
+
static async connect(directory, sourceFile) {
|
|
53
|
+
const provider = new MacProvider(directory, undefined, sourceFile);
|
|
54
|
+
if (!await provider.liveProvider())
|
|
55
|
+
throw new NativeCommandError('unavailable');
|
|
56
|
+
return provider;
|
|
57
|
+
}
|
|
43
58
|
provider;
|
|
44
59
|
identity = '';
|
|
45
60
|
revisions = new Map();
|
|
46
|
-
constructor(directory,
|
|
61
|
+
constructor(directory, _binding, sourceFile) {
|
|
47
62
|
this.directory = directory;
|
|
48
|
-
this.
|
|
63
|
+
this.sourceFile = sourceFile;
|
|
49
64
|
}
|
|
50
65
|
async grant() {
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
try {
|
|
54
|
-
const stat = await file.stat();
|
|
55
|
-
if (!stat.isFile() || stat.uid !== process.getuid?.() || stat.mode & 0o077 || stat.size > 16384)
|
|
56
|
-
throw new NativeCommandError('unavailable');
|
|
57
|
-
grant = macDesktopGrantSchema.parse(JSON.parse(await file.readFile('utf8')));
|
|
58
|
-
}
|
|
59
|
-
finally {
|
|
60
|
-
await file.close();
|
|
61
|
-
}
|
|
62
|
-
if (normalizeWorkstationServer(grant.binding.server) !== normalizeWorkstationServer(this.binding.server) || grant.binding.workstationId !== this.binding.workstationId || grant.binding.ownerUserId !== this.binding.ownerUserId)
|
|
66
|
+
const grant = await readDeviceHostGrant(this.sourceFile ?? path.join(this.directory, 'desktop-provider.json'));
|
|
67
|
+
if (this.original && JSON.stringify(grant) !== JSON.stringify(this.original))
|
|
63
68
|
throw new NativeCommandError('unavailable');
|
|
64
69
|
if (!grant.executable.endsWith('/Axom Work Computer Use.app/Contents/MacOS/Axom Work Computer Use'))
|
|
65
70
|
throw new NativeCommandError('unavailable');
|
|
66
|
-
|
|
67
|
-
try {
|
|
68
|
-
const stat = await executable.stat();
|
|
69
|
-
if (!stat.isFile() || stat.size > 32 * 1024 * 1024 || stat.mode & 0o022)
|
|
70
|
-
throw new NativeCommandError('unavailable');
|
|
71
|
-
const hash = createHash('sha256');
|
|
72
|
-
for await (const bytes of executable.createReadStream({ autoClose: false }))
|
|
73
|
-
hash.update(bytes);
|
|
74
|
-
if (hash.digest('hex') !== grant.sha256)
|
|
75
|
-
throw new NativeCommandError('unavailable');
|
|
76
|
-
}
|
|
77
|
-
finally {
|
|
78
|
-
await executable.close();
|
|
79
|
-
}
|
|
80
|
-
await execute('/usr/bin/codesign', ['--verify', '--strict', path.resolve(grant.executable, '../../..')], { timeout: 7000, maxBuffer: 65536 });
|
|
71
|
+
await execute('/usr/bin/codesign', ['--verify', '--strict', '-R', '=identifier "com.platless.axom.computer-use"', path.resolve(grant.executable, '../../..')], { timeout: 7000, maxBuffer: 65536 });
|
|
81
72
|
await checkMacHost(grant);
|
|
82
73
|
return grant;
|
|
83
74
|
}
|
|
@@ -107,10 +98,20 @@ export class MacProvider {
|
|
|
107
98
|
async liveProvider() {
|
|
108
99
|
try {
|
|
109
100
|
const grant = await this.grant(), identity = grant.sha256 + grant.token;
|
|
101
|
+
if (this.original && identity !== this.identity)
|
|
102
|
+
return null;
|
|
110
103
|
if (identity !== this.identity) {
|
|
104
|
+
const admission = await NativeDeviceAdmission.connect(grant);
|
|
105
|
+
if (admission.hostLifetimeId !== grant.hostGeneration)
|
|
106
|
+
throw new NativeCommandError('unavailable');
|
|
107
|
+
this.original = grant;
|
|
108
|
+
this.admission = admission;
|
|
111
109
|
this.identity = identity;
|
|
112
110
|
this.provider = new NativeProvider({ helperPath: grant.executable, runtimeDir: this.directory, backend: 'videotoolbox',
|
|
113
|
-
|
|
111
|
+
deviceAdmission: admission, environment: { ...process.env, AXOM_KEEPER_SOCKET: grant.socket, AXOM_KEEPER_TOKEN: grant.token },
|
|
112
|
+
enumerate: () => this.capabilities(), sessionHelperFactory: async (binding) => HelperProcess.launch(grant.executable, { ...process.env, AXOM_KEEPER_TARGET: binding.displayId,
|
|
113
|
+
AXOM_KEEPER_SOCKET: grant.socket, AXOM_KEEPER_TOKEN: grant.token,
|
|
114
|
+
AXOM_DEVICE_CONTEXT: admission.context(binding.deviceGrant?.registrationId ?? '', binding.deviceGeneration, binding.sessionId, binding.deviceGrant) }, 'macos') });
|
|
114
115
|
}
|
|
115
116
|
return this.provider;
|
|
116
117
|
}
|
|
@@ -131,19 +132,19 @@ export class MacProvider {
|
|
|
131
132
|
}
|
|
132
133
|
return this.capabilities();
|
|
133
134
|
}
|
|
134
|
-
async prepare(selection, binding) {
|
|
135
|
+
async prepare(selection, binding, current) {
|
|
135
136
|
const provider = await this.liveProvider();
|
|
136
137
|
if (!provider)
|
|
137
138
|
throw new NativeCommandError('unavailable');
|
|
138
139
|
// NativeProvider.prepare performs fresh permission/geometry admission itself.
|
|
139
140
|
// Repeating full enumeration here added another WindowServer round trip per window.
|
|
140
|
-
return provider.prepare(selection, binding);
|
|
141
|
+
return provider.prepare(selection, binding, current);
|
|
141
142
|
}
|
|
142
|
-
async wallpaper(targetId) {
|
|
143
|
+
async wallpaper(targetId, pixels = { width: 1920, height: 1080 }) {
|
|
143
144
|
const grant = await this.grant();
|
|
144
145
|
if (!/^[a-f0-9]{32}$/.test(targetId))
|
|
145
146
|
throw new NativeCommandError('invalid_request');
|
|
146
|
-
const result = await execute(grant.executable, ['--wallpaper', targetId], { timeout: 8000, maxBuffer:
|
|
147
|
+
const result = await execute(grant.executable, ['--wallpaper', targetId, String(pixels.width), String(pixels.height)], { timeout: 8000, maxBuffer: 3 * 1024 * 1024,
|
|
147
148
|
env: { ...process.env, AXOM_KEEPER_SOCKET: grant.socket, AXOM_KEEPER_TOKEN: grant.token } });
|
|
148
149
|
return decodeWallpaper(result.stdout);
|
|
149
150
|
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { createConnection } from 'node:net';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { matchesDeviceGrant, nativeReceiptMatches } from './ownershipProtocol.js';
|
|
4
|
+
const identity = z.string().min(1).max(128);
|
|
5
|
+
const grantSchema = z.strictObject({ registrationId: identity, controllerId: identity, deviceEpoch: identity,
|
|
6
|
+
revision: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER), authorization: identity });
|
|
7
|
+
const receiptSchema = z.strictObject({ commandId: identity, hostLifetimeId: identity, action: z.enum(['admit', 'isolate']),
|
|
8
|
+
grant: grantSchema, admission: z.enum(['active', 'revoked']), physical: z.literal('completed') });
|
|
9
|
+
const registrationReceiptSchema = z.strictObject({ commandId: identity, registrationId: identity, deviceGeneration: identity,
|
|
10
|
+
hostLifetimeId: identity, physical: z.literal('completed'), admission: z.literal('revoked') });
|
|
11
|
+
const statusSchema = z.strictObject({ hostLifetimeId: identity, deviceEpoch: identity.nullable(), connected: z.boolean(), active: z.boolean(),
|
|
12
|
+
workers: z.number().int().min(0).max(1024), operations: z.number().int().min(0).max(64),
|
|
13
|
+
scopeCount: z.number().int().min(0).max(1024), scopeBudget: z.literal(1024), rotationRequired: z.boolean(), quiescent: z.boolean(),
|
|
14
|
+
}).refine(value => value.quiescent === (!value.active && value.workers === 0 && value.operations === 0) &&
|
|
15
|
+
value.rotationRequired === (value.scopeCount === value.scopeBudget || !value.connected));
|
|
16
|
+
const hostRetirementSchema = z.strictObject({ commandId: identity, hostLifetimeId: identity, action: z.literal('retireHost'),
|
|
17
|
+
admission: z.literal('revoked'), physical: z.literal('completed') });
|
|
18
|
+
export function nativeDeviceRequest(original) {
|
|
19
|
+
const endpoint = { ...original };
|
|
20
|
+
return value => new Promise((resolve, reject) => {
|
|
21
|
+
const socket = createConnection(endpoint.socket);
|
|
22
|
+
let authenticated = false, settled = false, bytes = Buffer.alloc(0);
|
|
23
|
+
const finish = (error, result) => {
|
|
24
|
+
if (settled)
|
|
25
|
+
return;
|
|
26
|
+
settled = true;
|
|
27
|
+
clearTimeout(timer);
|
|
28
|
+
socket.destroy();
|
|
29
|
+
if (error)
|
|
30
|
+
reject(error);
|
|
31
|
+
else
|
|
32
|
+
resolve(result);
|
|
33
|
+
};
|
|
34
|
+
const timer = setTimeout(() => finish(new Error('device_admission_unconfirmed')), 5000);
|
|
35
|
+
socket.once('connect', () => socket.write(endpoint.token));
|
|
36
|
+
socket.on('error', error => finish(error));
|
|
37
|
+
socket.on('end', () => finish(new Error('device_admission_unconfirmed')));
|
|
38
|
+
socket.on('data', chunk => {
|
|
39
|
+
bytes = Buffer.concat([bytes, chunk]);
|
|
40
|
+
if (!authenticated) {
|
|
41
|
+
if (bytes[0] !== 1) {
|
|
42
|
+
finish(new Error('device_admission_unconfirmed'));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
authenticated = true;
|
|
46
|
+
bytes = bytes.subarray(1);
|
|
47
|
+
socket.write(`device ${JSON.stringify(value)}\n`);
|
|
48
|
+
}
|
|
49
|
+
if (bytes.length > 8192) {
|
|
50
|
+
finish(new Error('device_admission_unconfirmed'));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const newline = bytes.indexOf(10);
|
|
54
|
+
if (newline < 0)
|
|
55
|
+
return;
|
|
56
|
+
try {
|
|
57
|
+
finish(undefined, JSON.parse(bytes.subarray(0, newline).toString('utf8')));
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
finish(new Error('device_admission_unconfirmed'));
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
export class NativeDeviceAdmission {
|
|
66
|
+
hostLifetimeId;
|
|
67
|
+
request;
|
|
68
|
+
live = true;
|
|
69
|
+
nativeRetired = false;
|
|
70
|
+
retirementCommandId;
|
|
71
|
+
epoch;
|
|
72
|
+
retiredRevision = -1;
|
|
73
|
+
installed = new Map();
|
|
74
|
+
obligations = new Map();
|
|
75
|
+
revocations = new Map();
|
|
76
|
+
constructor(hostLifetimeId, request) {
|
|
77
|
+
this.hostLifetimeId = hostLifetimeId;
|
|
78
|
+
this.request = request;
|
|
79
|
+
identity.parse(hostLifetimeId);
|
|
80
|
+
}
|
|
81
|
+
static async connect(original) {
|
|
82
|
+
const request = nativeDeviceRequest(original);
|
|
83
|
+
const hello = z.strictObject({ hostLifetimeId: identity, deviceAdmissionProtocol: z.literal(1) }).parse(await request({ action: 'hello' }));
|
|
84
|
+
return new NativeDeviceAdmission(hello.hostLifetimeId, request);
|
|
85
|
+
}
|
|
86
|
+
current() { return this.live; }
|
|
87
|
+
async bindDeviceEpoch(commandId, deviceEpoch) {
|
|
88
|
+
identity.parse(deviceEpoch);
|
|
89
|
+
if (!this.live || this.epoch && this.epoch !== deviceEpoch)
|
|
90
|
+
throw Error('control_lost');
|
|
91
|
+
const schema = z.strictObject({ commandId: identity, hostLifetimeId: identity, action: z.literal('bindDeviceEpoch'), deviceEpoch: identity });
|
|
92
|
+
const receipt = schema.parse(await this.request({ action: 'bindDeviceEpoch', commandId, hostLifetimeId: this.hostLifetimeId, deviceEpoch }));
|
|
93
|
+
if (!this.live || receipt.commandId !== commandId || receipt.hostLifetimeId !== this.hostLifetimeId || receipt.deviceEpoch !== deviceEpoch)
|
|
94
|
+
throw Error('device_admission_unconfirmed');
|
|
95
|
+
this.epoch = deviceEpoch;
|
|
96
|
+
}
|
|
97
|
+
retire() { this.live = false; for (const grant of this.obligations.values())
|
|
98
|
+
this.revoke(grant); }
|
|
99
|
+
key(grant) { return JSON.stringify(grantSchema.parse(grant)); }
|
|
100
|
+
async admit(commandId, grant) {
|
|
101
|
+
const key = this.key(grant);
|
|
102
|
+
if (!this.live || grant.revision <= this.retiredRevision || (this.epoch && this.epoch !== grant.deviceEpoch))
|
|
103
|
+
throw new Error('control_lost');
|
|
104
|
+
const original = grantSchema.parse(grant);
|
|
105
|
+
this.epoch = original.deviceEpoch;
|
|
106
|
+
this.obligations.set(key, original);
|
|
107
|
+
try {
|
|
108
|
+
const receipt = receiptSchema.parse(await this.request({ action: 'admit', commandId, grant: original, hostLifetimeId: this.hostLifetimeId }));
|
|
109
|
+
if (!this.live || original.revision <= this.retiredRevision || !nativeReceiptMatches(this, commandId, original, 'admit', receipt))
|
|
110
|
+
throw new Error('device_admission_unconfirmed');
|
|
111
|
+
this.installed.set(key, original);
|
|
112
|
+
return receipt;
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
if (this.nativeRetired)
|
|
116
|
+
this.obligations.delete(key);
|
|
117
|
+
else
|
|
118
|
+
this.revoke(original);
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
revoke(grant) {
|
|
123
|
+
const key = this.key(grant);
|
|
124
|
+
if (this.nativeRetired)
|
|
125
|
+
return;
|
|
126
|
+
if (this.epoch && this.epoch !== grant.deviceEpoch)
|
|
127
|
+
throw new Error('control_lost');
|
|
128
|
+
this.epoch = grant.deviceEpoch;
|
|
129
|
+
this.retiredRevision = Math.max(this.retiredRevision, grant.revision);
|
|
130
|
+
this.installed.delete(key);
|
|
131
|
+
this.obligations.set(key, grantSchema.parse(grant));
|
|
132
|
+
if (this.revocations.has(key))
|
|
133
|
+
return;
|
|
134
|
+
const pending = this.request({ action: 'revoke', grant: grantSchema.parse(grant), hostLifetimeId: this.hostLifetimeId }).then(value => z.strictObject({}).parse(value));
|
|
135
|
+
this.revocations.set(key, pending);
|
|
136
|
+
void pending.catch(() => { if (this.revocations.get(key) === pending)
|
|
137
|
+
this.revocations.delete(key); });
|
|
138
|
+
}
|
|
139
|
+
async isolate(commandId, grant) {
|
|
140
|
+
this.revoke(grant);
|
|
141
|
+
await this.revocations.get(this.key(grant))?.catch(() => { });
|
|
142
|
+
const original = grantSchema.parse(grant);
|
|
143
|
+
const receipt = receiptSchema.parse(await this.request({ action: 'isolate', commandId, grant: original, hostLifetimeId: this.hostLifetimeId }));
|
|
144
|
+
if (!nativeReceiptMatches(this, commandId, original, 'isolate', receipt))
|
|
145
|
+
throw new Error('device_admission_unconfirmed');
|
|
146
|
+
const key = this.key(original);
|
|
147
|
+
this.installed.delete(key);
|
|
148
|
+
this.obligations.delete(key);
|
|
149
|
+
this.revocations.delete(key);
|
|
150
|
+
return receipt;
|
|
151
|
+
}
|
|
152
|
+
async isolateRegistration(commandId, registrationId, deviceGeneration) {
|
|
153
|
+
const receipt = registrationReceiptSchema.parse(await this.request({ action: 'isolateRegistration', commandId, registrationId,
|
|
154
|
+
deviceGeneration, hostLifetimeId: this.hostLifetimeId }));
|
|
155
|
+
if (receipt.commandId !== commandId || receipt.registrationId !== registrationId || receipt.deviceGeneration !== deviceGeneration ||
|
|
156
|
+
receipt.hostLifetimeId !== this.hostLifetimeId)
|
|
157
|
+
throw new Error('device_admission_unconfirmed');
|
|
158
|
+
return receipt;
|
|
159
|
+
}
|
|
160
|
+
async status() {
|
|
161
|
+
const status = statusSchema.parse(await this.request({ action: 'status', hostLifetimeId: this.hostLifetimeId }));
|
|
162
|
+
if (status.hostLifetimeId !== this.hostLifetimeId)
|
|
163
|
+
throw new Error('device_admission_unconfirmed');
|
|
164
|
+
return status;
|
|
165
|
+
}
|
|
166
|
+
async retireHost(commandId) {
|
|
167
|
+
const receipt = hostRetirementSchema.parse(await this.request({ action: 'retireHost', commandId, hostLifetimeId: this.hostLifetimeId }));
|
|
168
|
+
if (receipt.commandId !== commandId || receipt.hostLifetimeId !== this.hostLifetimeId)
|
|
169
|
+
throw new Error('device_admission_unconfirmed');
|
|
170
|
+
this.nativeRetired = true;
|
|
171
|
+
this.live = false;
|
|
172
|
+
this.installed.clear();
|
|
173
|
+
this.obligations.clear();
|
|
174
|
+
this.revocations.clear();
|
|
175
|
+
this.retirementCommandId = commandId;
|
|
176
|
+
return receipt;
|
|
177
|
+
}
|
|
178
|
+
async stopHost(commandId) {
|
|
179
|
+
const schema = z.strictObject({ commandId: identity, hostLifetimeId: identity, action: z.literal('stopHost'), admission: z.literal('revoked'), physical: z.literal('completed') });
|
|
180
|
+
const receipt = schema.parse(await this.request({ action: 'stopHost', commandId, hostLifetimeId: this.hostLifetimeId }));
|
|
181
|
+
if (receipt.commandId !== commandId || receipt.hostLifetimeId !== this.hostLifetimeId)
|
|
182
|
+
throw new Error('device_admission_unconfirmed');
|
|
183
|
+
this.live = false;
|
|
184
|
+
this.installed.clear();
|
|
185
|
+
return receipt;
|
|
186
|
+
}
|
|
187
|
+
async acknowledgeHostRetirement(commandId) {
|
|
188
|
+
if (!this.nativeRetired || !this.retirementCommandId)
|
|
189
|
+
throw new Error('device_admission_unconfirmed');
|
|
190
|
+
const schema = z.strictObject({ commandId: identity, hostLifetimeId: identity, action: z.literal('acknowledgeHostRetirement'), admission: z.literal('revoked'), physical: z.literal('completed') });
|
|
191
|
+
const receipt = schema.parse(await this.request({ action: 'acknowledgeHostRetirement', commandId, retirementCommandId: this.retirementCommandId, hostLifetimeId: this.hostLifetimeId }));
|
|
192
|
+
if (receipt.commandId !== commandId || receipt.hostLifetimeId !== this.hostLifetimeId)
|
|
193
|
+
throw new Error('device_admission_unconfirmed');
|
|
194
|
+
return receipt;
|
|
195
|
+
}
|
|
196
|
+
context(registrationId, deviceGeneration, consumerId, grant) {
|
|
197
|
+
identity.parse(registrationId);
|
|
198
|
+
identity.parse(deviceGeneration);
|
|
199
|
+
identity.parse(consumerId);
|
|
200
|
+
if (!this.live || !this.epoch)
|
|
201
|
+
throw new Error('control_lost');
|
|
202
|
+
if (grant && (grant.registrationId !== registrationId || grant.controllerId !== consumerId ||
|
|
203
|
+
![...this.installed.values()].some(value => matchesDeviceGrant(value, grant))))
|
|
204
|
+
throw new Error('control_lost');
|
|
205
|
+
return JSON.stringify({ hostLifetimeId: this.hostLifetimeId, deviceEpoch: this.epoch, registrationId, deviceGeneration, consumerId, grant: grant ?? null });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -10,6 +10,7 @@ import { DesktopMediaStream, decodeMedia, desktopMediaLane, mediaRequire } from
|
|
|
10
10
|
import { canonicalDesktopJson, decodeCapabilityReport, decodeDesktopControl, supportsDesktopMode, captureRegion } from './protocol.js';
|
|
11
11
|
import { captureSamplingModes } from './captureModes.js';
|
|
12
12
|
import { captureTargetLabel } from './captureTargetLabel.js';
|
|
13
|
+
import { launchAutomationHelper } from './helperProcess.js';
|
|
13
14
|
import { XfceWallpaperReader, decodeWallpaper } from './wallpaper.js';
|
|
14
15
|
const execute = promisify(execFile);
|
|
15
16
|
const totals = ['captured', 'encoded', 'videoPayloadBytes', 'headerBytes', 'rateReopens'];
|
|
@@ -51,6 +52,15 @@ class ViewerLifetime {
|
|
|
51
52
|
/** No helper/session construction in discovery. Only the image's admitted software is offered. */
|
|
52
53
|
export class NativeProvider {
|
|
53
54
|
options;
|
|
55
|
+
async prepareAutomation(binding) {
|
|
56
|
+
await this.options.validateSession?.();
|
|
57
|
+
const admission = this.options.deviceAdmission;
|
|
58
|
+
if (binding.deviceGrant && !admission)
|
|
59
|
+
throw new NativeCommandError('control_lost');
|
|
60
|
+
const context = admission ? { AXOM_DEVICE_CONTEXT: admission.context(binding.registrationId ?? binding.deviceGrant?.registrationId ?? '', binding.route.deviceGeneration, binding.route.consumerId, binding.deviceGrant) } : {};
|
|
61
|
+
return launchAutomationHelper(this.executable, { ...this.environment, ...context }, binding);
|
|
62
|
+
}
|
|
63
|
+
get deviceAdmission() { return this.options.deviceAdmission; }
|
|
54
64
|
lifetimes = new Map();
|
|
55
65
|
revision = 1;
|
|
56
66
|
geometry;
|
|
@@ -66,6 +76,7 @@ export class NativeProvider {
|
|
|
66
76
|
this.displayId = '00000000000000000000000000000001';
|
|
67
77
|
this.executable = options.helperPath;
|
|
68
78
|
this.factory = options.helperFactory ?? (binding => HelperProcess.launch(this.executable, { ...this.environment,
|
|
79
|
+
...(binding && options.deviceAdmission ? { AXOM_DEVICE_CONTEXT: options.deviceAdmission.context(binding.deviceGrant?.registrationId ?? '', binding.deviceGeneration, binding.sessionId, binding.deviceGrant) } : {}),
|
|
69
80
|
...(binding ? { AXOM_KEEPER_TARGET: binding.displayId } : {}) }));
|
|
70
81
|
}
|
|
71
82
|
get environment() {
|
|
@@ -98,7 +109,7 @@ export class NativeProvider {
|
|
|
98
109
|
return next;
|
|
99
110
|
}
|
|
100
111
|
probe() { return this.withGeometry(() => this.probeCurrent()); }
|
|
101
|
-
async wallpaper(targetId) {
|
|
112
|
+
async wallpaper(targetId, pixels = { width: 1920, height: 1080 }) {
|
|
102
113
|
mediaRequire(/^[a-f0-9]{32}$/.test(targetId));
|
|
103
114
|
const capabilities = await this.probe();
|
|
104
115
|
mediaRequire(capabilities?.displays.some(display => display.id === targetId));
|
|
@@ -108,10 +119,10 @@ export class NativeProvider {
|
|
|
108
119
|
return result.stdout;
|
|
109
120
|
});
|
|
110
121
|
const discovered = await reader.discover();
|
|
111
|
-
const result = await execute(this.executable, ['--wallpaper-asset', discovered.file, discovered.background], { timeout: 4000, maxBuffer:
|
|
122
|
+
const result = await execute(this.executable, ['--wallpaper-asset', discovered.file, discovered.background, String(pixels.width), String(pixels.height)], { timeout: 4000, maxBuffer: 3 * 1024 * 1024, env: this.environment });
|
|
112
123
|
const wallpaper = decodeWallpaper(result.stdout);
|
|
113
124
|
return { asset: { ...wallpaper.asset, placement: discovered.placement,
|
|
114
|
-
background: discovered.background, fidelity: discovered.fidelity === 'approximate' ? 'approximate' : wallpaper.asset.fidelity },
|
|
125
|
+
background: discovered.background, fidelity: discovered.fidelity === 'approximate' ? 'approximate' : wallpaper.asset.fidelity }, bytes: wallpaper.bytes };
|
|
115
126
|
}
|
|
116
127
|
async probeCurrent() {
|
|
117
128
|
if (this.options.enumerate)
|
|
@@ -192,9 +203,16 @@ export class NativeProvider {
|
|
|
192
203
|
return actual;
|
|
193
204
|
});
|
|
194
205
|
}
|
|
195
|
-
async prepare(selection, binding) {
|
|
206
|
+
async prepare(selection, binding, current) {
|
|
207
|
+
const admitted = (beginEffect = false) => { if (current?.(beginEffect) === false)
|
|
208
|
+
throw new NativeCommandError('busy'); };
|
|
209
|
+
admitted();
|
|
210
|
+
if (binding.deviceGrant && !this.options.deviceAdmission)
|
|
211
|
+
throw new NativeCommandError('control_lost');
|
|
196
212
|
await this.options.validateSession?.();
|
|
213
|
+
admitted();
|
|
197
214
|
const caps = await this.probe();
|
|
215
|
+
admitted();
|
|
198
216
|
mediaRequire(caps?.health === 'ready' && ['granted', 'not_applicable'].includes(caps.permission));
|
|
199
217
|
if (selection.cursorVisible !== undefined)
|
|
200
218
|
mediaRequire(caps.cursorVisibility === true);
|
|
@@ -225,14 +243,19 @@ export class NativeProvider {
|
|
|
225
243
|
life = new ViewerLifetime();
|
|
226
244
|
this.lifetimes.set(binding.sessionId, life);
|
|
227
245
|
}
|
|
228
|
-
|
|
246
|
+
admitted();
|
|
247
|
+
if (life.session) {
|
|
248
|
+
admitted(true);
|
|
229
249
|
await life.session.stop();
|
|
250
|
+
}
|
|
251
|
+
admitted();
|
|
230
252
|
life.bind(binding);
|
|
231
253
|
let revision = Math.max(this.revision, display.revision);
|
|
232
254
|
if ((display.captureViewport !== true && (selection.width !== display.width || selection.height !== display.height)) || selection.dpi !== undefined) {
|
|
233
255
|
const physicalWidth = display.captureViewport === true ? display.width : selection.width;
|
|
234
256
|
const physicalHeight = display.captureViewport === true ? display.height : selection.height;
|
|
235
257
|
const actual = await this.geometryTransaction(async (displayRevision) => {
|
|
258
|
+
admitted(true);
|
|
236
259
|
const { stdout } = await execute(this.displayControl, ['--transaction', `${physicalWidth}x${physicalHeight}${selection.dpi === undefined ? '' : '@' + selection.dpi}`], { env: this.environment, timeout: 4500, maxBuffer: 4096 });
|
|
237
260
|
const result = z.object({ outcome: z.literal('applied'), width: z.literal(physicalWidth), height: z.literal(physicalHeight),
|
|
238
261
|
dpi: selection.dpi === undefined ? z.number().optional() : z.literal(selection.dpi) }).parse(JSON.parse(stdout));
|
|
@@ -240,12 +263,15 @@ export class NativeProvider {
|
|
|
240
263
|
});
|
|
241
264
|
revision = actual.displayRevision;
|
|
242
265
|
}
|
|
266
|
+
admitted();
|
|
243
267
|
const helper = await (this.options.sessionHelperFactory?.(binding) ?? this.factory(binding));
|
|
244
268
|
const identity = { sessionId: binding.sessionId, deviceGeneration: binding.deviceGeneration,
|
|
245
269
|
controlGeneration: binding.controlGeneration, displayRevision: revision };
|
|
246
270
|
const reserved = life.reserve();
|
|
247
271
|
try {
|
|
272
|
+
admitted();
|
|
248
273
|
mediaRequire((await helper.request('verify_cleanup', identity)).result.verified === true);
|
|
274
|
+
admitted(true);
|
|
249
275
|
const payload = { ...(selection.cursorVisible === undefined ? {} : { cursorVisible: selection.cursorVisible }), ...(effectiveRegion ? { captureRegion: effectiveRegion } : {}), width: selection.width, height: selection.height, fpsCeiling: life.prepared ? Math.min(life.appliedFps, selection.fpsCeiling) : selection.fpsCeiling,
|
|
250
276
|
rateTargetBps: life.appliedTargetBps, quality: selection.quality, backend: this.options.backend ?? 'libx264', leaseMs: 30000, representation: 1,
|
|
251
277
|
...reserved, ...(life.prepared ? { continuation: { initialReservoirBits: life.initialReservoirBits, inputSequence: life.inputSequence,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
const bounded = z.string().min(1).max(128).regex(/^[^\x00-\x1f\x7f]+$/);
|
|
3
|
+
const revision = z.number().int().min(0).max(Number.MAX_SAFE_INTEGER);
|
|
4
|
+
export const deviceAcquisitionSchema = z.discriminatedUnion('kind', [
|
|
5
|
+
z.strictObject({ kind: z.literal('if_free') }),
|
|
6
|
+
z.strictObject({ kind: z.literal('takeover'), expectedDeviceEpoch: bounded, expectedRevision: revision }),
|
|
7
|
+
]);
|
|
8
|
+
export const deviceOwnershipSnapshotSchema = z.strictObject({ deviceEpoch: bounded, revision,
|
|
9
|
+
state: z.enum(['free', 'draining', 'active']),
|
|
10
|
+
controller: z.strictObject({ registrationId: bounded, controllerId: bounded, kind: bounded }).nullable(),
|
|
11
|
+
});
|
|
12
|
+
export const deviceOwnershipCommandSchema = z.strictObject({ t: z.literal('desktop_ownership'), ownershipProtocol: z.literal(1),
|
|
13
|
+
commandId: z.string().regex(/^[0-9a-f]{32}$/), deviceGeneration: bounded, controllerId: bounded,
|
|
14
|
+
kind: z.enum(['viewer', 'automation']), action: z.enum(['acquire', 'commit', 'release', 'cancel']),
|
|
15
|
+
requestSequence: revision.refine(value => value > 0).optional(), acquireCommandId: z.string().regex(/^[0-9a-f]{32}$/).optional(),
|
|
16
|
+
acquisition: deviceAcquisitionSchema.optional(), deviceEpoch: bounded.optional(), revision: revision.optional(),
|
|
17
|
+
}).refine(value => value.action === 'acquire' ? value.acquisition != null && value.requestSequence != null :
|
|
18
|
+
value.action === 'cancel' ? value.acquireCommandId != null && value.requestSequence != null : value.deviceEpoch != null && value.revision != null);
|
|
19
|
+
const reservationCauseSchema = z.strictObject({ acquireCommandId: z.string().regex(/^[0-9a-f]{32}$/), contenderControllerId: bounded, deviceGeneration: bounded });
|
|
20
|
+
export const deviceOwnershipEventSchema = z.strictObject({ t: z.literal('desktop_ownership_result'), ownershipProtocol: z.literal(1),
|
|
21
|
+
commandId: z.string().regex(/^[0-9a-f]{32}$/), deviceGeneration: bounded, controllerId: bounded,
|
|
22
|
+
status: z.enum(['acquired', 'committed', 'released', 'cancelled', 'busy', 'conflict', 'unsupported', 'lost', 'snapshot']),
|
|
23
|
+
snapshot: deviceOwnershipSnapshotSchema, reservationCause: reservationCauseSchema.optional(),
|
|
24
|
+
}).refine(value => value.reservationCause == null || value.status === 'lost');
|
|
25
|
+
export const matchesDeviceGrant = (left, right) => left.deviceEpoch === right.deviceEpoch && left.revision === right.revision && left.registrationId === right.registrationId &&
|
|
26
|
+
left.controllerId === right.controllerId && left.authorization === right.authorization;
|
|
27
|
+
export function nativeReceiptMatches(handle, commandId, grant, action, receipt) {
|
|
28
|
+
return (action === 'isolate' || handle.current()) && receipt.commandId === commandId && receipt.hostLifetimeId === handle.hostLifetimeId &&
|
|
29
|
+
receipt.action === action && receipt.physical === 'completed' && receipt.admission === (action === 'admit' ? 'active' : 'revoked') &&
|
|
30
|
+
matchesDeviceGrant(grant, receipt.grant);
|
|
31
|
+
}
|