@platless/axom-cli 0.5.9 → 0.6.36
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 +18 -5
- package/dist/axom-patch/cli/README.md +100 -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/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/codex-terminal-output.mjs +33 -0
- package/dist/axom-patch/cli/probes/kimi-subagent-subwork.mjs +82 -0
- package/dist/axom-patch/cli/qoder/dist/axom-acp-compat.mjs +1591 -0
- package/dist/axom-patch/cli/qoder/releases/1.1.75.json +360 -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 +16 -18
- package/dist/axom-patch/cli/zcode-account-limits.json +10 -11
- package/dist/axom-patch/cli/zcode-models.json +1 -2
- package/dist/axom-patch/cli/zcode-subagent-subwork.json +9 -10
- package/dist/cli.js +4 -36
- package/dist/commands/knowledge.js +47 -0
- package/dist/commands/update.js +65 -3
- package/dist/commands/workstation.js +73 -7
- package/dist/install/claude/commands.js +13 -13
- package/dist/mcp.js +1 -1
- package/dist/native/darwin-arm64/axom-quic +0 -0
- package/dist/workstation/acp/acpAgents.js +102 -98
- 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/patchAssets.js +40 -0
- package/dist/workstation/acp/processor/checkpoint.js +25 -34
- package/dist/workstation/acp/processor/control.js +5 -78
- package/dist/workstation/acp/processor/runtime.js +5 -81
- package/dist/workstation/acp/processor/session.js +51 -80
- package/dist/workstation/acp/processor/state.js +6 -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/updates.js +131 -105
- package/dist/workstation/acp/processorSession.js +8 -172
- 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 +28 -0
- 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 +38 -143
- package/dist/workstation/channels/management.js +95 -11
- package/dist/workstation/channels/url.js +1 -1
- package/dist/workstation/config.js +16 -4
- package/dist/workstation/connection/encryptedChannel.js +4 -1
- package/dist/workstation/control/bindingStore.js +200 -0
- package/dist/workstation/control/commands.js +124 -0
- package/dist/workstation/control/daemonIdentity.js +53 -0
- package/dist/workstation/control/enrollment.js +106 -0
- package/dist/workstation/control/inspect.js +71 -0
- package/dist/workstation/control/locks.js +63 -0
- package/dist/workstation/control/protocol.js +40 -0
- package/dist/workstation/control/restartGuard.js +46 -0
- package/dist/workstation/control/runtimeClient.js +97 -0
- package/dist/workstation/control/runtimeHost.js +148 -0
- package/dist/workstation/control/serverIdentity.js +35 -0
- package/dist/workstation/daemon.js +106 -6
- package/dist/workstation/daemonctl.js +77 -14
- package/dist/workstation/desktopControl/audio.js +99 -0
- package/dist/workstation/desktopControl/capabilities.js +92 -0
- package/dist/workstation/desktopControl/captureModes.js +9 -0
- package/dist/workstation/desktopControl/captureTargetLabel.js +9 -0
- package/dist/workstation/desktopControl/deviceSockets.js +246 -0
- package/dist/workstation/desktopControl/helperProcess.js +353 -0
- package/dist/workstation/desktopControl/lobby.js +129 -0
- package/dist/workstation/desktopControl/macProvider.js +155 -0
- package/dist/workstation/desktopControl/media.js +331 -0
- package/dist/workstation/desktopControl/nativeProvider.js +1018 -0
- package/dist/workstation/desktopControl/networkEvidence.js +43 -0
- package/dist/workstation/desktopControl/overview.js +58 -0
- package/dist/workstation/desktopControl/protocol.js +302 -0
- package/dist/workstation/desktopControl/quicMedia.js +240 -0
- package/dist/workstation/desktopControl/rateController.js +226 -0
- package/dist/workstation/desktopControl/supervisor.js +1681 -0
- package/dist/workstation/desktopControl/transport.js +27 -0
- package/dist/workstation/desktopControl/wallpaper.js +78 -0
- package/dist/workstation/files/credentials.js +142 -0
- package/dist/workstation/files/text.js +42 -8
- package/dist/workstation/keepers/kit/bundle.js +21 -17
- package/dist/workstation/keepers/kit/launcher.js +47 -7
- package/dist/workstation/keepers/kit/paths.js +2 -1
- package/dist/workstation/managedInstall.js +57 -0
- package/dist/workstation/protocol/agent.js +2 -15
- package/dist/workstation/protocol/management.js +93 -4
- package/dist/workstation/protocol/runtimeDiagnostics.js +15 -0
- package/dist/workstation/rd/artifact.js +119 -0
- package/dist/workstation/rd/control.js +119 -0
- package/dist/workstation/rd/display.js +29 -0
- package/dist/workstation/rd/provider.js +222 -0
- package/dist/workstation/rd/sessions.js +391 -0
- package/dist/workstation/rd/state.js +45 -0
- package/dist/workstation/rd/update.js +171 -0
- package/dist/workstation/rd/upkeep.js +38 -0
- package/dist/workstation/register.js +27 -37
- package/dist/workstation/service/installer.js +104 -13
- package/dist/workstation/service/launchd.js +1 -0
- package/dist/workstation/storageMetrics.js +118 -0
- package/dist/workstation/systemMetrics.js +44 -92
- package/dist/workstation/toolchain.js +94 -0
- package/dist/workstation/tools/browser.js +28 -4
- package/dist/workstation/update/artifact.js +2 -1
- package/dist/workstation/update/globalInstall.js +116 -0
- package/dist/workstation/update/npmInstaller.js +49 -23
- package/dist/workstation/update/rdRollback.js +145 -0
- package/dist/workstation/update/runUpdate.js +3 -2
- package/dist/workstation/update/watchdog.js +16790 -334
- package/package.json +5 -5
- 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/provider.js} +0 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import { spawn, execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { DesktopProtocolError, parseDesktopJson } from './protocol.js';
|
|
6
|
+
import { decodeMedia, mediaRequire } from './media.js';
|
|
7
|
+
import { decodeAudio } from './audio.js';
|
|
8
|
+
const execute = promisify(execFile);
|
|
9
|
+
const commands = ['focus', 'cache_dependency', 'transport_cost', 'prepare_motion_cut', 'commit_motion_cut', 'prepare', 'start', 'decoder_ready', 'first_presented', 'acknowledge', 'credit', 'renew', 'input', 'clipboard', 'overview', 'reserve_overview', 'resize',
|
|
10
|
+
'held_state', 'text_result', 'recover_display', 'set_rate', 'refresh', 'stop', 'verify_cleanup', 'probe', 'status'];
|
|
11
|
+
const safe = z.number().int().min(0).max(Number.MAX_SAFE_INTEGER);
|
|
12
|
+
const handshakeSchema = z.strictObject({ ipc: z.literal(1), providerVersion: z.literal('axom-linux-1'),
|
|
13
|
+
representations: z.tuple([z.literal(1)]),
|
|
14
|
+
inputAbi: z.literal(1), rateAbi: z.literal(1), continuationAbi: z.literal(1), motionRecoveryAbi: z.literal(1), runtimeRateReconfigure: z.literal(false),
|
|
15
|
+
rateLimits: z.strictObject({ minBps: z.literal(500000), maxBps: z.literal(20000000), stepBps: z.literal(4000), maxFps: z.literal(60) }),
|
|
16
|
+
supportedCommands: z.array(z.string()).refine(a => commands.every(c => a.includes(c))) });
|
|
17
|
+
export function validateNativeHandshake(value, platform = 'linux') {
|
|
18
|
+
(platform === 'linux' ? handshakeSchema : handshakeSchema.extend({ providerVersion: z.literal('axom-macos-1'),
|
|
19
|
+
rateLimits: handshakeSchema.shape.rateLimits.extend({ maxFps: z.literal(30) }) })).parse(value);
|
|
20
|
+
}
|
|
21
|
+
/** One waiter, pulled reads: a blocked media consumer never blocks the control reader. */
|
|
22
|
+
export class NativeByteReader {
|
|
23
|
+
max;
|
|
24
|
+
iterator;
|
|
25
|
+
bytes = Buffer.alloc(0);
|
|
26
|
+
constructor(stream, max = 65600) {
|
|
27
|
+
this.max = max;
|
|
28
|
+
this.iterator = stream[Symbol.asyncIterator]();
|
|
29
|
+
}
|
|
30
|
+
async exact(size) {
|
|
31
|
+
mediaRequire(Number.isInteger(size) && size > 0 && size <= this.max);
|
|
32
|
+
while (this.bytes.length < size) {
|
|
33
|
+
const next = await this.iterator.next();
|
|
34
|
+
if (next.done)
|
|
35
|
+
throw new DesktopProtocolError('Native stream ended');
|
|
36
|
+
mediaRequire(Buffer.isBuffer(next.value) && this.bytes.length + next.value.length <= this.max + 65536);
|
|
37
|
+
this.bytes = Buffer.concat([this.bytes, next.value]);
|
|
38
|
+
}
|
|
39
|
+
const value = this.bytes.subarray(0, size);
|
|
40
|
+
this.bytes = this.bytes.subarray(size);
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export class NativeControlReader {
|
|
45
|
+
reader;
|
|
46
|
+
constructor(stream) { this.reader = new NativeByteReader(stream, 49152); }
|
|
47
|
+
async read() {
|
|
48
|
+
const size = (await this.reader.exact(4)).readUInt32BE();
|
|
49
|
+
mediaRequire(size > 0 && size <= 49152);
|
|
50
|
+
// lastEncodeMs is the sole fractional native counter.
|
|
51
|
+
const bytes = await this.reader.exact(size);
|
|
52
|
+
return parseDesktopJson(bytes, 49152, nativeIntegerFields);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const nativeIntegerFields = new Set(['ipc', 'id', 'epoch', 'width', 'height', 'dpi', 'fpsCeiling', 'targetBps', 'rateTargetBps',
|
|
56
|
+
'motionRecoveryAbi', 'cutId', 'pictureThrough', 'frameThrough', 'continuationAbi', 'inputAbi', 'rateAbi', 'representations', 'representation', 'minBps', 'maxBps', 'stepBps', 'maxFps',
|
|
57
|
+
'maxRateBps', 'bufferBits', 'maxBFrames', 'nextFrameId', 'frameId', 'leaseMs', 'motionConsumedBytes', 'cursorConsumedBytes', 'exactConsumedBytes', 'windowBytes',
|
|
58
|
+
'hrdBitRate', 'hrdCpbSize', 'spsWidth', 'spsHeight', 'spsMaxReorderFrames',
|
|
59
|
+
'rcModeValue', 'blockLevelRateControl', 'spsMaxRateBps', 'spsBufferBits',
|
|
60
|
+
'controlGeneration', 'displayRevision', 'sequence', 'lastAllocatedFrameId', 'frameIdLimit', 'captured', 'encoded', 'sentPayloadBytes',
|
|
61
|
+
'videoPayloadBytes', 'headerBytes', 'rateReopens', 'refinementFrames', 'motionContinuations', 'pendingPayloadBytes', 'rateCooldownUntilMs', 'nowMs', 'initialAtMs',
|
|
62
|
+
'cacheId', 'exactSequence', 'initialReservoirBits', 'capacityBits', 'availableBits', 'integratedTargetBits', 'transportBytes', 'debtBits', 'appliedAtMonotonicMs', 'lastInputSequence', 'lastAppliedInputSequence', 'wireBytes',
|
|
63
|
+
'sourceCaptureUs', 'captureBeginUs', 'captureEndUs', 'captureCompareBeginUs', 'captureCompareEndUs', 'captureCopyBeginUs', 'captureCopyEndUs', 'encodeBeginUs', 'encodeEndUs', 'committedUs']);
|
|
64
|
+
const budgetSchema = z.strictObject({ nowMs: safe, initialAtMs: safe, initialReservoirBits: safe, capacityBits: safe,
|
|
65
|
+
availableBits: safe, integratedTargetBits: safe, transportBytes: safe, debtBits: safe });
|
|
66
|
+
const frameTimingSchema = z.strictObject({ clock: z.literal('steady_clock'), epoch: safe.min(1), frameId: safe.min(1), repair: z.boolean(),
|
|
67
|
+
sourceCaptureUs: safe, captureBeginUs: safe, captureEndUs: safe, captureCompareBeginUs: safe.default(0), captureCompareEndUs: safe.default(0),
|
|
68
|
+
captureCopyBeginUs: safe.default(0), captureCopyEndUs: safe.default(0), encodeBeginUs: safe, encodeEndUs: safe, committedUs: safe,
|
|
69
|
+
}).refine(timing => timing.encodeBeginUs <= timing.encodeEndUs && timing.encodeEndUs <= timing.committedUs &&
|
|
70
|
+
(timing.repair ? timing.sourceCaptureUs === 0 && timing.captureBeginUs === 0 && timing.captureEndUs === 0 :
|
|
71
|
+
timing.captureBeginUs <= timing.captureEndUs && timing.captureEndUs <= timing.encodeBeginUs &&
|
|
72
|
+
(timing.captureCompareBeginUs === 0 && timing.captureCompareEndUs === 0 && timing.captureCopyBeginUs === 0 && timing.captureCopyEndUs === 0 ||
|
|
73
|
+
timing.captureCompareBeginUs <= timing.captureCompareEndUs &&
|
|
74
|
+
timing.captureCompareEndUs <= timing.captureCopyBeginUs && timing.captureCopyBeginUs <= timing.captureCopyEndUs &&
|
|
75
|
+
timing.captureCopyEndUs <= timing.captureEndUs)), 'Invalid native frame timing');
|
|
76
|
+
const countersSchema = z.strictObject({ epoch: safe, lastAllocatedFrameId: safe, frameIdLimit: safe, captured: safe, encoded: safe,
|
|
77
|
+
sentPayloadBytes: safe, videoPayloadBytes: safe, headerBytes: safe, rateReopens: safe, targetBps: safe, fpsCeiling: safe,
|
|
78
|
+
pauseCapture: z.boolean(), lastEncodeMs: z.number().finite().min(0), lastFrameTiming: frameTimingSchema.nullable(), refinementFrames: safe.optional(), motionContinuations: safe.optional(), pendingPayloadBytes: safe, rateCooldownUntilMs: safe,
|
|
79
|
+
rateBudget: budgetSchema.optional(), inputAvailable: z.boolean(), lastInputSequence: safe });
|
|
80
|
+
const replySchema = z.union([
|
|
81
|
+
z.strictObject({ ipc: z.literal(1), id: safe, ok: z.literal(true), result: z.record(z.string(), z.unknown()), counters: countersSchema }),
|
|
82
|
+
z.strictObject({ ipc: z.literal(1), id: safe, ok: z.literal(false), code: z.enum(['invalid_request', 'unavailable', 'conflict', 'busy',
|
|
83
|
+
'rate_limited', 'authorization_lost', 'encoder_failed']), counters: countersSchema }),
|
|
84
|
+
]);
|
|
85
|
+
export class NativeCommandError extends Error {
|
|
86
|
+
code;
|
|
87
|
+
counters;
|
|
88
|
+
constructor(code, counters) {
|
|
89
|
+
super(code);
|
|
90
|
+
this.code = code;
|
|
91
|
+
this.counters = counters;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** Local queue retirement, created only before allocating an ID or writing bytes. */
|
|
95
|
+
class NativeCommandCancelled extends NativeCommandError {
|
|
96
|
+
constructor() { super('cancelled'); }
|
|
97
|
+
}
|
|
98
|
+
export async function bounded(promise, ms, code = 'transport_failed') {
|
|
99
|
+
let timer;
|
|
100
|
+
try {
|
|
101
|
+
return await Promise.race([promise, new Promise((_, reject) => { timer = setTimeout(() => reject(new NativeCommandError(code)), ms); })]);
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
clearTimeout(timer);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
export class HelperProcess {
|
|
108
|
+
executable;
|
|
109
|
+
env;
|
|
110
|
+
child;
|
|
111
|
+
reader;
|
|
112
|
+
media;
|
|
113
|
+
audio;
|
|
114
|
+
audioSupported = false;
|
|
115
|
+
id = 0;
|
|
116
|
+
queued = 0;
|
|
117
|
+
tail = Promise.resolve();
|
|
118
|
+
stopped = false;
|
|
119
|
+
termination;
|
|
120
|
+
pending = new Map();
|
|
121
|
+
active = new Set();
|
|
122
|
+
stopPromise;
|
|
123
|
+
exited = false;
|
|
124
|
+
closed;
|
|
125
|
+
counters;
|
|
126
|
+
constructor(executable, env) {
|
|
127
|
+
this.executable = executable;
|
|
128
|
+
this.env = env;
|
|
129
|
+
this.child = spawn(executable, ['--media-fd', '3'], { stdio: ['pipe', 'pipe', 'ignore', 'pipe', 'pipe'], env: { ...env, AXOM_KEEPER_AUDIO_FD: '4' }, detached: false });
|
|
130
|
+
this.child.stdin.on('error', () => { });
|
|
131
|
+
this.reader = new NativeControlReader(this.child.stdout);
|
|
132
|
+
this.media = new NativeByteReader(this.child.stdio[3]);
|
|
133
|
+
this.audio = new NativeByteReader(this.child.stdio[4]);
|
|
134
|
+
this.closed = new Promise(resolve => {
|
|
135
|
+
this.child.once('exit', () => { this.exited = true; this.rejectPending(new NativeCommandError('transport_failed')); resolve(); });
|
|
136
|
+
this.child.once('error', () => { this.exited = true; this.rejectPending(new NativeCommandError('transport_failed')); resolve(); });
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
static async launch(executable, env = process.env, platform = 'linux') {
|
|
140
|
+
const h = new HelperProcess(executable, env);
|
|
141
|
+
try {
|
|
142
|
+
const handshake = await bounded(h.reader.read(), 3000, 'unavailable');
|
|
143
|
+
validateNativeHandshake(handshake, platform);
|
|
144
|
+
h.audioSupported = handshake.supportedCommands.includes('audio_start');
|
|
145
|
+
void h.readReplies();
|
|
146
|
+
return h;
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
await h.terminate();
|
|
150
|
+
throw new NativeCommandError('unavailable');
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
get pid() { return this.child.pid; }
|
|
154
|
+
async readReplies() {
|
|
155
|
+
try {
|
|
156
|
+
while (!this.stopped && !this.exited) {
|
|
157
|
+
const reply = replySchema.parse(await this.reader.read());
|
|
158
|
+
const first = this.pending.entries().next();
|
|
159
|
+
mediaRequire(!first.done && reply.id === first.value[0]);
|
|
160
|
+
const [id, pending] = first.value;
|
|
161
|
+
this.pending.delete(id);
|
|
162
|
+
clearTimeout(pending.timer);
|
|
163
|
+
this.counters = reply.counters;
|
|
164
|
+
if (reply.ok)
|
|
165
|
+
pending.resolve({ result: reply.result, counters: reply.counters });
|
|
166
|
+
else
|
|
167
|
+
pending.reject(new NativeCommandError(reply.code, reply.counters));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
if (!this.stopped)
|
|
172
|
+
this.fail(error);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
rejectPending(error) {
|
|
176
|
+
for (const pending of this.pending.values()) {
|
|
177
|
+
clearTimeout(pending.timer);
|
|
178
|
+
pending.reject(error);
|
|
179
|
+
}
|
|
180
|
+
this.pending.clear();
|
|
181
|
+
}
|
|
182
|
+
fail(error) { this.rejectPending(error); void this.terminate(); }
|
|
183
|
+
request(command, binding, payload = {}, current = () => true) {
|
|
184
|
+
if (this.stopped || this.exited || ![...commands, 'cursor_visibility', 'audio_start', 'audio_rate', 'audio_stop', 'audio_status'].includes(command) || this.queued >= 32)
|
|
185
|
+
return Promise.reject(new NativeCommandError('transport_failed'));
|
|
186
|
+
const barrier = !['credit', 'status', 'input', 'transport_cost', 'cache_dependency'].includes(command);
|
|
187
|
+
const prior = barrier ? [...this.active] : [];
|
|
188
|
+
const identity = { ...binding }, epoch = this.counters?.epoch;
|
|
189
|
+
let data;
|
|
190
|
+
try {
|
|
191
|
+
data = structuredClone(payload);
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
this.fail(error);
|
|
195
|
+
return Promise.reject(error);
|
|
196
|
+
}
|
|
197
|
+
this.queued++;
|
|
198
|
+
const queuedAt = performance.now();
|
|
199
|
+
let startedAt = queuedAt;
|
|
200
|
+
let response;
|
|
201
|
+
const issued = this.tail.then(async () => {
|
|
202
|
+
// A barrier drains prior validated replies and their owner continuations,
|
|
203
|
+
// then excludes every later write until its own reply has completed.
|
|
204
|
+
if (barrier)
|
|
205
|
+
await Promise.allSettled(prior);
|
|
206
|
+
startedAt = performance.now();
|
|
207
|
+
if (this.stopped || this.exited || !Number.isSafeInteger(this.id + 1))
|
|
208
|
+
throw new NativeCommandError('transport_failed');
|
|
209
|
+
if (!current() || command === 'input' && epoch !== undefined && this.counters?.epoch !== epoch)
|
|
210
|
+
throw new NativeCommandCancelled();
|
|
211
|
+
// The owner may withdraw capture authority while this rate command waits.
|
|
212
|
+
// All other payload fields retain their enqueue-time snapshot.
|
|
213
|
+
if (command === 'set_rate')
|
|
214
|
+
Object.assign(data, { pauseCapture: payload.pauseCapture });
|
|
215
|
+
const id = ++this.id, bytes = Buffer.from(JSON.stringify({ ipc: 1, id, command, binding: identity, payload: data }));
|
|
216
|
+
mediaRequire(bytes.length > 0 && bytes.length <= 49152);
|
|
217
|
+
response = new Promise((resolve, reject) => {
|
|
218
|
+
const timer = setTimeout(() => this.fail(new NativeCommandError('transport_failed')), 5000);
|
|
219
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
220
|
+
});
|
|
221
|
+
// A reply can precede the write callback; retain its failure without an
|
|
222
|
+
// unhandled rejection while write ownership is still being confirmed.
|
|
223
|
+
void response.catch(() => { });
|
|
224
|
+
const prefix = Buffer.alloc(4);
|
|
225
|
+
prefix.writeUInt32BE(bytes.length);
|
|
226
|
+
await bounded(write(this.child.stdin, Buffer.concat([prefix, bytes])), 5000);
|
|
227
|
+
});
|
|
228
|
+
const run = issued.then(() => response);
|
|
229
|
+
const report = (error) => {
|
|
230
|
+
if (error instanceof NativeCommandCancelled)
|
|
231
|
+
return;
|
|
232
|
+
if (!(error instanceof NativeCommandError) || !['busy', 'rate_limited'].includes(error.code)) {
|
|
233
|
+
console.error(JSON.stringify({ event: 'desktop_native_command_failed', command,
|
|
234
|
+
code: error instanceof NativeCommandError ? error.code : 'transport_failed', elapsedMs: Math.round(performance.now() - startedAt), queuedMs: Math.round(startedAt - queuedAt), queued: this.queued }));
|
|
235
|
+
}
|
|
236
|
+
if (!(error instanceof NativeCommandError) || !error.counters)
|
|
237
|
+
this.fail(error);
|
|
238
|
+
};
|
|
239
|
+
const completed = run.catch(error => { report(error); throw error; }).finally(() => {
|
|
240
|
+
this.queued--;
|
|
241
|
+
this.active.delete(completed);
|
|
242
|
+
});
|
|
243
|
+
this.active.add(completed);
|
|
244
|
+
// Observe issuance errors before allowing the next queued write. Reply
|
|
245
|
+
// failures are reported by completed; the reader independently fails all
|
|
246
|
+
// in-flight work on malformed or unknown replies.
|
|
247
|
+
this.tail = (barrier ? completed : issued).catch(error => {
|
|
248
|
+
if (!(error instanceof NativeCommandCancelled) && (!(error instanceof NativeCommandError) || !error.counters))
|
|
249
|
+
this.fail(error);
|
|
250
|
+
});
|
|
251
|
+
return completed;
|
|
252
|
+
}
|
|
253
|
+
get packets() {
|
|
254
|
+
const self = this;
|
|
255
|
+
return { async *[Symbol.asyncIterator]() {
|
|
256
|
+
while (!self.stopped && !self.exited) {
|
|
257
|
+
const header = await self.media.exact(64), size = header.readUInt32BE(48);
|
|
258
|
+
mediaRequire(size > 0 && size <= 65536);
|
|
259
|
+
const bytes = Buffer.concat([header, await self.media.exact(size)]);
|
|
260
|
+
decodeMedia(bytes);
|
|
261
|
+
yield bytes;
|
|
262
|
+
}
|
|
263
|
+
} };
|
|
264
|
+
}
|
|
265
|
+
get audioPackets() {
|
|
266
|
+
const self = this;
|
|
267
|
+
return { async *[Symbol.asyncIterator]() {
|
|
268
|
+
while (!self.stopped && !self.exited) {
|
|
269
|
+
const header = await self.audio.exact(40), size = header.readUInt16BE(36);
|
|
270
|
+
mediaRequire(size > 0 && size <= 1000);
|
|
271
|
+
const bytes = Buffer.concat([header, await self.audio.exact(size)]);
|
|
272
|
+
decodeAudio(bytes);
|
|
273
|
+
yield bytes;
|
|
274
|
+
}
|
|
275
|
+
} };
|
|
276
|
+
}
|
|
277
|
+
stop(binding) {
|
|
278
|
+
return this.stopPromise ??= (async () => {
|
|
279
|
+
if (!this.exited) {
|
|
280
|
+
try {
|
|
281
|
+
await bounded(this.request('stop', binding), 750);
|
|
282
|
+
}
|
|
283
|
+
catch { /* EOF cleanup remains authoritative. */ }
|
|
284
|
+
}
|
|
285
|
+
await this.terminate();
|
|
286
|
+
await HelperProcess.verifyCleanup(this.executable, this.env);
|
|
287
|
+
})();
|
|
288
|
+
}
|
|
289
|
+
terminate() {
|
|
290
|
+
return this.termination ??= (async () => {
|
|
291
|
+
this.stopped = true;
|
|
292
|
+
this.rejectPending(new NativeCommandError('transport_failed'));
|
|
293
|
+
if (!this.exited) {
|
|
294
|
+
this.child.kill('SIGTERM');
|
|
295
|
+
try {
|
|
296
|
+
await bounded(this.closed, 1000);
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
this.child.kill('SIGKILL');
|
|
300
|
+
await bounded(this.closed, 2000);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
this.child.stdin?.destroy();
|
|
304
|
+
this.child.stdout?.destroy();
|
|
305
|
+
this.child.stdio[3]?.destroy();
|
|
306
|
+
this.child.stdio[4]?.destroy();
|
|
307
|
+
})();
|
|
308
|
+
}
|
|
309
|
+
static async verifyCleanup(executable, env = process.env) {
|
|
310
|
+
const deadline = performance.now() + 3000;
|
|
311
|
+
do {
|
|
312
|
+
try {
|
|
313
|
+
await execute(executable, ['--verify-input-cleanup'], { env, timeout: 750, maxBuffer: 4096 });
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
await delay(25);
|
|
318
|
+
}
|
|
319
|
+
} while (performance.now() < deadline);
|
|
320
|
+
throw new NativeCommandError('unavailable');
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
function write(stream, bytes) {
|
|
324
|
+
return new Promise((resolve, reject) => stream.write(bytes, error => error ? reject(error) : resolve()));
|
|
325
|
+
}
|
|
326
|
+
/** A single bounded session/event consumer; overflow retires the session. */
|
|
327
|
+
export class DesktopQueue {
|
|
328
|
+
limit;
|
|
329
|
+
values = [];
|
|
330
|
+
wake;
|
|
331
|
+
ended = false;
|
|
332
|
+
constructor(limit = 64) {
|
|
333
|
+
this.limit = limit;
|
|
334
|
+
}
|
|
335
|
+
push(value) {
|
|
336
|
+
if (this.ended)
|
|
337
|
+
return;
|
|
338
|
+
if (this.wake) {
|
|
339
|
+
const wake = this.wake;
|
|
340
|
+
this.wake = undefined;
|
|
341
|
+
wake({ value, done: false });
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
mediaRequire(this.values.length < this.limit);
|
|
345
|
+
this.values.push(value);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
close() { this.ended = true; this.values = []; this.wake?.({ value: undefined, done: true }); this.wake = undefined; }
|
|
349
|
+
[Symbol.asyncIterator]() {
|
|
350
|
+
return { next: () => this.values.length ? Promise.resolve({ value: this.values.shift(), done: false }) :
|
|
351
|
+
this.ended ? Promise.resolve({ value: undefined, done: true }) : new Promise(resolve => { mediaRequire(!this.wake); this.wake = resolve; }) };
|
|
352
|
+
}
|
|
353
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { decodeDesktopSessionRequest, parseDesktopJson } from './protocol.js';
|
|
3
|
+
const identifier = z.string().regex(/^[a-f0-9]{32}$/);
|
|
4
|
+
const safe = z.number().int().min(1).max(Number.MAX_SAFE_INTEGER);
|
|
5
|
+
const sourceAxis = z.number().int().min(2).max(4096);
|
|
6
|
+
const axis = sourceAxis.refine(value => value % 2 === 0);
|
|
7
|
+
const sourceModeSchema = z.strictObject({ width: sourceAxis, height: sourceAxis });
|
|
8
|
+
const modeSchema = z.strictObject({ width: axis, height: axis });
|
|
9
|
+
const viewportSchema = z.strictObject({ widthDp: z.number().finite().positive().max(16384),
|
|
10
|
+
heightDp: z.number().finite().positive().max(16384), deviceScale: z.number().finite().min(0.5).max(8),
|
|
11
|
+
orientation: z.enum(['landscape', 'portrait']) });
|
|
12
|
+
const resolutionSchema = z.discriminatedUnion('mode', [z.strictObject({ mode: z.literal('fit') }),
|
|
13
|
+
z.strictObject({ mode: z.literal('fixed'), width: axis, height: axis })]);
|
|
14
|
+
export const lobbyRequestSchema = z.strictObject({ requestId: identifier, revision: safe, viewport: viewportSchema,
|
|
15
|
+
resolution: resolutionSchema, dpi: z.number().int().min(64).max(384).nullable(),
|
|
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() });
|
|
18
|
+
const rangeSchema = z.strictObject({ minWidth: axis, minHeight: axis, maxWidth: axis, maxHeight: axis });
|
|
19
|
+
export const lobbyPlanSchema = z.strictObject({ lobbyId: identifier, planId: identifier, revision: safe, targetId: identifier,
|
|
20
|
+
desktop: sourceModeSchema, dpi: z.number().int().min(64).max(384).nullable(), resize: z.enum(['none', 'modes', 'dynamic']),
|
|
21
|
+
modes: z.array(modeSchema).max(32), resizeRange: rangeSchema.nullable(), dynamicDpi: z.boolean(), canAcquire: z.boolean(),
|
|
22
|
+
reason: z.string().max(128).nullable(), maxDpi: z.number().int().min(64).max(384).nullable(), captureViewport: z.boolean(),
|
|
23
|
+
sampling: modeSchema, targets: z.array(z.strictObject({ id: identifier, label: z.string().min(1).max(128),
|
|
24
|
+
kind: z.enum(['display', 'window']), available: z.boolean() })).max(128) });
|
|
25
|
+
export function decodeLobbyRequest(text) {
|
|
26
|
+
if (Buffer.byteLength(text) > 49152)
|
|
27
|
+
throw new Error('invalid_request');
|
|
28
|
+
const request = lobbyRequestSchema.parse(parseDesktopJson(text));
|
|
29
|
+
if (request.viewer != null)
|
|
30
|
+
decodeDesktopSessionRequest(JSON.stringify({ requestId: request.requestId,
|
|
31
|
+
displayId: request.targetId ?? request.requestId, viewer: request.viewer, viewport: request.viewport,
|
|
32
|
+
resolution: request.resolution, quality: 'auto', takeover: false }));
|
|
33
|
+
return request;
|
|
34
|
+
}
|
|
35
|
+
export const wallpaperAssetSchema = z.strictObject({ assetId: z.string().min(1).max(128), version: z.string().min(1).max(128),
|
|
36
|
+
originalWidth: z.number().int().min(1).max(65536), originalHeight: z.number().int().min(1).max(65536),
|
|
37
|
+
width: z.number().int().min(1).max(4096), height: z.number().int().min(1).max(4096),
|
|
38
|
+
placement: z.enum(['cover', 'fit', 'stretch', 'center', 'tile']).default('cover'),
|
|
39
|
+
coverage: z.enum(['full_asset', 'composited']).default('full_asset'), compatibleDesktop: sourceModeSchema.nullable().optional(),
|
|
40
|
+
anchorX: z.number().finite().min(0).max(1).default(0.5), anchorY: z.number().finite().min(0).max(1).default(0.5),
|
|
41
|
+
background: z.string().regex(/^#[a-fA-F0-9]{6}$/).default('#000000'),
|
|
42
|
+
intrinsicScale: z.number().finite().min(0.125).max(16).default(1),
|
|
43
|
+
fidelity: z.enum(['exact', 'approximate']).default('exact'), colorSpace: z.literal('srgb').default('srgb')
|
|
44
|
+
}).refine(asset => asset.width * asset.height <= 4_000_000 && (asset.coverage !== 'composited' || asset.compatibleDesktop != null));
|
|
45
|
+
export function composeWallpaper(value, selected) {
|
|
46
|
+
const asset = wallpaperAssetSchema.parse(value);
|
|
47
|
+
const desktop = sourceModeSchema.parse(selected);
|
|
48
|
+
if (asset.coverage === 'composited' && (asset.compatibleDesktop?.width !== desktop.width || asset.compatibleDesktop.height !== desktop.height))
|
|
49
|
+
throw new Error('incompatible_wallpaper');
|
|
50
|
+
const full = { x: 0, y: 0, width: 1, height: 1 };
|
|
51
|
+
if (asset.coverage === 'composited' || asset.placement === 'stretch')
|
|
52
|
+
return { source: full, destination: full, version: 1 };
|
|
53
|
+
const imageWidth = asset.originalWidth * asset.intrinsicScale;
|
|
54
|
+
const imageHeight = asset.originalHeight * asset.intrinsicScale;
|
|
55
|
+
if (asset.placement === 'tile')
|
|
56
|
+
return { source: full, destination: full,
|
|
57
|
+
tileWidth: imageWidth / desktop.width, tileHeight: imageHeight / desktop.height, version: 1 };
|
|
58
|
+
const scale = asset.placement === 'cover' ? Math.max(desktop.width / imageWidth, desktop.height / imageHeight) :
|
|
59
|
+
asset.placement === 'fit' ? Math.min(desktop.width / imageWidth, desktop.height / imageHeight) : 1;
|
|
60
|
+
const sourceWidth = Math.min(1, desktop.width / (imageWidth * scale));
|
|
61
|
+
const sourceHeight = Math.min(1, desktop.height / (imageHeight * scale));
|
|
62
|
+
const destinationWidth = Math.min(1, imageWidth * scale / desktop.width);
|
|
63
|
+
const destinationHeight = Math.min(1, imageHeight * scale / desktop.height);
|
|
64
|
+
return { source: { x: (1 - sourceWidth) * asset.anchorX, y: (1 - sourceHeight) * asset.anchorY,
|
|
65
|
+
width: sourceWidth, height: sourceHeight },
|
|
66
|
+
destination: { x: (1 - destinationWidth) * asset.anchorX, y: (1 - destinationHeight) * asset.anchorY,
|
|
67
|
+
width: destinationWidth, height: destinationHeight }, version: 1 };
|
|
68
|
+
}
|
|
69
|
+
export function planLobby(request, capabilities, lobbyId, planId, automaticDpi = true) {
|
|
70
|
+
const target = capabilities.displays.find(display => display.id === request.targetId) ??
|
|
71
|
+
(request.targetId == null ? capabilities.displays.find(display => display.kind !== 'window') : undefined);
|
|
72
|
+
if (!target)
|
|
73
|
+
throw new Error('unavailable');
|
|
74
|
+
const viewer = request.viewer == null ? null : decodeDesktopSessionRequest(JSON.stringify({ requestId: request.requestId,
|
|
75
|
+
displayId: target.id, viewer: request.viewer, viewport: request.viewport, resolution: request.resolution,
|
|
76
|
+
quality: 'auto', takeover: false })).viewer;
|
|
77
|
+
const limits = capabilities.encoderBackends.flatMap(backend => backend.formats).filter(format => format.cappedRate &&
|
|
78
|
+
capabilities.formats.includes(format.format) && (!viewer || viewer.formats.includes(format.format)));
|
|
79
|
+
const limit = limits.sort((first, second) => second.maxPixels - first.maxPixels)[0];
|
|
80
|
+
if (!limit || viewer && (viewer.protocols.min > 2 || viewer.protocols.max < 2))
|
|
81
|
+
throw new Error('unsupported_client');
|
|
82
|
+
const maxWidth = Math.min(viewer?.maxWidth ?? 4096, limit.maxWidth);
|
|
83
|
+
const maxHeight = Math.min(viewer?.maxHeight ?? 4096, limit.maxHeight);
|
|
84
|
+
const maxPixels = Math.min(viewer?.maxPixels ?? 4_000_000, limit.maxPixels);
|
|
85
|
+
const layoutScale = !automaticDpi && !target.captureViewport ? 1 : request.viewport.deviceScale;
|
|
86
|
+
const desired = request.resolution.mode === 'fixed' ? request.resolution : {
|
|
87
|
+
width: Math.max(2, Math.floor(request.viewport.widthDp * layoutScale / 2) * 2),
|
|
88
|
+
height: Math.max(2, Math.floor(request.viewport.heightDp * layoutScale / 2) * 2)
|
|
89
|
+
};
|
|
90
|
+
let desktop;
|
|
91
|
+
if (target.captureViewport || target.resize === 'none')
|
|
92
|
+
desktop = { width: target.width, height: target.height };
|
|
93
|
+
else if (target.resize === 'dynamic' && target.resizeRange) {
|
|
94
|
+
const range = target.resizeRange;
|
|
95
|
+
const factor = Math.min(1, Math.min(maxWidth, range.maxWidth) / desired.width, Math.min(maxHeight, range.maxHeight) / desired.height, Math.sqrt(Math.min(maxPixels, 4_000_000) / (desired.width * desired.height)));
|
|
96
|
+
desktop = { width: Math.max(range.minWidth, Math.floor(desired.width * factor / 2) * 2),
|
|
97
|
+
height: Math.max(range.minHeight, Math.floor(desired.height * factor / 2) * 2) };
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
desktop = [...target.modes].filter(candidate => candidate.width <= maxWidth && candidate.height <= maxHeight &&
|
|
101
|
+
candidate.width * candidate.height <= maxPixels).sort((first, second) => {
|
|
102
|
+
const score = (candidate) => Math.abs(Math.log((candidate.width / candidate.height) / (desired.width / desired.height))) * 10 +
|
|
103
|
+
Math.abs(Math.log(candidate.width * candidate.height / (desired.width * desired.height)));
|
|
104
|
+
return score(first) - score(second);
|
|
105
|
+
})[0] ?? { width: target.width, height: target.height };
|
|
106
|
+
}
|
|
107
|
+
const maximumDpi = target.maxDpi == null ? undefined : Math.min(target.maxDpi, viewer?.maxDpi ?? target.maxDpi);
|
|
108
|
+
const baselineDpi = automaticDpi && request.resolution.mode === 'fit' ? Math.max(96, Math.round(96 * Math.min(desktop.width / request.viewport.widthDp, desktop.height / request.viewport.heightDp))) : 96;
|
|
109
|
+
const dpi = maximumDpi == null ? null : Math.min(maximumDpi, request.dpi ?? baselineDpi);
|
|
110
|
+
let sampling = desktop;
|
|
111
|
+
if (target.captureViewport) {
|
|
112
|
+
const density = Math.min(target.capturePixelScale ?? 1, request.resolution.mode === 'fixed' ?
|
|
113
|
+
Math.min(desired.width / desktop.width, desired.height / desktop.height) :
|
|
114
|
+
Math.min(request.viewport.widthDp / desktop.width, request.viewport.heightDp / desktop.height) * request.viewport.deviceScale);
|
|
115
|
+
const width = desktop.width * density, height = desktop.height * density;
|
|
116
|
+
const factor = Math.min(1, maxWidth / width, maxHeight / height, Math.sqrt(maxPixels / (width * height)));
|
|
117
|
+
sampling = { width: Math.max(2, Math.floor(width * factor / 2) * 2), height: Math.max(2, Math.floor(height * factor / 2) * 2) };
|
|
118
|
+
}
|
|
119
|
+
const supported = sampling.width <= maxWidth && sampling.height <= maxHeight && sampling.width * sampling.height <= maxPixels;
|
|
120
|
+
const modes = target.captureViewport ? [sampling] : target.resize === 'none' ? [desktop] : [...target.modes.filter(candidate => candidate.width <= maxWidth && candidate.height <= maxHeight && candidate.width * candidate.height <= maxPixels &&
|
|
121
|
+
(candidate.width !== desktop.width || candidate.height !== desktop.height)).slice(0, 31), desktop];
|
|
122
|
+
return { lobbyId, planId, revision: request.revision, targetId: target.id, desktop, dpi,
|
|
123
|
+
resize: target.captureViewport ? 'none' : target.resize, modes,
|
|
124
|
+
resizeRange: target.captureViewport ? null : target.resizeRange ?? null, dynamicDpi: maximumDpi != null,
|
|
125
|
+
maxDpi: maximumDpi ?? null, captureViewport: target.captureViewport ?? false, sampling,
|
|
126
|
+
canAcquire: supported && capabilities.configured && capabilities.health === 'ready' && ['granted', 'not_applicable'].includes(capabilities.permission),
|
|
127
|
+
reason: supported ? capabilities.reason : 'unsupported_client', targets: capabilities.displays.map(display => ({ id: display.id, label: display.label,
|
|
128
|
+
kind: display.kind ?? 'display', available: true })) };
|
|
129
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { promises as fs, constants } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { execFile } from 'node:child_process';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
import { createConnection } from 'node:net';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { NativeProvider } from './nativeProvider.js';
|
|
9
|
+
import { HelperProcess, bounded, NativeCommandError } from './helperProcess.js';
|
|
10
|
+
import { desktopCapabilitiesSchema } from './protocol.js';
|
|
11
|
+
import { normalizeWorkstationServer } from '../control/serverIdentity.js';
|
|
12
|
+
import { captureTargetLabel } from './captureTargetLabel.js';
|
|
13
|
+
import { decodeWallpaper } from './wallpaper.js';
|
|
14
|
+
import { captureSamplingModes as macCaptureModes } from './captureModes.js';
|
|
15
|
+
const execute = promisify(execFile);
|
|
16
|
+
export const macDesktopGrantSchema = z.strictObject({ schemaVersion: z.literal(1),
|
|
17
|
+
binding: z.strictObject({ server: z.string(), workstationId: z.string(), ownerUserId: z.string() }),
|
|
18
|
+
executable: z.string().startsWith('/').max(4096), sha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
19
|
+
socket: z.string().startsWith('/').max(103), token: z.string().regex(/^[a-f0-9]{64}$/) });
|
|
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(),
|
|
21
|
+
width: z.number().int().min(2).max(4096).refine(n => n % 2 === 0), height: z.number().int().min(2).max(4096).refine(n => n % 2 === 0) }).refine(t => t.width * t.height <= 16777216);
|
|
22
|
+
const geometry = z.object({ targets: z.array(target).min(1).max(128), screenRecording: z.boolean(), accessibility: z.boolean(), independentCursor: z.boolean(), captureViewport: z.boolean().optional(), overview: z.boolean().optional(), cursorVisibility: z.boolean().optional(), audio: z.boolean().optional() });
|
|
23
|
+
export { captureTargetLabel } from './captureTargetLabel.js';
|
|
24
|
+
export { captureSamplingModes as macCaptureModes } from './captureModes.js';
|
|
25
|
+
export async function checkMacHost(grant) {
|
|
26
|
+
const socket = createConnection(grant.socket);
|
|
27
|
+
try {
|
|
28
|
+
await bounded(new Promise((resolve, reject) => {
|
|
29
|
+
socket.once('error', reject);
|
|
30
|
+
socket.once('connect', () => socket.write(grant.token));
|
|
31
|
+
socket.once('data', bytes => bytes.length === 1 && bytes[0] === 1 ? resolve() : reject(new NativeCommandError('unavailable')));
|
|
32
|
+
socket.once('end', () => reject(new NativeCommandError('unavailable')));
|
|
33
|
+
}), 1000, 'unavailable');
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
socket.destroy();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** Discovery can report permissions but cannot prompt, capture, encode or inject input. */
|
|
40
|
+
export class MacProvider {
|
|
41
|
+
directory;
|
|
42
|
+
binding;
|
|
43
|
+
provider;
|
|
44
|
+
identity = '';
|
|
45
|
+
revisions = new Map();
|
|
46
|
+
constructor(directory, binding) {
|
|
47
|
+
this.directory = directory;
|
|
48
|
+
this.binding = binding;
|
|
49
|
+
}
|
|
50
|
+
async grant() {
|
|
51
|
+
const file = await fs.open(path.join(this.directory, 'desktop-provider.json'), constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
52
|
+
let grant;
|
|
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)
|
|
63
|
+
throw new NativeCommandError('unavailable');
|
|
64
|
+
if (!grant.executable.endsWith('/Axom Work Computer Use.app/Contents/MacOS/Axom Work Computer Use'))
|
|
65
|
+
throw new NativeCommandError('unavailable');
|
|
66
|
+
const executable = await fs.open(grant.executable, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
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 });
|
|
81
|
+
await checkMacHost(grant);
|
|
82
|
+
return grant;
|
|
83
|
+
}
|
|
84
|
+
async capabilities() {
|
|
85
|
+
const grant = await this.grant();
|
|
86
|
+
const { stdout } = await execute(grant.executable, ['--probe'], { timeout: 7000, maxBuffer: 65536, env: { ...process.env, AXOM_KEEPER_SOCKET: grant.socket, AXOM_KEEPER_TOKEN: grant.token } });
|
|
87
|
+
const g = geometry.parse(JSON.parse(stdout));
|
|
88
|
+
const displays = g.targets.map(t => {
|
|
89
|
+
const previous = this.revisions.get(t.id);
|
|
90
|
+
const identity = t.displayIdentity;
|
|
91
|
+
const revision = previous ? (previous.identity === identity ? previous.revision : previous.revision + 1) : 1;
|
|
92
|
+
this.revisions.set(t.id, { identity, revision });
|
|
93
|
+
return { ...(g.overview ? { overview: true } : {}), id: t.id, kind: t.kind, label: captureTargetLabel(t.label), width: t.width, height: t.height, revision, resize: g.captureViewport ? 'dynamic' : 'none', modes: g.captureViewport ? macCaptureModes(t.width, t.height, t.capturePixelScale ?? 1) : [], ...(g.captureViewport ? { captureViewport: true, capturePixelScale: t.capturePixelScale ?? 1, resizeRange: { minWidth: 2, minHeight: 2, maxWidth: Math.min(4096, Math.floor(t.width * (t.capturePixelScale ?? 1) / 2) * 2), maxHeight: Math.min(4096, Math.floor(t.height * (t.capturePixelScale ?? 1) / 2) * 2) } } : {}) };
|
|
94
|
+
});
|
|
95
|
+
const liveIds = new Set(displays.map(d => d.id));
|
|
96
|
+
for (const id of this.revisions.keys())
|
|
97
|
+
if (!liveIds.has(id))
|
|
98
|
+
this.revisions.delete(id);
|
|
99
|
+
const formats = ['h264-420-v1'];
|
|
100
|
+
const permission = g.screenRecording && g.accessibility, ready = permission && g.independentCursor;
|
|
101
|
+
return desktopCapabilitiesSchema.parse({ protocols: { min: 2, max: g.audio ? 3 : 2 }, providerId: 'macos-arm64-v1', providerVersion: 'axom-macos-1', configured: true, cursorEmbedded: false, cursorVisibility: g.cursorVisibility === true,
|
|
102
|
+
permission: permission ? 'granted' : 'required', health: ready ? 'ready' : 'unavailable', reason: ready ? null : permission ? 'cursor_unavailable' : 'permission_required', formats,
|
|
103
|
+
encoderBackends: [{ id: 'videotoolbox', kind: 'hardware', formats: formats.map(format => ({ format, maxWidth: 4096, maxHeight: 4096, maxPixels: 8847360, maxFps: 30, cappedRate: true })) }],
|
|
104
|
+
input: { pointer: true, physicalKeyboard: true, unicodeText: true, clipboard: true, textPaste: true },
|
|
105
|
+
displays });
|
|
106
|
+
}
|
|
107
|
+
async liveProvider() {
|
|
108
|
+
try {
|
|
109
|
+
const grant = await this.grant(), identity = grant.sha256 + grant.token;
|
|
110
|
+
if (identity !== this.identity) {
|
|
111
|
+
this.identity = identity;
|
|
112
|
+
this.provider = new NativeProvider({ helperPath: grant.executable, runtimeDir: this.directory, backend: 'videotoolbox',
|
|
113
|
+
enumerate: () => this.capabilities(), sessionHelperFactory: async (binding) => { const current = await this.grant(); return HelperProcess.launch(current.executable, { ...process.env, AXOM_KEEPER_TARGET: binding.displayId, AXOM_KEEPER_SOCKET: current.socket, AXOM_KEEPER_TOKEN: current.token }, 'macos'); } });
|
|
114
|
+
}
|
|
115
|
+
return this.provider;
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async probe() {
|
|
122
|
+
// Only explicit removal of the desktop grant disables discovery. A stopped or
|
|
123
|
+
// unresponsive keeper is a connection failure, not a disappearing capability.
|
|
124
|
+
try {
|
|
125
|
+
await fs.lstat(path.join(this.directory, 'desktop-provider.json'));
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
if (error.code === 'ENOENT')
|
|
129
|
+
return null;
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
return this.capabilities();
|
|
133
|
+
}
|
|
134
|
+
async prepare(selection, binding) {
|
|
135
|
+
const provider = await this.liveProvider();
|
|
136
|
+
if (!provider)
|
|
137
|
+
throw new NativeCommandError('unavailable');
|
|
138
|
+
// NativeProvider.prepare performs fresh permission/geometry admission itself.
|
|
139
|
+
// Repeating full enumeration here added another WindowServer round trip per window.
|
|
140
|
+
return provider.prepare(selection, binding);
|
|
141
|
+
}
|
|
142
|
+
async wallpaper(targetId) {
|
|
143
|
+
const grant = await this.grant();
|
|
144
|
+
if (!/^[a-f0-9]{32}$/.test(targetId))
|
|
145
|
+
throw new NativeCommandError('invalid_request');
|
|
146
|
+
const result = await execute(grant.executable, ['--wallpaper', targetId], { timeout: 8000, maxBuffer: 100000,
|
|
147
|
+
env: { ...process.env, AXOM_KEEPER_SOCKET: grant.socket, AXOM_KEEPER_TOKEN: grant.token } });
|
|
148
|
+
return decodeWallpaper(result.stdout);
|
|
149
|
+
}
|
|
150
|
+
async recoverDisplay(binding, reserved) {
|
|
151
|
+
if (!await this.probe() || !this.provider)
|
|
152
|
+
throw new NativeCommandError('unavailable');
|
|
153
|
+
return this.provider.recoverDisplay(binding, reserved);
|
|
154
|
+
}
|
|
155
|
+
}
|