@platless/axom-cli 0.1.0 → 0.5.9

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 (281) hide show
  1. package/README.md +1 -1
  2. package/dist/authScopes.js +19 -0
  3. package/dist/axom-patch/cli/README.md +132 -0
  4. package/dist/axom-patch/cli/account-limits/README.md +27 -0
  5. package/dist/axom-patch/cli/account-limits/antigravity-proxy.mjs +47 -0
  6. package/dist/axom-patch/cli/account-limits/antigravity-read.mjs +90 -0
  7. package/dist/axom-patch/cli/account-limits/antigravity.mjs +46 -0
  8. package/dist/axom-patch/cli/account-limits/generate.py +120 -0
  9. package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +64 -0
  10. package/dist/axom-patch/cli/account-limits/normalize.mjs +151 -0
  11. package/dist/axom-patch/cli/claude-account-limits.json +37 -0
  12. package/dist/axom-patch/cli/codex-account-limits.json +37 -0
  13. package/dist/axom-patch/cli/commandcode-account-limits.json +30 -0
  14. package/dist/axom-patch/cli/nightly-task.md +152 -0
  15. package/dist/axom-patch/cli/pi-steering.json +37 -0
  16. package/dist/axom-patch/cli/pi-subagent-subwork.json +23 -0
  17. package/dist/axom-patch/cli/probes/claude-late-async-terminal.mjs +38 -0
  18. package/dist/axom-patch/cli/probes/pi-steering.mjs +80 -0
  19. package/dist/axom-patch/cli/probes/pi-subagent-subwork.mjs +211 -0
  20. package/dist/axom-patch/cli/probes/qoder-steering.mjs +89 -0
  21. package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +61 -0
  22. package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +257 -0
  23. package/dist/axom-patch/cli/qoder-background-terminal.md +82 -0
  24. package/dist/axom-patch/cli/qoder-foreground-output.md +42 -0
  25. package/dist/axom-patch/cli/qoder-steering.json +52 -0
  26. package/dist/axom-patch/cli/qoder-steering.md +30 -0
  27. package/dist/axom-patch/cli/qoder-subagent-origin.json +73 -0
  28. package/dist/axom-patch/cli/qoder-subagent-subwork.md +73 -0
  29. package/dist/axom-patch/cli/runtime.json +63 -0
  30. package/dist/axom-patch/cli/steering/claude-steering-js.patch +89 -0
  31. package/dist/axom-patch/cli/steering/claude-steering-types.patch +28 -0
  32. package/dist/axom-patch/cli/steering/codex-steering.patch +57 -0
  33. package/dist/axom-patch/cli/steering/pi-steering.js +79 -0
  34. package/dist/axom-patch/cli/zcode-account-limits.json +31 -0
  35. package/dist/axom-patch/cli/zcode-models.json +38 -0
  36. package/dist/axom-patch/cli/zcode-subagent-subwork.json +73 -0
  37. package/dist/cli.js +36 -1
  38. package/dist/commands/auth.js +4 -13
  39. package/dist/commands/execute.js +4 -4
  40. package/dist/commands/record.js +6 -6
  41. package/dist/commands/struct.js +4 -4
  42. package/dist/commands/workstation.js +2 -2
  43. package/dist/credentials.js +2 -2
  44. package/dist/install/claude/commands.js +13 -13
  45. package/dist/keepers/acp/keeper.mjs +3083 -0
  46. package/dist/keepers/acp/manifest.json +9 -0
  47. package/dist/keepers/terminal/keeper.mjs +8469 -0
  48. package/dist/keepers/terminal/manifest.json +9 -0
  49. package/dist/keepers/terminal/node_modules/node-pty/lib/conpty_console_list_agent.js +25 -0
  50. package/dist/keepers/terminal/node_modules/node-pty/lib/eventEmitter2.js +47 -0
  51. package/dist/keepers/terminal/node_modules/node-pty/lib/index.js +52 -0
  52. package/dist/keepers/terminal/node_modules/node-pty/lib/interfaces.js +7 -0
  53. package/dist/keepers/terminal/node_modules/node-pty/lib/shared/conout.js +10 -0
  54. package/dist/keepers/terminal/node_modules/node-pty/lib/terminal.js +190 -0
  55. package/dist/keepers/terminal/node_modules/node-pty/lib/types.js +7 -0
  56. package/dist/keepers/terminal/node_modules/node-pty/lib/unixTerminal.js +351 -0
  57. package/dist/keepers/terminal/node_modules/node-pty/lib/utils.js +38 -0
  58. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsConoutConnection.js +131 -0
  59. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsPtyAgent.js +344 -0
  60. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsTerminal.js +209 -0
  61. package/dist/keepers/terminal/node_modules/node-pty/lib/worker/conoutSocketWorker.js +22 -0
  62. package/dist/keepers/terminal/node_modules/node-pty/package.json +66 -0
  63. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
  64. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
  65. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/pty.node +0 -0
  66. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
  67. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-arm64/pty.node +0 -0
  68. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-x64/pty.node +0 -0
  69. package/dist/mcp.js +32 -14
  70. package/dist/runtime-deps/qoder-sdk/package-lock.json +1189 -0
  71. package/dist/runtime-deps/qoder-sdk/package.json +8 -0
  72. package/dist/test/keeperKit.js +67 -0
  73. package/dist/workstation/acp/acpAgents.js +2514 -681
  74. package/dist/workstation/acp/acpClient.js +95 -318
  75. package/dist/workstation/acp/activityBusy.js +43 -0
  76. package/dist/workstation/acp/activityReporting.js +54 -0
  77. package/dist/workstation/acp/adoption.js +28 -356
  78. package/dist/workstation/acp/authUrlSniffer.js +36 -0
  79. package/dist/workstation/acp/binaryInstall.js +100 -27
  80. package/dist/workstation/acp/builds/gc.js +245 -0
  81. package/dist/workstation/acp/builds/install/binary.js +28 -0
  82. package/dist/workstation/acp/builds/install/npm.js +235 -0
  83. package/dist/workstation/acp/builds/install/uv.js +254 -0
  84. package/dist/workstation/acp/builds/migrate.js +195 -0
  85. package/dist/workstation/acp/builds/model.js +128 -0
  86. package/dist/workstation/acp/builds/nodeGate.js +100 -0
  87. package/dist/workstation/acp/builds/store.js +352 -0
  88. package/dist/workstation/acp/commandTableProbe.js +68 -0
  89. package/dist/workstation/acp/configOptions.js +103 -1
  90. package/dist/workstation/acp/delegationMcp.js +24 -0
  91. package/dist/workstation/acp/envSecrets.js +27 -0
  92. package/dist/workstation/acp/initializeResponse.js +285 -0
  93. package/dist/workstation/acp/installErrors.js +12 -2
  94. package/dist/workstation/acp/installProbe.js +1 -1
  95. package/dist/workstation/acp/managedPatches.js +144 -0
  96. package/dist/workstation/acp/outbox.js +330 -0
  97. package/dist/workstation/acp/patchAssets.js +80 -0
  98. package/dist/workstation/acp/permission.js +335 -37
  99. package/dist/workstation/acp/processor/checkpoint.js +87 -0
  100. package/dist/workstation/acp/processor/commands.js +35 -0
  101. package/dist/workstation/acp/processor/control.js +355 -0
  102. package/dist/workstation/acp/processor/emit.js +26 -0
  103. package/dist/workstation/acp/processor/jsonrpc.js +172 -0
  104. package/dist/workstation/acp/processor/memoryLog.js +49 -0
  105. package/dist/workstation/acp/processor/origins.js +48 -0
  106. package/dist/workstation/acp/processor/output.js +67 -0
  107. package/dist/workstation/acp/processor/projection.js +42 -0
  108. package/dist/workstation/acp/processor/record.js +128 -0
  109. package/dist/workstation/acp/processor/runtime.js +181 -0
  110. package/dist/workstation/acp/processor/sdkSchemas.js +61 -0
  111. package/dist/workstation/acp/processor/session.js +525 -0
  112. package/dist/workstation/acp/processor/sideChannel.js +80 -0
  113. package/dist/workstation/acp/processor/state.js +74 -0
  114. package/dist/workstation/acp/processor/step.js +459 -0
  115. package/dist/workstation/acp/processor/test/fixtureLog.js +65 -0
  116. package/dist/workstation/acp/processor/test/harness.js +120 -0
  117. package/dist/workstation/acp/processor/test/largeIdEcho.js +50 -0
  118. package/dist/workstation/acp/processor/test/rawAgent.js +70 -0
  119. package/dist/workstation/acp/processor/transport/generations.js +76 -0
  120. package/dist/workstation/acp/processor/transport/keeper.js +130 -0
  121. package/dist/workstation/acp/processor/transport/local.js +269 -0
  122. package/dist/workstation/acp/processor/transport/types.js +78 -0
  123. package/dist/workstation/acp/processor/updates.js +364 -0
  124. package/dist/workstation/acp/processorSession.js +1512 -0
  125. package/dist/workstation/acp/qoderStartup.js +45 -0
  126. package/dist/workstation/acp/registry/catalog.js +385 -4
  127. package/dist/workstation/acp/registry/detect.js +55 -0
  128. package/dist/workstation/acp/registry/launch.js +61 -119
  129. package/dist/workstation/acp/sessionUpdateMapper.js +56 -27
  130. package/dist/workstation/acp/steering.js +58 -0
  131. package/dist/workstation/acp/switching.js +114 -0
  132. package/dist/workstation/acp/terminalLogin.js +171 -0
  133. package/dist/workstation/acp/terminalLoginSpec.js +41 -0
  134. package/dist/workstation/acp/test/adapterFixtures.js +256 -0
  135. package/dist/workstation/acp/test/daemonLife.js +106 -0
  136. package/dist/workstation/acp/test/fakeAgent.js +185 -9
  137. package/dist/workstation/acp/test/keeperWorld.js +267 -0
  138. package/dist/workstation/acp/test/memoryOutbox.js +55 -0
  139. package/dist/workstation/acp/test/raw/fixtures.js +196 -0
  140. package/dist/workstation/acp/test/raw/qoderCaptures.js +125 -0
  141. package/dist/workstation/acp/test/raw/sanitize.js +344 -0
  142. package/dist/workstation/acp/test/raw/sources.js +249 -0
  143. package/dist/workstation/acp/test/realAdapter.js +377 -0
  144. package/dist/workstation/acp/test/scriptAgent.js +206 -0
  145. package/dist/workstation/acp/test/sessionFrames.js +42 -0
  146. package/dist/workstation/acp/timedCommand.js +4 -2
  147. package/dist/workstation/acp/uvxInstall.js +72 -122
  148. package/dist/workstation/acp/validate.js +33 -5
  149. package/dist/workstation/agentFlow.js +103 -0
  150. package/dist/workstation/agentSessions.js +1686 -348
  151. package/dist/workstation/channels/agent.js +14 -6
  152. package/dist/workstation/channels/computer.js +203 -7
  153. package/dist/workstation/channels/computerExecs.js +200 -0
  154. package/dist/workstation/channels/management.js +86 -6
  155. package/dist/workstation/config.js +3 -1
  156. package/dist/workstation/connection/encryptedChannel.js +88 -21
  157. package/dist/workstation/daemon.js +309 -6
  158. package/dist/workstation/daemonctl.js +28 -2
  159. package/dist/workstation/desktop/binding.js +36 -0
  160. package/dist/workstation/desktop/bridge.js +212 -0
  161. package/dist/workstation/desktop/flow-control.js +52 -0
  162. package/dist/workstation/desktop/focused-text.js +130 -0
  163. package/dist/workstation/desktop/protocol.js +35 -0
  164. package/dist/workstation/exec/attach.js +126 -0
  165. package/dist/workstation/exec/command.js +40 -0
  166. package/dist/workstation/exec/durable.js +517 -0
  167. package/dist/workstation/exec/fallback.js +106 -0
  168. package/dist/workstation/exec/policy.js +837 -0
  169. package/dist/workstation/exec/shaping.js +211 -0
  170. package/dist/workstation/exec.js +3 -112
  171. package/dist/workstation/files/adapters/computerFileFrames.js +51 -0
  172. package/dist/workstation/files/adapters/toolRequests.js +121 -0
  173. package/dist/workstation/files/client.js +455 -0
  174. package/dist/workstation/files/errors.js +28 -0
  175. package/dist/workstation/files/fence.js +350 -0
  176. package/dist/workstation/files/find/fileIndex.js +153 -0
  177. package/dist/workstation/files/find/fuzzy.js +155 -0
  178. package/dist/workstation/files/host.js +122 -0
  179. package/dist/workstation/files/hostProtocol.js +2 -0
  180. package/dist/workstation/files/media/cache.js +620 -0
  181. package/dist/workstation/files/media/encoding.js +308 -0
  182. package/dist/workstation/files/media/estimator.js +100 -0
  183. package/dist/workstation/files/media/media.js +325 -0
  184. package/dist/workstation/files/media/packedAudio.js +23 -0
  185. package/dist/workstation/files/media/scheduler.js +551 -0
  186. package/dist/workstation/files/media/types.js +5 -0
  187. package/dist/workstation/files/scope.js +45 -0
  188. package/dist/workstation/files/structured.js +284 -0
  189. package/dist/workstation/files/text.js +356 -0
  190. package/dist/workstation/files/uploads.js +351 -0
  191. package/dist/workstation/journal.js +65 -236
  192. package/dist/workstation/keepers/acp/client.js +101 -0
  193. package/dist/workstation/keepers/acp/envelope.js +62 -0
  194. package/dist/workstation/keepers/acp/host.js +419 -0
  195. package/dist/workstation/keepers/acp/main.js +18 -0
  196. package/dist/workstation/keepers/acp/memoryStream.js +121 -0
  197. package/dist/workstation/keepers/acp/params.js +137 -0
  198. package/dist/workstation/keepers/acp/protocol.js +66 -0
  199. package/dist/workstation/keepers/acp/recover.js +120 -0
  200. package/dist/workstation/keepers/acp/session.js +584 -0
  201. package/dist/workstation/keepers/acp/stderr.js +154 -0
  202. package/dist/workstation/keepers/kit/activity.js +103 -0
  203. package/dist/workstation/keepers/kit/bootstrap.js +164 -0
  204. package/dist/workstation/keepers/kit/buildVersion.js +51 -0
  205. package/dist/workstation/keepers/kit/bundle.js +120 -0
  206. package/dist/workstation/keepers/kit/client.js +432 -0
  207. package/dist/workstation/keepers/kit/identity.js +21 -0
  208. package/dist/workstation/keepers/kit/ipc.js +112 -0
  209. package/dist/workstation/keepers/kit/launchConfig.js +8 -0
  210. package/dist/workstation/keepers/kit/launcher.js +249 -0
  211. package/dist/workstation/keepers/kit/lock.js +123 -0
  212. package/dist/workstation/keepers/kit/orphanSweep.js +86 -0
  213. package/dist/workstation/keepers/kit/paths.js +75 -0
  214. package/dist/workstation/keepers/kit/protocol.js +43 -0
  215. package/dist/workstation/keepers/kit/server.js +518 -0
  216. package/dist/workstation/keepers/kit/streamLog.js +697 -0
  217. package/dist/workstation/keepers/legacy.js +296 -0
  218. package/dist/workstation/keepers/policy.js +576 -0
  219. package/dist/workstation/keepers/stop.js +34 -0
  220. package/dist/workstation/keepers/terminal/client.js +213 -0
  221. package/dist/workstation/keepers/terminal/exec.js +981 -0
  222. package/dist/workstation/keepers/terminal/generations.js +129 -0
  223. package/dist/workstation/keepers/terminal/keeper.js +62 -0
  224. package/dist/workstation/keepers/terminal/main.js +43 -0
  225. package/dist/workstation/keepers/terminal/orphanSweep.js +3 -0
  226. package/dist/workstation/keepers/terminal/params.js +27 -0
  227. package/dist/workstation/keepers/terminal/protocol.js +56 -0
  228. package/dist/workstation/keepers/terminal/pty.js +330 -0
  229. package/dist/workstation/oauth/deviceAuthorization.js +21 -12
  230. package/dist/workstation/protocol/accountLimits.js +47 -0
  231. package/dist/workstation/protocol/agent.js +201 -8
  232. package/dist/workstation/protocol/common.js +36 -0
  233. package/dist/workstation/protocol/computer.js +107 -7
  234. package/dist/workstation/protocol/management.js +50 -2
  235. package/dist/workstation/qoderAdapter/backgroundProcess.js +118 -0
  236. package/dist/workstation/qoderAdapter/backgroundTerminal.js +193 -0
  237. package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +372 -0
  238. package/dist/workstation/qoderAdapter/callbackContext.js +76 -0
  239. package/dist/workstation/qoderAdapter/completedContinuation.js +139 -0
  240. package/dist/workstation/qoderAdapter/contract.js +78 -0
  241. package/dist/workstation/qoderAdapter/foregroundOutput.js +36 -0
  242. package/dist/workstation/qoderAdapter/history.js +85 -0
  243. package/dist/workstation/qoderAdapter/identityIndex.js +191 -0
  244. package/dist/workstation/qoderAdapter/inputQueue.js +42 -0
  245. package/dist/workstation/qoderAdapter/main.js +95 -0
  246. package/dist/workstation/qoderAdapter/mapper.js +117 -0
  247. package/dist/workstation/qoderAdapter/observerPlugin.js +245 -0
  248. package/dist/workstation/qoderAdapter/observerReceiver.js +187 -0
  249. package/dist/workstation/qoderAdapter/permissions.js +61 -0
  250. package/dist/workstation/qoderAdapter/profile.js +93 -0
  251. package/dist/workstation/qoderAdapter/sdkDriver.js +48 -0
  252. package/dist/workstation/qoderAdapter/sdkRuntime.js +93 -0
  253. package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +145 -0
  254. package/dist/workstation/qoderAdapter/session.js +319 -0
  255. package/dist/workstation/qoderAdapter/subagentMeta.js +28 -0
  256. package/dist/workstation/qoderAdapter/subwork.js +85 -0
  257. package/dist/workstation/qoderAdapter/taskControl.js +38 -0
  258. package/dist/workstation/qoderAdapter/taskLedger.js +163 -0
  259. package/dist/workstation/qoderAdapter/types.js +1 -0
  260. package/dist/workstation/qoderAdapter/usage.js +37 -0
  261. package/dist/workstation/register.js +4 -0
  262. package/dist/workstation/systemMetrics.js +110 -1
  263. package/dist/workstation/tools/browser.js +394 -0
  264. package/dist/workstation/tools/client.js +277 -0
  265. package/dist/workstation/tools/info.js +50 -0
  266. package/dist/workstation/tools/keeperProcess.js +124 -0
  267. package/dist/workstation/tools/router.js +55 -0
  268. package/dist/workstation/tools/terminalRouter.js +222 -0
  269. package/dist/workstation/tools/validation.js +86 -0
  270. package/dist/workstation/update/coordinator.js +32 -0
  271. package/dist/workstation/update/npmInstaller.js +2 -0
  272. package/dist/workstation/update/ptyPreflight.js +45 -0
  273. package/dist/workstation/update/taskRegistry.js +141 -23
  274. package/dist/workstation/update/watchdog.js +66 -0
  275. package/package.json +20 -7
  276. package/skills/axom/SKILL.md +55 -17
  277. package/skills/axom-definition/SKILL.md +32 -32
  278. package/skills/axom-platform/SKILL.md +39 -39
  279. package/dist/workstation/acp/acpSession.js +0 -1047
  280. package/dist/workstation/acp/contract.js +0 -494
  281. package/dist/workstation/acp/draftInterceptor.js +0 -154
@@ -0,0 +1,212 @@
1
+ import net from 'node:net';
2
+ import { connectFocusedCdp, insertFocusedText } from './focused-text.js';
3
+ import { WebSocket } from 'ws';
4
+ import { browserCapability, resolveBrowserBinding } from './binding.js';
5
+ import { DesktopFlow } from './flow-control.js';
6
+ import { DESKTOP_LEASE_MS, DESKTOP_MAX_STREAMS, DESKTOP_QUEUE_BYTES, DESKTOP_WINDOW_BYTES } from './protocol.js';
7
+ /** Bridges only explicitly configured live providers; owns no browser or profile lifecycle. */
8
+ export class DesktopBridge {
9
+ send;
10
+ bufferedAmount;
11
+ streams = new Map();
12
+ generation = '';
13
+ wireVersion = 1;
14
+ ended = false;
15
+ timer;
16
+ binding;
17
+ constructor(binding, send, bufferedAmount = () => 0) {
18
+ this.send = send;
19
+ this.bufferedAmount = bufferedAmount;
20
+ this.binding = resolveBrowserBinding(binding, {});
21
+ this.timer = setInterval(() => { for (const s of this.streams.values())
22
+ if (Date.now() - s.lease >= DESKTOP_LEASE_MS)
23
+ this.finish(s, 'lease_expired'); }, 1000);
24
+ this.timer.unref();
25
+ }
26
+ ready(generation, wireVersion) {
27
+ for (const s of this.streams.values())
28
+ this.finish(s, 'replaced');
29
+ this.generation = generation;
30
+ this.wireVersion = wireVersion;
31
+ if (wireVersion >= 4)
32
+ this.send({ t: 'desktop_capability', ...browserCapability(this.binding) });
33
+ }
34
+ async insertText(params) {
35
+ if (!this.binding?.cdpEndpoint)
36
+ throw Error('Chinese assistance unavailable: CDP is not configured');
37
+ const stream = this.streams.get(String(params.streamId));
38
+ const active = () => !this.ended && !!stream && this.streams.get(stream.id) === stream && stream.opened &&
39
+ this.binding?.bindingId === params.bindingId && stream.generation === params.generation &&
40
+ this.generation === params.generation && Date.now() - stream.lease < DESKTOP_LEASE_MS;
41
+ return insertFocusedText(params, active, () => connectFocusedCdp(this.binding.cdpEndpoint));
42
+ }
43
+ onFrame(frame) {
44
+ if (this.ended)
45
+ return;
46
+ if (frame.t === 'desktop_open') {
47
+ this.open(frame);
48
+ return;
49
+ }
50
+ if (frame.t === 'desktop_capability')
51
+ return;
52
+ const s = this.streams.get(frame.streamId);
53
+ if (!s)
54
+ return;
55
+ if (frame.generation !== s.generation || frame.generation !== this.generation) {
56
+ this.finish(s, 'invalid_frame');
57
+ return;
58
+ }
59
+ try {
60
+ if (frame.t === 'desktop_close') {
61
+ this.finish(s, frame.code, false);
62
+ return;
63
+ }
64
+ if (frame.t === 'desktop_credit') {
65
+ s.flow.credit(frame.bytes);
66
+ s.lease = Date.now();
67
+ this.schedule(s);
68
+ return;
69
+ }
70
+ if (frame.t === 'desktop_data') {
71
+ if (!s.opened)
72
+ throw new Error('Desktop not opened');
73
+ const bytes = s.flow.receive(frame.seq, frame.data);
74
+ const consumed = (error) => {
75
+ if (!this.streams.has(s.id))
76
+ return;
77
+ if (error) {
78
+ this.finish(s, 'io_error');
79
+ return;
80
+ }
81
+ try {
82
+ s.flow.consumed(bytes.length);
83
+ this.send({ t: 'desktop_credit', streamId: s.id, generation: s.generation, bytes: bytes.length });
84
+ }
85
+ catch {
86
+ this.finish(s, 'io_error');
87
+ }
88
+ };
89
+ if (s.socket instanceof net.Socket)
90
+ s.socket.write(bytes, consumed);
91
+ else {
92
+ if (s.socket.bufferedAmount + bytes.length > DESKTOP_QUEUE_BYTES) {
93
+ this.finish(s, 'overflow');
94
+ return;
95
+ }
96
+ s.socket.send(bytes, { binary: true }, consumed);
97
+ }
98
+ }
99
+ else
100
+ this.finish(s, 'invalid_frame');
101
+ }
102
+ catch {
103
+ this.finish(s, 'invalid_frame');
104
+ }
105
+ }
106
+ close() { if (this.ended)
107
+ return; this.ended = true; clearInterval(this.timer); for (const s of this.streams.values())
108
+ this.finish(s, 'offline', false); }
109
+ open(frame) {
110
+ const refuse = (code) => this.send({ t: 'desktop_close', streamId: frame.streamId, generation: frame.generation, code });
111
+ if (this.wireVersion < 4) {
112
+ refuse('unsupported_protocol');
113
+ return;
114
+ }
115
+ if (frame.generation !== this.generation) {
116
+ refuse('invalid_frame');
117
+ return;
118
+ }
119
+ if (!this.binding || frame.bindingId !== this.binding.bindingId) {
120
+ refuse('unconfigured');
121
+ return;
122
+ }
123
+ if (this.streams.has(frame.streamId) || this.streams.size >= DESKTOP_MAX_STREAMS || this.streams.size > 0) {
124
+ refuse('busy');
125
+ return;
126
+ }
127
+ const url = new URL(this.binding.vncEndpoint);
128
+ const socket = url.protocol === 'tcp:' ? net.createConnection({ host: url.hostname.replace(/^\[|\]$/g, ''), port: Number(url.port) }) : new WebSocket(url, { maxPayload: DESKTOP_QUEUE_BYTES, perMessageDeflate: false });
129
+ const s = { id: frame.streamId, generation: frame.generation, flow: new DesktopFlow(), socket, opened: false, lease: Date.now(), connectTimer: setTimeout(() => this.finish(s, 'connect_timeout'), 10_000) };
130
+ this.streams.set(s.id, s);
131
+ const opened = () => {
132
+ if (!this.streams.has(s.id))
133
+ return;
134
+ clearTimeout(s.connectTimer);
135
+ s.opened = true;
136
+ try {
137
+ this.send({ t: 'desktop_opened', streamId: s.id, bindingId: frame.bindingId, generation: s.generation });
138
+ }
139
+ catch {
140
+ this.finish(s, 'io_error');
141
+ }
142
+ };
143
+ const data = (bytes) => {
144
+ if (!this.streams.has(s.id))
145
+ return;
146
+ try {
147
+ s.flow.enqueue(bytes);
148
+ socket.pause();
149
+ this.schedule(s);
150
+ }
151
+ catch {
152
+ this.finish(s, 'overflow');
153
+ }
154
+ };
155
+ if (socket instanceof net.Socket) {
156
+ socket.once('connect', opened);
157
+ socket.on('data', data);
158
+ }
159
+ else {
160
+ socket.once('open', opened);
161
+ socket.on('message', (raw, binary) => { if (!binary) {
162
+ this.finish(s, 'invalid_frame');
163
+ return;
164
+ } data(Array.isArray(raw) ? Buffer.concat(raw) : Buffer.from(raw)); });
165
+ }
166
+ socket.once('error', () => this.finish(s, s.opened ? 'io_error' : 'connect_failed'));
167
+ socket.once('close', () => this.finish(s, 'io_error'));
168
+ }
169
+ schedule(s) {
170
+ if (s.scheduled || !this.streams.has(s.id))
171
+ return;
172
+ // Yield after every raw block so shell/file results and channel heartbeats can run.
173
+ s.scheduled = setTimeout(() => {
174
+ s.scheduled = undefined;
175
+ if (!this.streams.has(s.id))
176
+ return;
177
+ try {
178
+ if (this.bufferedAmount() > DESKTOP_QUEUE_BYTES) {
179
+ this.finish(s, 'overflow');
180
+ return;
181
+ }
182
+ let blocks = 0;
183
+ s.flow.flush((seq, data) => this.send({ t: 'desktop_data', streamId: s.id, generation: s.generation, seq, data }), () => this.bufferedAmount() < DESKTOP_WINDOW_BYTES && blocks++ === 0);
184
+ if (s.flow.queuedBytes) {
185
+ if (s.flow.canSend)
186
+ this.schedule(s);
187
+ }
188
+ else if (s.flow.canSend)
189
+ s.socket.resume();
190
+ }
191
+ catch {
192
+ this.finish(s, 'io_error');
193
+ }
194
+ }, 1);
195
+ }
196
+ finish(s, code, notify = true) {
197
+ if (!this.streams.delete(s.id))
198
+ return;
199
+ clearTimeout(s.connectTimer);
200
+ if (s.scheduled)
201
+ clearTimeout(s.scheduled);
202
+ if (s.socket instanceof net.Socket)
203
+ s.socket.destroy();
204
+ else
205
+ s.socket.terminate();
206
+ if (notify && !this.ended)
207
+ try {
208
+ this.send({ t: 'desktop_close', streamId: s.id, generation: s.generation, code });
209
+ }
210
+ catch { /* channel closed */ }
211
+ }
212
+ }
@@ -0,0 +1,52 @@
1
+ import { desktopBytes, DESKTOP_BLOCK_BYTES, DESKTOP_WINDOW_BYTES, DESKTOP_QUEUE_BYTES } from './protocol.js';
2
+ export { DESKTOP_WINDOW_BYTES, DESKTOP_QUEUE_BYTES } from './protocol.js';
3
+ /** Independent raw-byte windows; byte queues never discard or replay an RFB block. */
4
+ export class DesktopFlow {
5
+ queue = [];
6
+ queuedBytes = 0;
7
+ sendAvailable = DESKTOP_WINDOW_BYTES;
8
+ receiveAvailable = DESKTOP_WINDOW_BYTES;
9
+ sendSeq = 0;
10
+ receiveSeq = 0;
11
+ get canSend() { return this.sendAvailable > 0; }
12
+ enqueue(bytes) {
13
+ if (this.queuedBytes + bytes.length > DESKTOP_QUEUE_BYTES || this.queue.length + Math.ceil(bytes.length / DESKTOP_BLOCK_BYTES) > 1024)
14
+ throw new Error('Desktop queue overflow');
15
+ for (let i = 0; i < bytes.length; i += DESKTOP_BLOCK_BYTES)
16
+ this.queue.push(Buffer.from(bytes.subarray(i, i + DESKTOP_BLOCK_BYTES)));
17
+ this.queuedBytes += bytes.length;
18
+ }
19
+ flush(send, writable = () => true) {
20
+ // One block per event-loop turn at the adapter keeps tool/control frames schedulable.
21
+ while (this.queue.length && this.sendAvailable && writable()) {
22
+ const head = this.queue[0];
23
+ const size = Math.min(head.length, this.sendAvailable);
24
+ const bytes = head.subarray(0, size);
25
+ if (size === head.length)
26
+ this.queue.shift();
27
+ else
28
+ this.queue[0] = head.subarray(size);
29
+ this.queuedBytes -= size;
30
+ this.sendAvailable -= size;
31
+ send(this.sendSeq++, bytes.toString('base64'));
32
+ }
33
+ }
34
+ credit(bytes) {
35
+ if (!Number.isSafeInteger(bytes) || bytes < 0 || bytes > DESKTOP_WINDOW_BYTES - this.sendAvailable)
36
+ throw new Error('Invalid desktop credit');
37
+ this.sendAvailable += bytes;
38
+ }
39
+ receive(seq, data) {
40
+ const bytes = desktopBytes(data);
41
+ if (seq !== this.receiveSeq || !Number.isSafeInteger(seq) || bytes.length > this.receiveAvailable)
42
+ throw new Error('Invalid desktop sequence/window');
43
+ this.receiveSeq++;
44
+ this.receiveAvailable -= bytes.length;
45
+ return bytes;
46
+ }
47
+ consumed(bytes) {
48
+ if (!Number.isSafeInteger(bytes) || bytes <= 0 || bytes > DESKTOP_WINDOW_BYTES - this.receiveAvailable)
49
+ throw new Error('Invalid desktop consumption');
50
+ this.receiveAvailable += bytes;
51
+ }
52
+ }
@@ -0,0 +1,130 @@
1
+ import { WebSocket } from 'ws';
2
+ import { keys, string } from '../tools/validation.js';
3
+ // activeElement on the outer document is the iframe or shadow host, not the
4
+ // actual input. Follow only the current focus chain; never search for another
5
+ // editor or force focus. Opaque cross-origin/closed roots remain noneditable.
6
+ const focusExpression = `(() => {
7
+ let e=document.activeElement;
8
+ for(let depth=0;e && depth<32;depth++) {
9
+ let child=e.shadowRoot?.activeElement;
10
+ if(!child && (e.tagName==='IFRAME' || e.tagName==='FRAME')) {
11
+ try { child=e.contentDocument?.activeElement; } catch {}
12
+ }
13
+ if(!child) break;
14
+ e=child;
15
+ }
16
+ return {focused:document.hasFocus(),editable:!!e && !e.disabled && !e.readOnly &&
17
+ (e.isContentEditable || e.tagName==='TEXTAREA' || e.tagName==='INPUT' && /^(text|search|email|url|tel|password|number)$/.test(e.type))};
18
+ })()`;
19
+ export async function connectFocusedCdp(endpoint) {
20
+ const response = await fetch(new URL('/json/version', endpoint), { signal: AbortSignal.timeout(1000), redirect: 'error' });
21
+ if (!response.ok)
22
+ throw Error('CDP unavailable');
23
+ const version = await response.json();
24
+ const url = new URL(version.webSocketDebuggerUrl ?? '');
25
+ const configured = new URL(endpoint);
26
+ if (!['ws:', 'wss:'].includes(url.protocol) || url.hostname !== configured.hostname || url.port !== configured.port || url.username || url.password)
27
+ throw Error('Invalid local CDP address');
28
+ const socket = new WebSocket(url, { maxPayload: 1024 * 1024, perMessageDeflate: false });
29
+ let counter = 0;
30
+ const pending = new Map();
31
+ const fail = () => { for (const p of pending.values()) {
32
+ clearTimeout(p.timer);
33
+ p.reject(Error('CDP unavailable'));
34
+ } pending.clear(); };
35
+ socket.on('error', fail);
36
+ socket.on('close', fail);
37
+ socket.on('message', raw => { try {
38
+ const value = JSON.parse(raw.toString());
39
+ const p = pending.get(value.id);
40
+ if (!p)
41
+ return;
42
+ pending.delete(value.id);
43
+ clearTimeout(p.timer);
44
+ value.error ? p.reject(Error('CDP operation refused')) : p.resolve(value.result);
45
+ }
46
+ catch {
47
+ socket.terminate();
48
+ } });
49
+ await new Promise((resolve, reject) => { const timer = setTimeout(() => { socket.terminate(); reject(Error('CDP unavailable')); }, 1000); socket.once('open', () => { clearTimeout(timer); resolve(); }); socket.once('error', () => { clearTimeout(timer); reject(Error('CDP unavailable')); }); });
50
+ function request(method, params = {}, sessionId) {
51
+ const id = ++counter;
52
+ return new Promise((resolve, reject) => {
53
+ const timer = setTimeout(() => { pending.delete(id); reject(Error('CDP timed out; write outcome may be unknown')); }, 1000);
54
+ pending.set(id, { resolve, reject, timer });
55
+ try {
56
+ socket.send(JSON.stringify({ id, method, params, ...(sessionId ? { sessionId } : {}) }));
57
+ }
58
+ catch {
59
+ clearTimeout(timer);
60
+ pending.delete(id);
61
+ reject(Error('CDP unavailable'));
62
+ }
63
+ });
64
+ }
65
+ const sessions = new Map();
66
+ return {
67
+ pages: async () => { const { targetInfos } = await request('Target.getTargets'); return targetInfos.filter((t) => t.type === 'page').map((t) => t.targetId); },
68
+ focus: async (id) => {
69
+ let session = sessions.get(id);
70
+ if (!session) {
71
+ session = (await request('Target.attachToTarget', { targetId: id, flatten: true })).sessionId;
72
+ sessions.set(id, session);
73
+ }
74
+ const r = await request('Runtime.evaluate', { expression: focusExpression, returnByValue: true }, session);
75
+ if (r.exceptionDetails || typeof r.result?.value?.focused !== 'boolean' || typeof r.result?.value?.editable !== 'boolean')
76
+ throw Error('CDP focus unavailable');
77
+ return r.result.value;
78
+ },
79
+ insert: (id, text) => request('Input.insertText', { text }, sessions.get(id)).then(() => { }),
80
+ close: () => socket.terminate(), // Closes this debugger connection only, never Browser.close.
81
+ };
82
+ }
83
+ export async function insertFocusedText(params, active, connect) {
84
+ keys(params, ['bindingId', 'streamId', 'generation', 'text']);
85
+ for (const key of ['bindingId', 'streamId', 'generation'])
86
+ string(params, key, 128);
87
+ const text = string(params, 'text', 8192);
88
+ if (/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(text) || /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/.test(text))
89
+ throw Error('Invalid text');
90
+ const deadline = Date.now() + 6000;
91
+ const check = () => { if (!active() || Date.now() >= deadline)
92
+ throw Error('Viewer inactive, replaced or text assistance expired'); };
93
+ check();
94
+ const cdp = await connect();
95
+ try {
96
+ check();
97
+ const pages = await cdp.pages();
98
+ check();
99
+ if (pages.length > 64)
100
+ throw Error('Ambiguous focus');
101
+ const focused = [];
102
+ for (const id of pages) {
103
+ const state = await cdp.focus(id);
104
+ check();
105
+ if (state.focused)
106
+ focused.push({ id, ...state });
107
+ }
108
+ if (focused.length !== 1)
109
+ throw Error('Unique document focus unavailable');
110
+ const target = focused[0];
111
+ if (!target.editable)
112
+ throw Error('Focused element is not editable');
113
+ const final = await cdp.focus(target.id);
114
+ check();
115
+ if (!final.focused || !final.editable)
116
+ throw Error('Editable focus changed');
117
+ // Once insertion is enqueued, neither a missing reply nor takeover can establish nonexecution.
118
+ try {
119
+ await cdp.insert(target.id, text);
120
+ check();
121
+ return { ok: true };
122
+ }
123
+ catch (cause) {
124
+ throw new Error('Text outcome unknown; check remote content before sending again', { cause });
125
+ }
126
+ }
127
+ finally {
128
+ cdp.close();
129
+ }
130
+ }
@@ -0,0 +1,35 @@
1
+ export const DESKTOP_BLOCK_BYTES = 64 * 1024;
2
+ export const DESKTOP_WINDOW_BYTES = 512 * 1024;
3
+ export const DESKTOP_QUEUE_BYTES = 4 * 1024 * 1024;
4
+ export const DESKTOP_MAX_STREAMS = 4;
5
+ export const DESKTOP_LEASE_MS = 45_000;
6
+ export const DESKTOP_CLOSE_CODES = ['normal', 'unconfigured', 'busy', 'unsupported_protocol', 'invalid_frame', 'connect_failed', 'connect_timeout', 'io_error', 'overflow', 'lease_expired', 'offline', 'replaced'];
7
+ export function desktopBytes(data) {
8
+ if (!data || data.length > Math.ceil(DESKTOP_BLOCK_BYTES / 3) * 4 || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(data))
9
+ throw new Error('Invalid desktop bytes');
10
+ const bytes = Buffer.from(data, 'base64');
11
+ if (bytes.length > DESKTOP_BLOCK_BYTES || bytes.toString('base64') !== data)
12
+ throw new Error('Invalid desktop bytes');
13
+ return bytes;
14
+ }
15
+ export function validDesktopFrame(frame) {
16
+ const id = (v) => typeof v === 'string' && v.length > 0 && v.length <= 128;
17
+ const keys = (allowed) => Object.keys(frame).every(k => allowed.includes(k));
18
+ if (frame.t === 'desktop_capability')
19
+ return keys(['t', 'bindingId', 'displayName', 'configured', 'cdp']) && typeof frame.configured === 'boolean' && typeof frame.cdp === 'boolean' && (frame.bindingId === null || id(frame.bindingId)) && (frame.displayName === null || id(frame.displayName)) && (frame.configured ? frame.bindingId !== null && frame.displayName !== null : frame.bindingId === null && frame.displayName === null && frame.cdp === false);
20
+ if (!id(frame.streamId) || !id(frame.generation))
21
+ return false;
22
+ switch (frame.t) {
23
+ case 'desktop_open':
24
+ case 'desktop_opened': return id(frame.bindingId) && keys(['t', 'streamId', 'generation', 'bindingId']);
25
+ case 'desktop_data': try {
26
+ return keys(['t', 'streamId', 'generation', 'seq', 'data']) && Number.isSafeInteger(frame.seq) && frame.seq >= 0 && typeof frame.data === 'string' && desktopBytes(frame.data).length > 0;
27
+ }
28
+ catch {
29
+ return false;
30
+ }
31
+ case 'desktop_credit': return keys(['t', 'streamId', 'generation', 'bytes']) && Number.isSafeInteger(frame.bytes) && frame.bytes >= 0 && frame.bytes <= DESKTOP_WINDOW_BYTES;
32
+ case 'desktop_close': return keys(['t', 'streamId', 'generation', 'code']) && DESKTOP_CLOSE_CODES.includes(frame.code);
33
+ default: return false;
34
+ }
35
+ }
@@ -0,0 +1,126 @@
1
+ import { ExecWireText } from './shaping.js';
2
+ /** Spec §4.3: `exec_output` waits while the socket holds more than the high mark, until it drains below the low mark. */
3
+ export const EXEC_WINDOW_HIGH_BYTES = 1024 * 1024;
4
+ export const EXEC_WINDOW_LOW_BYTES = 256 * 1024;
5
+ const WINDOW_POLL_MS = 20;
6
+ /** Records per `exec.read` page (the keeper also caps a page at 1.5 MiB). */
7
+ const PAGE_RECORDS = 1024;
8
+ /** Re-read cadence when nothing new arrived and no progress was signalled. */
9
+ const IDLE_POLL_MS = 500;
10
+ const RETRY_MS = 1000;
11
+ /** The v5 `exec_exit` of an ended durable exec (`exitFrame` is the today-shaped frame, exec/policy.ts `durableExitFrame`). */
12
+ export function wireExitFrame(exitFrame, outcome, gapBytes) {
13
+ const signal = outcome.state === 'exited' ? outcome.exit.signal : null;
14
+ return { ...exitFrame, cancelled: outcome.cancelled, ...(signal ? { signal } : {}), ...(gapBytes > 0 ? { gaps: gapBytes } : {}) };
15
+ }
16
+ const sleep = (ms, signal) => new Promise((resolve) => {
17
+ if (signal.aborted) {
18
+ resolve();
19
+ return;
20
+ }
21
+ const done = () => { clearTimeout(timer); signal.removeEventListener('abort', done); resolve(); };
22
+ const timer = setTimeout(done, ms);
23
+ signal.addEventListener('abort', done, { once: true });
24
+ });
25
+ /** Resolves once the socket has room for more output: at once below the high mark, else when it drained below the low mark. */
26
+ export async function waitForWindow(bufferedAmount, signal, pollMs = WINDOW_POLL_MS) {
27
+ if (bufferedAmount() <= EXEC_WINDOW_HIGH_BYTES)
28
+ return;
29
+ while (!signal.aborted && bufferedAmount() > EXEC_WINDOW_LOW_BYTES)
30
+ await sleep(pollMs, signal);
31
+ }
32
+ /**
33
+ * Streams `exec` to `channel`: reads its records from keeper-terminal from the commit point on (decoding silently up to
34
+ * `fromOffset`), sends one `exec_output` per chunk at or after `fromOffset`, honouring the socket window, then the final
35
+ * `exec_status` and `exec_exit` once the exec ended. A non-streaming exec sends only the final two. `stop()` ends it silently
36
+ * (the channel closed, or a newer attach replaced it); the exec itself is untouched.
37
+ */
38
+ export class ExecAttachment {
39
+ exec;
40
+ channel;
41
+ options;
42
+ abort = new AbortController();
43
+ done;
44
+ constructor(exec, channel, options) {
45
+ this.exec = exec;
46
+ this.channel = channel;
47
+ this.options = options;
48
+ this.done = this.run().catch((error) => { this.options.log?.(`exec ${options.id}: attachment failed: ${error instanceof Error ? error.message : String(error)}`); });
49
+ }
50
+ stop() { this.abort.abort(); }
51
+ get stopped() { return this.abort.signal.aborted; }
52
+ async run() {
53
+ const { exec } = this;
54
+ if (exec.wire?.streaming !== false)
55
+ await this.stream();
56
+ if (this.stopped)
57
+ return;
58
+ const outcome = await Promise.race([exec.outcome, new Promise((resolve) => this.abort.signal.addEventListener('abort', () => resolve(null), { once: true }))]);
59
+ if (outcome === null || this.stopped)
60
+ return;
61
+ const id = this.options.id;
62
+ this.channel.send({ t: 'exec_status', id, state: 'exited', headOffset: exec.finalHead, committedOffset: exec.committedOffset });
63
+ this.channel.send(this.options.exitFrame(outcome));
64
+ }
65
+ /** Waits for more records: a progress signal from the exec's own follower, a stop, or the idle poll. */
66
+ wait(ms) {
67
+ return new Promise((resolve) => {
68
+ const unsubscribe = this.exec.onProgress(() => { unsubscribe(); resolve(); });
69
+ void sleep(ms, this.abort.signal).then(() => { unsubscribe(); resolve(); });
70
+ });
71
+ }
72
+ async stream() {
73
+ const { exec, channel } = this;
74
+ const { id, fromOffset } = this.options;
75
+ const text = new ExecWireText();
76
+ let cursor = exec.commitFloor + 1;
77
+ const hold = exec.hold(cursor);
78
+ try {
79
+ while (!this.stopped) {
80
+ const exitAt = exec.exitRecordOffset;
81
+ if (exitAt !== undefined && cursor > exitAt)
82
+ return;
83
+ let page;
84
+ try {
85
+ page = await exec.read(cursor, PAGE_RECORDS);
86
+ }
87
+ catch (error) {
88
+ if (exec.finished)
89
+ return; // deleted, reclaimed or unreachable: the outcome says so
90
+ this.options.log?.(`exec ${id}: read failed: ${error instanceof Error ? error.message : String(error)}`);
91
+ await this.wait(RETRY_MS);
92
+ continue;
93
+ }
94
+ if (this.stopped)
95
+ return;
96
+ if (page.records.length === 0) {
97
+ if (exec.finished)
98
+ return;
99
+ await this.wait(this.options.idleMs ?? IDLE_POLL_MS);
100
+ continue;
101
+ }
102
+ for (const record of page.records) {
103
+ if (record.o < cursor)
104
+ continue;
105
+ cursor = record.o + 1;
106
+ hold.at = cursor;
107
+ if (record.k === 'exit')
108
+ return;
109
+ if (record.k !== 'out' && record.k !== 'err')
110
+ continue;
111
+ const stream = record.k === 'out' ? 'stdout' : 'stderr';
112
+ const chunk = text.push(stream, record.so, Buffer.from(record.b, 'base64'));
113
+ if (!chunk || record.o < fromOffset)
114
+ continue;
115
+ await waitForWindow(() => channel.bufferedAmount(), this.abort.signal);
116
+ if (this.stopped)
117
+ return;
118
+ channel.send({ t: 'exec_output', id, stream, chunk, offset: record.o, recvAtMs: record.t });
119
+ }
120
+ }
121
+ }
122
+ finally {
123
+ exec.unhold(hold);
124
+ }
125
+ }
126
+ }
@@ -0,0 +1,40 @@
1
+ // What the daemon decides about one `exec_request` before anything runs (spec §4.2 "daemon (policy)" column; analysis E §6.3–6.4):
2
+ // the exact argv, the complete environment and the absolute deadline. Shared by the durable (keeper-terminal) path and the
3
+ // in-process fallback, so both run the same command line in the same environment.
4
+ import path from 'node:path';
5
+ /**
6
+ * `[shell, '-lc', command]`, element by element what the daemon has always spawned (`spawn(req.shell, ['-lc', req.command])`):
7
+ * a login, non-interactive shell of the user's choice reading the command string exactly. Never wrapped (`sh -c`, `; echo $?`, …):
8
+ * any wrapping would go through the user's shell dialect (zsh EQUALS, fish quoting).
9
+ */
10
+ export function execArgv(req) {
11
+ return [req.shell, '-lc', req.command];
12
+ }
13
+ /** The daemon's credential for its own server: never visible to a command (commands/workstation.ts drops it already; again here). */
14
+ export const ACCESS_TOKEN_ENV = 'AXOM_ACCESS_TOKEN';
15
+ /**
16
+ * The complete environment of the command: the daemon's environment without the access token, then the request's `env`, then
17
+ * `pathPrepend` in front of PATH (exactly as the daemon always did). keeper-terminal gets this whole map and never falls back to its
18
+ * own environment (under a systemd transient unit that would be the user manager's, frozen at the keeper's start).
19
+ */
20
+ export function execEnvironment(daemonEnv, req) {
21
+ const env = {};
22
+ for (const [name, value] of Object.entries(daemonEnv))
23
+ if (value !== undefined && name !== ACCESS_TOKEN_ENV)
24
+ env[name] = value;
25
+ Object.assign(env, req.env);
26
+ if (req.pathPrepend.length > 0)
27
+ env.PATH = [...req.pathPrepend, env.PATH ?? ''].filter(Boolean).join(path.delimiter);
28
+ return env;
29
+ }
30
+ /**
31
+ * The absolute deadline, from `timeoutSeconds` and the DAEMON's clock at the moment it first received the request (never the
32
+ * server's clock). No timeout (absent, null, 0 or negative: today's rule) means no deadline.
33
+ */
34
+ export function execDeadline(req, receivedAtMs) {
35
+ return req.timeoutSeconds && req.timeoutSeconds > 0 ? receivedAtMs + req.timeoutSeconds * 1000 : undefined;
36
+ }
37
+ /** keeper-terminal wants an absolute cwd; a relative one resolves against the daemon's cwd, as `spawn` always resolved it. */
38
+ export function execCwd(req) {
39
+ return path.resolve(req.cwd || '.');
40
+ }