@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.
Files changed (159) hide show
  1. package/README.md +7 -1
  2. package/dist/axom-patch/cli/README.md +31 -2
  3. package/dist/axom-patch/cli/account-limits/generate.py +2 -2
  4. package/dist/axom-patch/cli/claude-replay-lifecycle.json +16 -0
  5. package/dist/axom-patch/cli/codex-terminal-output.json +2 -2
  6. package/dist/axom-patch/cli/probes/claude-replay-lifecycle.mjs +31 -0
  7. package/dist/axom-patch/cli/probes/codex-terminal-output.mjs +1 -1
  8. package/dist/axom-patch/cli/probes/zcode-env-account.mjs +81 -0
  9. package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +7 -0
  10. package/dist/axom-patch/cli/qoder/dist/axom-acp-compat.mjs +137 -41
  11. package/dist/axom-patch/cli/qoder/releases/1.1.75.json +5 -88
  12. package/dist/axom-patch/cli/qoder-native-acp.json +23 -23
  13. package/dist/axom-patch/cli/runtime.json +3 -1
  14. package/dist/axom-patch/cli/zcode/env-account.mjs +13 -0
  15. package/dist/axom-patch/cli/zcode/generate-env-account.py +52 -0
  16. package/dist/axom-patch/cli/zcode-account-limits.json +7 -7
  17. package/dist/axom-patch/cli/zcode-env-account.json +58 -0
  18. package/dist/axom-patch/cli/zcode-models.json +9 -9
  19. package/dist/axom-patch/cli/zcode-subagent-subwork.json +10 -10
  20. package/dist/cli.js +9 -11
  21. package/dist/commands/auth.js +4 -1
  22. package/dist/commands/update.js +68 -7
  23. package/dist/commands/workstation.js +94 -223
  24. package/dist/commands/workstationRegistrations.js +116 -0
  25. package/dist/credentialBroker.js +13 -2
  26. package/dist/credentials.js +16 -0
  27. package/dist/install/claude/commands.js +2 -2
  28. package/dist/keepers/terminal/keeper.mjs +1089 -673
  29. package/dist/keepers/terminal/manifest.json +4 -4
  30. package/dist/native/darwin-x64/axom-quic +0 -0
  31. package/dist/workstation/acp/acpAgents.js +11 -4
  32. package/dist/workstation/acp/outbox.js +12 -5
  33. package/dist/workstation/acp/permission.js +2 -2
  34. package/dist/workstation/acp/processor/checkpoint.js +4 -3
  35. package/dist/workstation/acp/processor/control.js +2 -2
  36. package/dist/workstation/acp/processor/jsonrpc.js +8 -0
  37. package/dist/workstation/acp/processor/session.js +3 -2
  38. package/dist/workstation/acp/processor/state.js +1 -0
  39. package/dist/workstation/acp/processor/transport/local.js +19 -3
  40. package/dist/workstation/acp/processor/updates.js +25 -0
  41. package/dist/workstation/acp/processorSession.js +12 -1
  42. package/dist/workstation/acp/registry/catalog.js +6 -6
  43. package/dist/workstation/acp/test/keeperWorld.js +4 -3
  44. package/dist/workstation/agentSessions.js +76 -23
  45. package/dist/workstation/channels/agent.js +9 -1
  46. package/dist/workstation/channels/computer.js +27 -4
  47. package/dist/workstation/channels/management.js +36 -5
  48. package/dist/workstation/config.js +18 -6
  49. package/dist/workstation/control/bindingStore.js +4 -0
  50. package/dist/workstation/control/commands.js +1 -0
  51. package/dist/workstation/control/daemonIdentity.js +90 -0
  52. package/dist/workstation/control/deviceCommands.js +191 -0
  53. package/dist/workstation/control/deviceProtocol.js +68 -0
  54. package/dist/workstation/control/locks.js +51 -4
  55. package/dist/workstation/control/rootFormat.js +16 -0
  56. package/dist/workstation/control/runtimeClient.js +60 -5
  57. package/dist/workstation/control/runtimeHost.js +96 -9
  58. package/dist/workstation/control/serverIdentity.js +2 -2
  59. package/dist/workstation/control/updateTransition.js +91 -0
  60. package/dist/workstation/daemon.js +262 -360
  61. package/dist/workstation/daemonctl.js +150 -12
  62. package/dist/workstation/desktopControl/automationDevice.js +219 -0
  63. package/dist/workstation/desktopControl/automationJson.js +74 -0
  64. package/dist/workstation/desktopControl/automationLocalState.js +23 -0
  65. package/dist/workstation/desktopControl/automationPreparation.js +1 -0
  66. package/dist/workstation/desktopControl/automationProtocol.js +253 -0
  67. package/dist/workstation/desktopControl/automationSchema.generated.js +1 -0
  68. package/dist/workstation/desktopControl/automationSession.js +81 -0
  69. package/dist/workstation/desktopControl/automationTransport.js +138 -0
  70. package/dist/workstation/desktopControl/capabilities.js +2 -1
  71. package/dist/workstation/desktopControl/deviceAdmission.js +229 -0
  72. package/dist/workstation/desktopControl/deviceOwnership.js +130 -0
  73. package/dist/workstation/desktopControl/deviceSockets.js +2 -2
  74. package/dist/workstation/desktopControl/helperProcess.js +250 -1
  75. package/dist/workstation/desktopControl/hostGrant.js +65 -0
  76. package/dist/workstation/desktopControl/lobby.js +4 -2
  77. package/dist/workstation/desktopControl/macProvider.js +41 -40
  78. package/dist/workstation/desktopControl/nativeDeviceAdmission.js +207 -0
  79. package/dist/workstation/desktopControl/nativeProvider.js +31 -5
  80. package/dist/workstation/desktopControl/ownershipProtocol.js +31 -0
  81. package/dist/workstation/desktopControl/protocol.js +15 -6
  82. package/dist/workstation/desktopControl/supervisor.js +110 -17
  83. package/dist/workstation/desktopControl/wallpaper.js +26 -6
  84. package/dist/workstation/exec/policy.js +5 -0
  85. package/dist/workstation/files/client.js +10 -1
  86. package/dist/workstation/files/host.js +9 -2
  87. package/dist/workstation/journal.js +5 -2
  88. package/dist/workstation/keepers/acp/client.js +38 -2
  89. package/dist/workstation/keepers/kit/buildVersion.js +35 -11
  90. package/dist/workstation/keepers/kit/client.js +2 -2
  91. package/dist/workstation/keepers/kit/launcher.js +80 -4
  92. package/dist/workstation/keepers/legacy.js +16 -2
  93. package/dist/workstation/keepers/policy.js +5 -34
  94. package/dist/workstation/keepers/terminal/client.js +115 -23
  95. package/dist/workstation/keepers/terminal/exec.js +6 -3
  96. package/dist/workstation/keepers/terminal/frames.js +73 -0
  97. package/dist/workstation/keepers/terminal/generations.js +5 -8
  98. package/dist/workstation/keepers/terminal/gridProtocol.js +16 -0
  99. package/dist/workstation/keepers/terminal/input.js +146 -0
  100. package/dist/workstation/keepers/terminal/keeper.js +22 -8
  101. package/dist/workstation/keepers/terminal/main.js +1 -1
  102. package/dist/workstation/keepers/terminal/metadata.js +138 -0
  103. package/dist/workstation/keepers/terminal/protocol.js +4 -28
  104. package/dist/workstation/keepers/terminal/pty.js +1 -330
  105. package/dist/workstation/keepers/terminal/screen.js +246 -0
  106. package/dist/workstation/keepers/terminal/sessions.js +526 -0
  107. package/dist/workstation/oauth/deviceAuthorization.js +31 -17
  108. package/dist/workstation/protocol/management.js +23 -4
  109. package/dist/workstation/rd/artifact.js +4 -3
  110. package/dist/workstation/rd/control.js +3 -5
  111. package/dist/workstation/rd/host.js +213 -0
  112. package/dist/workstation/rd/migrationReadiness.js +46 -0
  113. package/dist/workstation/rd/provider.js +44 -10
  114. package/dist/workstation/rd/state.js +14 -2
  115. package/dist/workstation/rd/update.js +5 -8
  116. package/dist/workstation/rd/upkeep.js +44 -2
  117. package/dist/workstation/registrations/enrollment.js +250 -0
  118. package/dist/workstation/registrations/enrollmentPending.js +361 -0
  119. package/dist/workstation/registrations/migration.js +356 -0
  120. package/dist/workstation/registrations/model.js +34 -0
  121. package/dist/workstation/registrations/removal.js +222 -0
  122. package/dist/workstation/registrations/store.js +737 -0
  123. package/dist/workstation/runtime/desktopBinding.js +20 -0
  124. package/dist/workstation/runtime/deviceDesktop.js +193 -0
  125. package/dist/workstation/runtime/deviceRuntime.js +568 -0
  126. package/dist/workstation/runtime/deviceServices.js +117 -0
  127. package/dist/workstation/runtime/events.js +20 -0
  128. package/dist/workstation/runtime/ownership.js +100 -0
  129. package/dist/workstation/runtime/registrationRuntime.js +575 -0
  130. package/dist/workstation/runtime/remoteDesktopAdapter.js +186 -0
  131. package/dist/workstation/runtime/removalActivity.js +184 -0
  132. package/dist/workstation/service/installer.js +93 -1
  133. package/dist/workstation/service/launchd.js +1 -0
  134. package/dist/workstation/service/systemd.js +1 -0
  135. package/dist/workstation/systemMetrics.js +3 -0
  136. package/dist/workstation/tools/client.js +8 -27
  137. package/dist/workstation/tools/info.js +6 -13
  138. package/dist/workstation/tools/terminalRouter.js +17 -172
  139. package/dist/workstation/tools/validation.js +55 -1
  140. package/dist/workstation/update/archiveEvidence.js +206 -0
  141. package/dist/workstation/update/coordinator.js +34 -13
  142. package/dist/workstation/update/deviceCompatibility.js +119 -0
  143. package/dist/workstation/update/deviceCoordinator.js +369 -0
  144. package/dist/workstation/update/deviceHealth.js +104 -0
  145. package/dist/workstation/update/deviceHealthProtocol.js +26 -0
  146. package/dist/workstation/update/deviceRecoveryLifecycle.js +46 -0
  147. package/dist/workstation/update/deviceRollback.js +78 -0
  148. package/dist/workstation/update/globalInstall.js +80 -10
  149. package/dist/workstation/update/legacyBridge.js +414 -0
  150. package/dist/workstation/update/legacyOwner.js +39 -0
  151. package/dist/workstation/update/npmInstaller.js +2 -2
  152. package/dist/workstation/update/privateDurability.js +61 -0
  153. package/dist/workstation/update/recoveryPlan.js +124 -0
  154. package/dist/workstation/update/runUpdate.js +7 -1
  155. package/dist/workstation/update/taskRegistry.js +161 -3
  156. package/dist/workstation/update/watchdog.js +27427 -14410
  157. package/dist/workstation/update/watchdogProcess.js +72 -0
  158. package/package.json +7 -3
  159. package/dist/native/darwin-arm64/axom-quic +0 -0
@@ -0,0 +1,526 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { execFile } from 'node:child_process';
3
+ import { createRequire } from 'node:module';
4
+ import path from 'node:path';
5
+ import { KeeperMethodError } from '../kit/protocol.js';
6
+ import { TERMINAL_GRID_LIMITS as limits, } from './gridProtocol.js';
7
+ import { TerminalFrames } from './frames.js';
8
+ import { TerminalScreen } from './screen.js';
9
+ import { createTerminalInput } from './input.js';
10
+ import { createTerminalMetadata } from './metadata.js';
11
+ export const NATIVE_PTY_OPERATIONS = new Set(['attach', 'poll', 'resize', 'input', 'scroll', 'detach', 'close']);
12
+ const bad = (message) => { throw new KeeperMethodError('bad_request', message); };
13
+ const fail = (code, message) => { throw new KeeperMethodError(code, message); };
14
+ const ok = { ok: true };
15
+ const nativeScheduler = {
16
+ schedule(callback, delay) { const timer = setTimeout(callback, delay); timer.unref(); return { dispose: () => clearTimeout(timer) }; },
17
+ };
18
+ const basename = (name) => path.basename(name.trim()).replace(/^-/, '');
19
+ /** Keeper-owned sessions: ordered within a PTY, independent between PTYs. */
20
+ export class NativePtySessions {
21
+ options;
22
+ sessions = new Map();
23
+ now;
24
+ scheduler;
25
+ module;
26
+ unavailable;
27
+ disposed = false;
28
+ constructor(options = {}) {
29
+ this.options = options;
30
+ this.now = options.now ?? Date.now;
31
+ this.scheduler = options.scheduler ?? nativeScheduler;
32
+ try {
33
+ this.module = (options.loadPty ?? (() => createRequire(import.meta.url)('node-pty')))();
34
+ }
35
+ catch {
36
+ this.unavailable = 'Terminal native dependency unavailable; reinstall CLI with optional node-pty support';
37
+ }
38
+ }
39
+ list() {
40
+ return { schema: 1, limits: { ...limits }, sessions: [...this.sessions.values()].map(s => this.descriptor(s)) };
41
+ }
42
+ async create(params, launch) {
43
+ this.available();
44
+ request(params);
45
+ keys(params, ['id', 'cwd', 'registrationId']);
46
+ const id = identifier(params, 'id');
47
+ if (params.registrationId !== undefined)
48
+ identifier(params, 'registrationId');
49
+ if (params.cwd !== undefined && (typeof params.cwd !== 'string' || params.cwd.includes('\0')))
50
+ bad('Invalid cwd');
51
+ const existing = this.sessions.get(id);
52
+ if (existing)
53
+ return this.descriptor(existing);
54
+ this.checkLimit();
55
+ const resolved = await launch();
56
+ this.available();
57
+ const concurrent = this.sessions.get(id);
58
+ if (concurrent)
59
+ return this.descriptor(concurrent);
60
+ this.checkLimit();
61
+ const cols = 100;
62
+ const rows = 30;
63
+ const screen = new TerminalScreen(cols, rows);
64
+ let pty;
65
+ try {
66
+ pty = this.module.spawn(resolved.shell, ['-l'], { name: 'xterm-256color', cwd: resolved.cwd, cols, rows, env: { ...resolved.env, TERM: 'xterm-256color', COLORTERM: 'truecolor' } });
67
+ }
68
+ catch (error) {
69
+ screen.dispose();
70
+ throw error;
71
+ }
72
+ const createdAt = this.now();
73
+ const shell = basename(resolved.shell);
74
+ const s = {
75
+ value: { id, ...(typeof params.registrationId === 'string' ? { registrationId: params.registrationId } : {}), incarnation: randomUUID(), title: 'Terminal', cwd: resolved.cwd, cwdSource: 'launch', cols, rows, createdAt, exited: false, exitCode: null },
76
+ shell, shellNames: new Set([shell]), shellNameStarted: false, pty, screen, frames: new TerminalFrames(), attachments: new Map(), geometry: 1,
77
+ closed: false, queue: Promise.resolve(), rendered: Promise.resolve(), pendingBytes: 0, dirty: true, capturedAt: -Infinity,
78
+ listeners: [], input: createTerminalInput(screen), exitedAt: 0, observed: { label: shell, state: 'idle', since: createdAt },
79
+ };
80
+ const context = { pty, screen, session: () => this.descriptor(s), changed: () => this.dirty(s) };
81
+ try {
82
+ s.input = this.options.createInput?.(context) ?? s.input;
83
+ s.metadata = (this.options.createMetadata ?? createTerminalMetadata)(context);
84
+ this.capture(s, true);
85
+ s.listeners.push(disposable(pty.onData(data => this.output(s, data))), disposable(pty.onExit(event => {
86
+ if (s.closed)
87
+ return;
88
+ s.shellNameTimer?.dispose();
89
+ s.shellNameTimer = undefined;
90
+ s.value.exited = true;
91
+ s.value.exitCode = event.exitCode;
92
+ s.exitedAt = this.now();
93
+ this.dirty(s);
94
+ })), screen.onReply(data => { if (!s.closed && !s.value.exited) {
95
+ try {
96
+ pty.write(data);
97
+ }
98
+ catch (error) {
99
+ this.failed(s, error);
100
+ }
101
+ } }));
102
+ this.sessions.set(id, s);
103
+ }
104
+ catch (error) {
105
+ await this.close(s);
106
+ throw error;
107
+ }
108
+ return this.descriptor(s);
109
+ }
110
+ async operate(op, params) {
111
+ request(params);
112
+ const id = identifier(params, 'id');
113
+ const incarnation = identifier(params, 'incarnation');
114
+ const s = this.sessions.get(id);
115
+ if (!s) {
116
+ if (op === 'close' || op === 'detach')
117
+ return ok;
118
+ return fail('pty_not_found', 'Terminal session not found; keeper restart loses live PTYs');
119
+ }
120
+ if (s.value.incarnation !== incarnation)
121
+ return fail('pty_incarnation_mismatch', 'Terminal incarnation no longer matches');
122
+ return this.ordered(s, async () => {
123
+ if (s.closed) {
124
+ if (op === 'close' || op === 'detach')
125
+ return ok;
126
+ return fail('pty_not_found', 'Terminal session is closed');
127
+ }
128
+ if (op === 'close') {
129
+ keys(params, ['id', 'incarnation']);
130
+ await this.close(s);
131
+ return ok;
132
+ }
133
+ await this.expire(s);
134
+ const clientId = identifier(params, 'clientId');
135
+ const ref = ['id', 'incarnation', 'clientId'];
136
+ if (op === 'detach') {
137
+ keys(params, ref);
138
+ this.release(s, clientId);
139
+ await this.capacity(s);
140
+ return ok;
141
+ }
142
+ if (op === 'attach') {
143
+ keys(params, [...ref, 'cols', 'rows']);
144
+ const cols = integer(params, 'cols', limits.minCols, limits.maxCols);
145
+ const rows = integer(params, 'rows', limits.minRows, limits.maxRows);
146
+ if (!s.attachments.has(clientId) && s.attachments.size >= limits.maxAttachments)
147
+ return fail('pty_limit', 'Terminal attachment limit exceeded');
148
+ const previous = s.attachments.get(clientId);
149
+ s.attachments.set(clientId, { cols, rows, expires: this.now() + limits.leaseMs });
150
+ try {
151
+ await this.capacity(s);
152
+ await this.settle(s);
153
+ this.capture(s, true);
154
+ }
155
+ catch (error) {
156
+ if (previous)
157
+ s.attachments.set(clientId, previous);
158
+ else
159
+ this.release(s, clientId);
160
+ try {
161
+ await this.capacity(s);
162
+ }
163
+ catch (rollback) {
164
+ this.failed(s, rollback);
165
+ }
166
+ throw error;
167
+ }
168
+ this.scheduleLease(s);
169
+ return s.frames.poll(0);
170
+ }
171
+ const attachment = s.attachments.get(clientId);
172
+ if (!attachment)
173
+ return fail('pty_attachment_expired', 'Terminal attachment expired; attach again');
174
+ if (op === 'poll') {
175
+ keys(params, [...ref, 'after']);
176
+ const after = integer(params, 'after', 0, Number.MAX_SAFE_INTEGER);
177
+ attachment.expires = this.now() + limits.leaseMs;
178
+ this.scheduleLease(s);
179
+ await this.captureDue(s);
180
+ return s.frames.poll(after);
181
+ }
182
+ if (op === 'resize') {
183
+ keys(params, [...ref, 'cols', 'rows']);
184
+ const cols = integer(params, 'cols', limits.minCols, limits.maxCols);
185
+ const rows = integer(params, 'rows', limits.minRows, limits.maxRows);
186
+ const previous = { ...attachment };
187
+ attachment.cols = cols;
188
+ attachment.rows = rows;
189
+ try {
190
+ await this.capacity(s);
191
+ }
192
+ catch (error) {
193
+ Object.assign(attachment, previous);
194
+ throw error;
195
+ }
196
+ }
197
+ else if (op === 'input') {
198
+ const event = input(params, ref);
199
+ await this.settle(s);
200
+ this.assertRunning(s);
201
+ if (event.kind === 'pointer' || event.kind === 'button')
202
+ this.coordinates(s, event);
203
+ const data = s.input.input(event, clientId, this.inputContext(s));
204
+ if (data)
205
+ s.pty.write(data);
206
+ }
207
+ else if (op === 'scroll') {
208
+ keys(params, [...ref, 'lines', 'x', 'y', 'modifiers', 'geometry']);
209
+ const gesture = { lines: integer(params, 'lines', -Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER), ...position(params), modifiers: integer(params, 'modifiers', 0, 15) };
210
+ await this.settle(s);
211
+ this.coordinates(s, gesture);
212
+ const data = s.input.scroll(gesture, clientId, this.inputContext(s));
213
+ if (data) {
214
+ this.assertRunning(s);
215
+ s.pty.write(data);
216
+ }
217
+ else if (data === undefined && s.screen.state().buffer === 'normal') {
218
+ s.screen.scroll(gesture.lines);
219
+ this.dirty(s);
220
+ }
221
+ }
222
+ else
223
+ return bad('Unsupported terminal operation');
224
+ attachment.expires = this.now() + limits.leaseMs;
225
+ this.scheduleLease(s);
226
+ return ok;
227
+ });
228
+ }
229
+ activity() {
230
+ const metrics = { ptyOpen: 0, ptyActive: 0, ptyExited: 0 };
231
+ const items = [...this.sessions.values()].map(s => {
232
+ if (s.value.exited) {
233
+ metrics.ptyExited++;
234
+ return { type: 'pty', id: s.value.id, label: s.shell.slice(0, 64), state: 'exited', sinceEpochMs: s.exitedAt };
235
+ }
236
+ metrics.ptyOpen++;
237
+ let foreground = '';
238
+ try {
239
+ foreground = basename(s.pty.process);
240
+ }
241
+ catch { /* process can disappear during exit */ }
242
+ const active = foreground !== '' && !s.shellNames.has(foreground);
243
+ const label = (active ? foreground : s.shell).slice(0, 64);
244
+ const state = active ? 'active' : 'idle';
245
+ if (active)
246
+ metrics.ptyActive++;
247
+ if (s.observed.label !== label || s.observed.state !== state)
248
+ s.observed = { label, state, since: this.now() };
249
+ return { type: 'pty', id: s.value.id, label, state, sinceEpochMs: s.observed.since };
250
+ });
251
+ return { metrics, items };
252
+ }
253
+ async dispose() {
254
+ if (this.disposed)
255
+ return;
256
+ this.disposed = true;
257
+ await Promise.all([...this.sessions.values()].map(s => this.ordered(s, () => this.close(s))));
258
+ }
259
+ descriptor(s) { return { ...s.value, ...s.metadata?.read() }; }
260
+ available() {
261
+ if (this.disposed)
262
+ fail('pty_unavailable', 'Terminal host is disposed');
263
+ if (!this.module)
264
+ fail('pty_unavailable', this.unavailable ?? 'Terminal native dependency unavailable');
265
+ }
266
+ checkLimit() { if (this.sessions.size >= limits.maxSessions)
267
+ fail('pty_limit', 'Terminal session limit exceeded'); }
268
+ scheduleShellName(s, attempt = 0) {
269
+ const live = () => !s.closed && !s.value.exited && !this.disposed;
270
+ if (!live())
271
+ return;
272
+ s.shellNameTimer = this.scheduler.schedule(() => {
273
+ s.shellNameTimer = undefined;
274
+ if (!live())
275
+ return;
276
+ const retry = () => { if (attempt < 2)
277
+ this.scheduleShellName(s, attempt + 1); };
278
+ // Query the original shell PID: the foreground process may be a running command.
279
+ void Promise.resolve().then(() => live() ? (this.options.shellProcessName ?? defaultShellProcessName)(s.pty.pid) : null).then(name => {
280
+ if (!live())
281
+ return;
282
+ const shell = name && basename(name);
283
+ if (shell && shell !== 'spawn-helper')
284
+ s.shellNames.add(shell);
285
+ else
286
+ retry();
287
+ }, retry);
288
+ }, this.options.shellNameDelayMs ?? 250);
289
+ }
290
+ ordered(s, action) {
291
+ const operation = s.queue.then(action);
292
+ s.queue = operation.catch(() => { });
293
+ return operation;
294
+ }
295
+ async settle(s) {
296
+ for (;;) {
297
+ const rendered = s.rendered;
298
+ await rendered;
299
+ if (rendered === s.rendered)
300
+ break;
301
+ }
302
+ if (s.failure)
303
+ throw s.failure;
304
+ }
305
+ output(s, data) {
306
+ if (s.closed || !data || s.failure)
307
+ return;
308
+ // node-pty initially runs a spawn-helper trampoline at this PID. First output
309
+ // starts the bounded lookup after shell startup, once per session.
310
+ if (!s.shellNameStarted) {
311
+ s.shellNameStarted = true;
312
+ this.scheduleShellName(s);
313
+ }
314
+ const bytes = Buffer.byteLength(data);
315
+ s.pendingBytes += bytes;
316
+ try {
317
+ if (s.pendingBytes > 256 * 1024)
318
+ s.pty.pause();
319
+ s.metadata?.onData?.(data);
320
+ // Submit directly to xterm's ordered parser; chaining writes costs one timer per PTY chunk.
321
+ s.rendered = s.screen.write(data);
322
+ void s.rendered.then(() => {
323
+ s.pendingBytes -= bytes;
324
+ try {
325
+ if (!s.closed && !s.value.exited && s.pendingBytes < 64 * 1024)
326
+ s.pty.resume();
327
+ }
328
+ catch (error) {
329
+ this.failed(s, error);
330
+ }
331
+ this.dirty(s);
332
+ }, error => this.failed(s, error));
333
+ this.dirty(s);
334
+ }
335
+ catch (error) {
336
+ this.failed(s, error);
337
+ }
338
+ }
339
+ failed(s, error) { if (!s.closed) {
340
+ s.failure = error;
341
+ s.captureTimer?.dispose();
342
+ s.captureTimer = undefined;
343
+ } }
344
+ dirty(s) {
345
+ if (s.closed || s.failure)
346
+ return;
347
+ s.dirty = true;
348
+ if (!s.captureTimer)
349
+ s.captureTimer = this.scheduler.schedule(() => {
350
+ s.captureTimer = undefined;
351
+ void this.ordered(s, async () => { if (s.closed)
352
+ return; await this.settle(s); this.capture(s); }).catch(error => { if (!(error instanceof KeeperMethodError && error.code === 'pty_snapshot_too_large'))
353
+ this.failed(s, error); });
354
+ }, Math.max(0, 50 - (this.now() - s.capturedAt)));
355
+ }
356
+ async captureDue(s) {
357
+ if (s.dirty && this.now() - s.capturedAt < 50) {
358
+ s.captureTimer?.dispose();
359
+ s.captureTimer = undefined;
360
+ await new Promise(resolve => this.scheduler.schedule(resolve, 50 - (this.now() - s.capturedAt)));
361
+ }
362
+ await this.settle(s);
363
+ this.capture(s, true);
364
+ }
365
+ capture(s, force = false) {
366
+ if (!s.dirty || (!force && this.now() - s.capturedAt < 50))
367
+ return;
368
+ s.frames.publish(s.screen.capture(), this.descriptor(s), s.geometry);
369
+ s.dirty = false;
370
+ s.capturedAt = this.now();
371
+ s.captureTimer?.dispose();
372
+ s.captureTimer = undefined;
373
+ }
374
+ async capacity(s) {
375
+ if (!s.attachments.size)
376
+ return;
377
+ const cols = Math.min(...[...s.attachments.values()].map(a => a.cols));
378
+ const rows = Math.min(...[...s.attachments.values()].map(a => a.rows));
379
+ if (cols === s.value.cols && rows === s.value.rows)
380
+ return;
381
+ await this.settle(s);
382
+ if (!s.value.exited)
383
+ s.pty.resize(cols, rows);
384
+ try {
385
+ await s.screen.resize(cols, rows);
386
+ await this.settle(s);
387
+ }
388
+ catch (error) {
389
+ // A native emulator failure may have changed part of the grid. Never publish or accept input against uncertain geometry.
390
+ this.failed(s, error);
391
+ try {
392
+ if (!s.value.exited)
393
+ s.pty.resize(s.value.cols, s.value.rows);
394
+ }
395
+ catch (rollback) {
396
+ this.failed(s, rollback);
397
+ }
398
+ throw error;
399
+ }
400
+ s.value.cols = cols;
401
+ s.value.rows = rows;
402
+ s.geometry++;
403
+ this.dirty(s);
404
+ }
405
+ inputContext(s) { return { cols: s.value.cols, rows: s.value.rows, geometry: s.geometry, modes: s.screen.state().modes }; }
406
+ coordinates(s, event) {
407
+ if (event.geometry !== s.geometry)
408
+ fail('pty_geometry_mismatch', 'Terminal geometry changed');
409
+ if (event.x >= s.value.cols || event.y >= s.value.rows)
410
+ bad('Terminal coordinates outside geometry');
411
+ }
412
+ assertRunning(s) { if (s.value.exited)
413
+ fail('pty_exited', 'Terminal has exited'); }
414
+ release(s, clientId) {
415
+ if (s.attachments.delete(clientId))
416
+ s.input.release(clientId);
417
+ this.scheduleLease(s);
418
+ }
419
+ async expire(s) {
420
+ let changed = false;
421
+ for (const [id, attachment] of s.attachments)
422
+ if (attachment.expires <= this.now()) {
423
+ this.release(s, id);
424
+ changed = true;
425
+ }
426
+ if (changed)
427
+ await this.capacity(s);
428
+ }
429
+ scheduleLease(s) {
430
+ s.leaseTimer?.dispose();
431
+ s.leaseTimer = undefined;
432
+ if (s.closed || this.disposed || !s.attachments.size)
433
+ return;
434
+ const expires = Math.min(...[...s.attachments.values()].map(attachment => attachment.expires));
435
+ s.leaseTimer = this.scheduler.schedule(() => {
436
+ s.leaseTimer = undefined;
437
+ void this.ordered(s, () => this.expire(s)).catch(error => this.failed(s, error)).finally(() => this.scheduleLease(s));
438
+ }, Math.max(0, expires - this.now()));
439
+ }
440
+ async close(s) {
441
+ if (s.closed)
442
+ return;
443
+ s.closed = true;
444
+ if (this.sessions.get(s.value.id) === s)
445
+ this.sessions.delete(s.value.id);
446
+ s.shellNameTimer?.dispose();
447
+ s.shellNameTimer = undefined;
448
+ s.captureTimer?.dispose();
449
+ s.captureTimer = undefined;
450
+ s.leaseTimer?.dispose();
451
+ s.leaseTimer = undefined;
452
+ for (const listener of s.listeners)
453
+ listener.dispose();
454
+ for (const id of [...s.attachments.keys()])
455
+ this.release(s, id);
456
+ s.input.dispose();
457
+ s.metadata?.dispose?.();
458
+ try {
459
+ if (!s.value.exited)
460
+ s.pty.kill();
461
+ }
462
+ finally {
463
+ s.screen.dispose();
464
+ await s.rendered.catch(() => { });
465
+ }
466
+ }
467
+ }
468
+ function disposable(value) {
469
+ return value && typeof value.dispose === 'function' ? value : { dispose: () => { } };
470
+ }
471
+ function request(params) { if (Buffer.byteLength(JSON.stringify(params)) > limits.maxRequestBytes)
472
+ bad('Terminal request exceeds limit'); }
473
+ function keys(params, allowed) { if (Object.keys(params).some(key => !allowed.includes(key)))
474
+ bad('Unexpected terminal parameter'); }
475
+ function identifier(params, name) {
476
+ const value = params[name];
477
+ if (typeof value !== 'string' || !/^[A-Za-z0-9_.:-]{1,128}$/.test(value))
478
+ return bad(`Invalid ${name}`);
479
+ return value;
480
+ }
481
+ function integer(params, name, min, max) {
482
+ const value = params[name];
483
+ if (!Number.isSafeInteger(value) || value < min || value > max)
484
+ return bad(`Invalid ${name}`);
485
+ return value;
486
+ }
487
+ function position(params) {
488
+ return { x: integer(params, 'x', 0, limits.maxCols - 1), y: integer(params, 'y', 0, limits.maxRows - 1), geometry: integer(params, 'geometry', 1, Number.MAX_SAFE_INTEGER) };
489
+ }
490
+ function input(params, ref) {
491
+ switch (params.kind) {
492
+ case 'text':
493
+ case 'paste':
494
+ keys(params, [...ref, 'kind', 'text']);
495
+ if (typeof params.text !== 'string' || Buffer.byteLength(params.text) > limits.maxInputBytes)
496
+ return bad('Terminal input exceeds limit');
497
+ return { kind: params.kind, text: params.text };
498
+ case 'key':
499
+ keys(params, [...ref, 'kind', 'usage', 'down', 'modifiers']);
500
+ if (typeof params.down !== 'boolean')
501
+ return bad('Invalid down');
502
+ return { kind: 'key', usage: integer(params, 'usage', 0, 65535), down: params.down, modifiers: integer(params, 'modifiers', 0, 15) };
503
+ case 'pointer':
504
+ case 'button': {
505
+ keys(params, [...ref, 'kind', 'x', 'y', 'geometry', 'modifiers', ...(params.kind === 'button' ? ['button', 'down'] : [])]);
506
+ const point = { ...position(params), modifiers: integer(params, 'modifiers', 0, 15) };
507
+ if (params.kind === 'pointer')
508
+ return { kind: 'pointer', ...point };
509
+ if (typeof params.down !== 'boolean')
510
+ return bad('Invalid down');
511
+ return { kind: 'button', ...point, button: integer(params, 'button', 0, 2), down: params.down };
512
+ }
513
+ case 'release':
514
+ keys(params, [...ref, 'kind']);
515
+ return { kind: 'release' };
516
+ default: return bad('Invalid terminal input kind');
517
+ }
518
+ }
519
+ /** This lookup is never awaited by poll/input/activity and never exposes process arguments. */
520
+ function defaultShellProcessName(pid) {
521
+ if (process.platform !== 'darwin')
522
+ return Promise.resolve(null);
523
+ return new Promise(resolve => {
524
+ execFile('ps', ['-o', 'ucomm=', '-p', String(pid)], { timeout: 1000 }, (error, stdout) => resolve(error ? null : stdout.trim() || null));
525
+ });
526
+ }
@@ -42,8 +42,11 @@ function requireString(value, field, description) {
42
42
  throw new DeviceAuthorizationError(`${description} missing ${field}`);
43
43
  return value;
44
44
  }
45
- function websocketUrl(server) {
46
- const url = new URL('/oauth/device/ws', server);
45
+ function deviceEndpoint(server, endpoint, identity) {
46
+ return identity ? new URL(server + endpoint) : new URL(endpoint, server);
47
+ }
48
+ function websocketUrl(server, identity) {
49
+ const url = deviceEndpoint(server, '/oauth/device/ws', identity);
47
50
  url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
48
51
  return url.toString();
49
52
  }
@@ -89,23 +92,33 @@ function waitForNotification(socket, sessionId, notificationTicket, deadline, no
89
92
  socket.on('error', () => finish('reconnect'));
90
93
  });
91
94
  }
92
- async function exchangeDeviceCode(server, clientId, deviceCode, fetch_, requestedScopes) {
93
- const response = await requestJson(fetch_, new URL('/oauth/token', server), {
95
+ async function exchangeDeviceCode(server, clientId, deviceCode, fetch_, requestedScopes, identity) {
96
+ const response = await requestJson(fetch_, deviceEndpoint(server, '/oauth/token', identity), {
94
97
  method: 'POST',
95
98
  headers: { 'content-type': 'application/x-www-form-urlencoded' },
96
99
  body: new URLSearchParams({
97
100
  grant_type: DEVICE_CODE_GRANT_TYPE,
98
101
  client_id: clientId,
99
102
  device_code: deviceCode,
100
- device_id: await loadDeviceId(),
103
+ device_id: identity?.deviceId ?? await loadDeviceId(),
101
104
  }),
102
105
  }, 200, 'Device token exchange');
103
- return validateTokenResponse(server, response, requestedScopes, fetch_);
106
+ return validateTokenResponse(server, response, requestedScopes, fetch_, identity);
104
107
  }
105
- async function validateTokenResponse(server, response, requestedScopes, fetch_) {
106
- const accessToken = requireString(response.access_token, 'access_token', 'Device token response');
108
+ async function validateTokenResponse(server, response, requestedScopes, fetch_, identity) {
109
+ let accessToken;
110
+ try {
111
+ accessToken = requireString(response.access_token, 'access_token', 'Device token response');
112
+ }
113
+ catch (error) {
114
+ if (identity && typeof response.refresh_token === 'string' && response.refresh_token)
115
+ await revokeRefreshToken(server, response.refresh_token, fetch_);
116
+ throw error;
117
+ }
107
118
  const refreshToken = requireString(response.refresh_token, 'refresh_token', 'Device token response');
108
119
  if (!Number.isFinite(response.expires_in) || response.expires_in <= 0) {
120
+ if (identity)
121
+ await revokeRefreshToken(server, refreshToken, fetch_);
109
122
  throw new DeviceAuthorizationError('Device token response has invalid expires_in');
110
123
  }
111
124
  let scopes;
@@ -118,15 +131,15 @@ async function validateTokenResponse(server, response, requestedScopes, fetch_)
118
131
  }
119
132
  return { accessToken, refreshToken, expiresInSeconds: response.expires_in, scopes };
120
133
  }
121
- async function pollDeviceCode(server, clientId, deviceCode, fetch_, requestedScopes) {
122
- const response = await fetch_(new URL('/oauth/token', server), {
134
+ async function pollDeviceCode(server, clientId, deviceCode, fetch_, requestedScopes, identity) {
135
+ const response = await fetch_(deviceEndpoint(server, '/oauth/token', identity), {
123
136
  method: 'POST',
124
137
  headers: { 'content-type': 'application/x-www-form-urlencoded' },
125
138
  body: new URLSearchParams({
126
139
  grant_type: DEVICE_CODE_GRANT_TYPE,
127
140
  client_id: clientId,
128
141
  device_code: deviceCode,
129
- device_id: await loadDeviceId(),
142
+ device_id: identity?.deviceId ?? await loadDeviceId(),
130
143
  }),
131
144
  });
132
145
  if (response.status === 400) {
@@ -141,13 +154,13 @@ async function pollDeviceCode(server, clientId, deviceCode, fetch_, requestedSco
141
154
  if (response.status !== 200)
142
155
  throw new DeviceAuthorizationError(`Device token polling failed: ${response.status}`);
143
156
  const token = await response.json();
144
- return validateTokenResponse(server, token, requestedScopes, fetch_);
157
+ return validateTokenResponse(server, token, requestedScopes, fetch_, identity);
145
158
  }
146
159
  /**
147
160
  * Uses OAuth Device Authorization for both desktop and headless clients. The notification socket
148
161
  * only carries state; the tokens exist only in this call's return value.
149
162
  */
150
- export async function authorizeDevice(server, clientName, hostname, deps = {}, requestedScopes = DEFAULT_CLI_SCOPES) {
163
+ export async function authorizeDevice(server, clientName, hostname, deps = {}, requestedScopes = DEFAULT_CLI_SCOPES, identity) {
151
164
  const baseUrl = normalizedServer(server);
152
165
  const fetch_ = deps.fetch ?? globalThis.fetch;
153
166
  const print = deps.print ?? console.log;
@@ -155,9 +168,10 @@ export async function authorizeDevice(server, clientName, hostname, deps = {}, r
155
168
  const createWebSocket = deps.createWebSocket ?? ((url) => new WebSocket(url));
156
169
  const now = deps.now ?? Date.now;
157
170
  const wait = deps.sleep ?? sleep;
171
+ const flowIdentity = identity ? Object.freeze({ deviceId: requireString(identity.deviceId, 'deviceId', 'Device authorization identity') }) : undefined;
158
172
  void clientName;
159
173
  const clientId = 'axom-cli';
160
- const authorization = await requestJson(fetch_, new URL('/oauth/device_authorization', baseUrl), {
174
+ const authorization = await requestJson(fetch_, deviceEndpoint(baseUrl, '/oauth/device_authorization', flowIdentity), {
161
175
  method: 'POST',
162
176
  headers: { 'content-type': 'application/x-www-form-urlencoded' },
163
177
  body: new URLSearchParams({ client_id: clientId, hostname, scope: requestedScopes.join(' ') }),
@@ -176,7 +190,7 @@ export async function authorizeDevice(server, clientName, hostname, deps = {}, r
176
190
  while (now() < deadline) {
177
191
  let socket;
178
192
  try {
179
- socket = createWebSocket(websocketUrl(baseUrl));
193
+ socket = createWebSocket(websocketUrl(baseUrl, flowIdentity));
180
194
  }
181
195
  catch {
182
196
  const delay = Math.min(retry.recordFailure(), Math.max(0, deadline - now()));
@@ -187,14 +201,14 @@ export async function authorizeDevice(server, clientName, hostname, deps = {}, r
187
201
  }
188
202
  const status = await waitForNotification(socket, sessionId, notificationTicket, deadline, now, () => retry.recordSuccess());
189
203
  if (status === 'approved') {
190
- const tokens = await exchangeDeviceCode(baseUrl, clientId, deviceCode, fetch_, requestedScopes);
204
+ const tokens = await exchangeDeviceCode(baseUrl, clientId, deviceCode, fetch_, requestedScopes, flowIdentity);
191
205
  return { clientId, ...tokens };
192
206
  }
193
207
  if (status === 'denied')
194
208
  throw new DeviceAuthorizationError('Device authorization was denied');
195
209
  if (status === 'expired')
196
210
  break;
197
- const polled = await pollDeviceCode(baseUrl, clientId, deviceCode, fetch_, requestedScopes);
211
+ const polled = await pollDeviceCode(baseUrl, clientId, deviceCode, fetch_, requestedScopes, flowIdentity);
198
212
  if (polled)
199
213
  return { clientId, ...polled };
200
214
  const pollingFloor = Math.max(1, authorization.interval ?? 5) * 1_000;