@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.
Files changed (163) hide show
  1. package/README.md +18 -5
  2. package/dist/axom-patch/cli/README.md +100 -12
  3. package/dist/axom-patch/cli/account-limits/generate.py +15 -12
  4. package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +20 -3
  5. package/dist/axom-patch/cli/claude-account-limits.json +9 -9
  6. package/dist/axom-patch/cli/codex-account-limits.json +11 -11
  7. package/dist/axom-patch/cli/codex-terminal-output.json +16 -0
  8. package/dist/axom-patch/cli/commandcode-account-limits.json +9 -9
  9. package/dist/axom-patch/cli/kimi-quota.mjs +12 -0
  10. package/dist/axom-patch/cli/kimi-subagents.mjs +173 -0
  11. package/dist/axom-patch/cli/nightly-task.md +55 -13
  12. package/dist/axom-patch/cli/probes/codex-terminal-output.mjs +33 -0
  13. package/dist/axom-patch/cli/probes/kimi-subagent-subwork.mjs +82 -0
  14. package/dist/axom-patch/cli/qoder/dist/axom-acp-compat.mjs +1591 -0
  15. package/dist/axom-patch/cli/qoder/releases/1.1.75.json +360 -0
  16. package/dist/axom-patch/cli/qoder-background-terminal.md +13 -6
  17. package/dist/axom-patch/cli/qoder-foreground-output.md +14 -8
  18. package/dist/axom-patch/cli/qoder-native-acp.json +227 -0
  19. package/dist/axom-patch/cli/qoder-steering.md +20 -25
  20. package/dist/axom-patch/cli/qoder-subagent-subwork.md +17 -68
  21. package/dist/axom-patch/cli/runtime.json +16 -18
  22. package/dist/axom-patch/cli/zcode-account-limits.json +10 -11
  23. package/dist/axom-patch/cli/zcode-models.json +1 -2
  24. package/dist/axom-patch/cli/zcode-subagent-subwork.json +9 -10
  25. package/dist/cli.js +4 -36
  26. package/dist/commands/knowledge.js +47 -0
  27. package/dist/commands/update.js +65 -3
  28. package/dist/commands/workstation.js +73 -7
  29. package/dist/install/claude/commands.js +13 -13
  30. package/dist/mcp.js +1 -1
  31. package/dist/native/darwin-arm64/axom-quic +0 -0
  32. package/dist/workstation/acp/acpAgents.js +102 -98
  33. package/dist/workstation/acp/acpClient.js +26 -0
  34. package/dist/workstation/acp/builds/install/npm.js +99 -10
  35. package/dist/workstation/acp/initializeResponse.js +6 -1
  36. package/dist/workstation/acp/managedPatches.js +189 -25
  37. package/dist/workstation/acp/managedRuntime.js +314 -0
  38. package/dist/workstation/acp/patchAssets.js +40 -0
  39. package/dist/workstation/acp/processor/checkpoint.js +25 -34
  40. package/dist/workstation/acp/processor/control.js +5 -78
  41. package/dist/workstation/acp/processor/runtime.js +5 -81
  42. package/dist/workstation/acp/processor/session.js +51 -80
  43. package/dist/workstation/acp/processor/state.js +6 -13
  44. package/dist/workstation/acp/processor/step.js +3 -20
  45. package/dist/workstation/acp/processor/test/fixtureLog.js +9 -16
  46. package/dist/workstation/acp/processor/updates.js +131 -105
  47. package/dist/workstation/acp/processorSession.js +8 -172
  48. package/dist/workstation/acp/registry/catalog.js +65 -39
  49. package/dist/workstation/acp/registry/launch.js +16 -1
  50. package/dist/workstation/acp/switching.js +0 -3
  51. package/dist/workstation/acp/test/fakeAgent.js +13 -0
  52. package/dist/workstation/acp/test/keeperWorld.js +28 -0
  53. package/dist/workstation/acp/test/raw/sources.js +16 -130
  54. package/dist/workstation/acp/test/realAdapter.js +1 -47
  55. package/dist/workstation/acp/test/scriptAgent.js +11 -3
  56. package/dist/workstation/agentFlow.js +1 -1
  57. package/dist/workstation/agentSessions.js +38 -143
  58. package/dist/workstation/channels/management.js +95 -11
  59. package/dist/workstation/channels/url.js +1 -1
  60. package/dist/workstation/config.js +16 -4
  61. package/dist/workstation/connection/encryptedChannel.js +4 -1
  62. package/dist/workstation/control/bindingStore.js +200 -0
  63. package/dist/workstation/control/commands.js +124 -0
  64. package/dist/workstation/control/daemonIdentity.js +53 -0
  65. package/dist/workstation/control/enrollment.js +106 -0
  66. package/dist/workstation/control/inspect.js +71 -0
  67. package/dist/workstation/control/locks.js +63 -0
  68. package/dist/workstation/control/protocol.js +40 -0
  69. package/dist/workstation/control/restartGuard.js +46 -0
  70. package/dist/workstation/control/runtimeClient.js +97 -0
  71. package/dist/workstation/control/runtimeHost.js +148 -0
  72. package/dist/workstation/control/serverIdentity.js +35 -0
  73. package/dist/workstation/daemon.js +106 -6
  74. package/dist/workstation/daemonctl.js +77 -14
  75. package/dist/workstation/desktopControl/audio.js +99 -0
  76. package/dist/workstation/desktopControl/capabilities.js +92 -0
  77. package/dist/workstation/desktopControl/captureModes.js +9 -0
  78. package/dist/workstation/desktopControl/captureTargetLabel.js +9 -0
  79. package/dist/workstation/desktopControl/deviceSockets.js +246 -0
  80. package/dist/workstation/desktopControl/helperProcess.js +353 -0
  81. package/dist/workstation/desktopControl/lobby.js +129 -0
  82. package/dist/workstation/desktopControl/macProvider.js +155 -0
  83. package/dist/workstation/desktopControl/media.js +331 -0
  84. package/dist/workstation/desktopControl/nativeProvider.js +1018 -0
  85. package/dist/workstation/desktopControl/networkEvidence.js +43 -0
  86. package/dist/workstation/desktopControl/overview.js +58 -0
  87. package/dist/workstation/desktopControl/protocol.js +302 -0
  88. package/dist/workstation/desktopControl/quicMedia.js +240 -0
  89. package/dist/workstation/desktopControl/rateController.js +226 -0
  90. package/dist/workstation/desktopControl/supervisor.js +1681 -0
  91. package/dist/workstation/desktopControl/transport.js +27 -0
  92. package/dist/workstation/desktopControl/wallpaper.js +78 -0
  93. package/dist/workstation/files/credentials.js +142 -0
  94. package/dist/workstation/files/text.js +42 -8
  95. package/dist/workstation/keepers/kit/bundle.js +21 -17
  96. package/dist/workstation/keepers/kit/launcher.js +47 -7
  97. package/dist/workstation/keepers/kit/paths.js +2 -1
  98. package/dist/workstation/managedInstall.js +57 -0
  99. package/dist/workstation/protocol/agent.js +2 -15
  100. package/dist/workstation/protocol/management.js +93 -4
  101. package/dist/workstation/protocol/runtimeDiagnostics.js +15 -0
  102. package/dist/workstation/rd/artifact.js +119 -0
  103. package/dist/workstation/rd/control.js +119 -0
  104. package/dist/workstation/rd/display.js +29 -0
  105. package/dist/workstation/rd/provider.js +222 -0
  106. package/dist/workstation/rd/sessions.js +391 -0
  107. package/dist/workstation/rd/state.js +45 -0
  108. package/dist/workstation/rd/update.js +171 -0
  109. package/dist/workstation/rd/upkeep.js +38 -0
  110. package/dist/workstation/register.js +27 -37
  111. package/dist/workstation/service/installer.js +104 -13
  112. package/dist/workstation/service/launchd.js +1 -0
  113. package/dist/workstation/storageMetrics.js +118 -0
  114. package/dist/workstation/systemMetrics.js +44 -92
  115. package/dist/workstation/toolchain.js +94 -0
  116. package/dist/workstation/tools/browser.js +28 -4
  117. package/dist/workstation/update/artifact.js +2 -1
  118. package/dist/workstation/update/globalInstall.js +116 -0
  119. package/dist/workstation/update/npmInstaller.js +49 -23
  120. package/dist/workstation/update/rdRollback.js +145 -0
  121. package/dist/workstation/update/runUpdate.js +3 -2
  122. package/dist/workstation/update/watchdog.js +16790 -334
  123. package/package.json +5 -5
  124. package/skills/axom/SKILL.md +11 -11
  125. package/skills/axom-definition/SKILL.md +3 -3
  126. package/skills/axom-platform/SKILL.md +3 -3
  127. package/dist/axom-patch/cli/probes/qoder-steering.mjs +0 -89
  128. package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +0 -61
  129. package/dist/axom-patch/cli/qoder-steering.json +0 -52
  130. package/dist/axom-patch/cli/qoder-subagent-origin.json +0 -73
  131. package/dist/commands/guide.js +0 -38
  132. package/dist/runtime-deps/qoder-sdk/package-lock.json +0 -1189
  133. package/dist/runtime-deps/qoder-sdk/package.json +0 -8
  134. package/dist/workstation/acp/delegationMcp.js +0 -24
  135. package/dist/workstation/acp/processor/sideChannel.js +0 -80
  136. package/dist/workstation/acp/qoderStartup.js +0 -45
  137. package/dist/workstation/acp/test/raw/qoderCaptures.js +0 -125
  138. package/dist/workstation/qoderAdapter/backgroundProcess.js +0 -118
  139. package/dist/workstation/qoderAdapter/backgroundTerminal.js +0 -193
  140. package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +0 -372
  141. package/dist/workstation/qoderAdapter/callbackContext.js +0 -76
  142. package/dist/workstation/qoderAdapter/completedContinuation.js +0 -139
  143. package/dist/workstation/qoderAdapter/contract.js +0 -78
  144. package/dist/workstation/qoderAdapter/foregroundOutput.js +0 -36
  145. package/dist/workstation/qoderAdapter/history.js +0 -85
  146. package/dist/workstation/qoderAdapter/identityIndex.js +0 -191
  147. package/dist/workstation/qoderAdapter/inputQueue.js +0 -42
  148. package/dist/workstation/qoderAdapter/main.js +0 -95
  149. package/dist/workstation/qoderAdapter/mapper.js +0 -117
  150. package/dist/workstation/qoderAdapter/observerPlugin.js +0 -245
  151. package/dist/workstation/qoderAdapter/observerReceiver.js +0 -187
  152. package/dist/workstation/qoderAdapter/permissions.js +0 -61
  153. package/dist/workstation/qoderAdapter/profile.js +0 -93
  154. package/dist/workstation/qoderAdapter/sdkDriver.js +0 -48
  155. package/dist/workstation/qoderAdapter/sdkRuntime.js +0 -93
  156. package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +0 -145
  157. package/dist/workstation/qoderAdapter/session.js +0 -319
  158. package/dist/workstation/qoderAdapter/subagentMeta.js +0 -28
  159. package/dist/workstation/qoderAdapter/subwork.js +0 -85
  160. package/dist/workstation/qoderAdapter/taskControl.js +0 -38
  161. package/dist/workstation/qoderAdapter/taskLedger.js +0 -163
  162. package/dist/workstation/qoderAdapter/usage.js +0 -37
  163. /package/dist/workstation/{qoderAdapter/types.js → desktopControl/provider.js} +0 -0
@@ -21,7 +21,7 @@
21
21
  * window could ever open for them;
22
22
  * - **control frames** bypass both and are written at once: RPC replies (`agent_opened`, `agent_steered`,
23
23
  * `agent_config_option_set`, `agent_native_sessions`, `agent_native_deleted`, `agent_verified`, `agent_authenticated`,
24
- * `agent_installed`, `agent_delegation_renewed`, `agent_provider_task_stopped`, `agent_backfill_done`), levels
24
+ * `agent_installed`, `agent_provider_task_stopped`, `agent_backfill_done`), levels
25
25
  * (`agent_session_state`, `agent_runtime_profile`, `agent_provider_task_event`), `agent_session_lost`, `agent_resume_offer`,
26
26
  * `agent_auth_url`/`agent_auth_done`, and the channel's own heartbeat. They are small and rare, an answer the server is waiting
27
27
  * for (a 10–120 s timeout) must not queue behind a session's backlog, and an answer to a user (a permission card's state) must
@@ -1,21 +1,13 @@
1
1
  import path from 'node:path';
2
- import { createHash, randomUUID } from 'node:crypto';
3
- import { AGENT_WIRE_AUTH_URL_VERSION, AGENT_WIRE_HEARTBEAT_INVENTORY_VERSION, AGENT_WIRE_SCOPED_ORIGIN_VERSION, AGENT_WIRE_ORPHAN_OFFER_VERSION, AGENT_WIRE_SESSION_AUTH_METHODS_VERSION, AGENT_WIRE_SESSION_LOST_VERSION, AGENT_WIRE_VERSION, AGENT_WIRE_DELEGATION_MCP_VERSION, AGENT_WIRE_DELEGATION_RENEW_VERSION, AGENT_WIRE_PROVIDER_TASK_VERSION, AGENT_WIRE_REOPEN_REQUEST_VERSION, AGENT_WIRE_RELATION_ACK_VERSION, outboundAgentFrameRejection, outboundFailureReply, } from './protocol/agent.js';
2
+ import { AGENT_WIRE_AUTH_URL_VERSION, AGENT_WIRE_HEARTBEAT_INVENTORY_VERSION, AGENT_WIRE_SCOPED_ORIGIN_VERSION, AGENT_WIRE_ORPHAN_OFFER_VERSION, AGENT_WIRE_SESSION_AUTH_METHODS_VERSION, AGENT_WIRE_SESSION_LOST_VERSION, AGENT_WIRE_VERSION, AGENT_WIRE_PROVIDER_TASK_VERSION, AGENT_WIRE_REOPEN_REQUEST_VERSION, AGENT_WIRE_RELATION_ACK_VERSION, outboundAgentFrameRejection, outboundFailureReply, } from './protocol/agent.js';
4
3
  import { listLegacyJournalRefs, readLegacyJournal, removeLegacyJournal } from './journal.js';
5
4
  import { SESSION_WINDOW_BYTES, SESSION_WINDOW_EVENTS } from './acp/outbox.js';
6
5
  import { SocketGate } from './agentFlow.js';
7
6
  import { AGENT_PROBE_TIMEOUT_MS, AgentRefusedSessionError, LOCAL_SESSION_UNMOVABLE } from './acp/acpAgents.js';
8
- import { AdapterStillInstallingError } from './acp/builds/model.js';
9
7
  import { projectFrame } from './acp/processor/projection.js';
10
8
  import { reportsActivity, supportsAsyncTaskStop } from './acp/activityReporting.js';
11
9
  import { activityIsBusy } from './acp/activityBusy.js';
12
- import { buildSessionMcpServers } from './acp/delegationMcp.js';
13
10
  import { ADAPTER_LOST_REOPEN_ATTEMPTS, plainSwitchBlocker, REOPEN_REQUEST_RETRY_MS, switchBlockers, SWITCH_QUIET_MS } from './acp/switching.js';
14
- function delegationLeaseId(lease) {
15
- return createHash('sha256').update(JSON.stringify([
16
- lease.url, lease.headers, lease.bindingId,
17
- ])).digest('hex');
18
- }
19
11
  /**
20
12
  * 从线上收来的验证预算的绝对上限。
21
13
  *
@@ -129,10 +121,6 @@ function openSpecOf(frame) {
129
121
  openRequestId: frame.id,
130
122
  sessionRef: frame.sessionRef,
131
123
  agentKey: frame.agentKey,
132
- ...(frame.delegationMcp ? { delegation: {
133
- leaseId: delegationLeaseId(frame.delegationMcp),
134
- ...(frame.delegationMcp.bindingId ? { bindingId: frame.delegationMcp.bindingId, instanceId: randomUUID() } : {}),
135
- } } : {}),
136
124
  // `frame.bridge` is the registry agent id now — the wire field keeps its name because Plan A
137
125
  // renames nothing (Global Constraint 3); ruling R2 replaced what it *means*.
138
126
  agentId: frame.bridge,
@@ -143,7 +131,6 @@ function openSpecOf(frame) {
143
131
  args: frame.args ?? [],
144
132
  ...(frame.nativeSessionId ? { nativeSessionId: frame.nativeSessionId } : {}),
145
133
  ...(frame.requireSessionLoad ? { requireSessionLoad: true } : {}),
146
- ...(frame.delegationMcp ? { delegationMcp: frame.delegationMcp } : {}),
147
134
  // Retaining a whole adopted history costs memory for the session's lifetime, so it is only
148
135
  // kept when this open is the one that will stream it back.
149
136
  backfill: frame.backfill === true,
@@ -163,13 +150,6 @@ function exitWords(exit) {
163
150
  function errorMessage(err) {
164
151
  return err instanceof Error ? err.message : String(err);
165
152
  }
166
- /**
167
- * The reply text of an open that failed. A managed (delegation) open hides the cause behind one fixed sentence — except when
168
- * the adapter is still being installed: that one is actionable ("try again shortly") and carries nothing about the lease (15c).
169
- */
170
- function openFailureText(frame, err) {
171
- return frame.delegationMcp && !(err instanceof AdapterStillInstallingError) ? 'Managed delegation session could not open' : errorMessage(err);
172
- }
173
153
  /**
174
154
  * Batch size for `agent_backfill_events`. Kept modest so a session with a long history does not
175
155
  * arrive as one giant frame; the last batch of a stream is whatever remains below this size.
@@ -251,6 +231,8 @@ export class AgentSessionManager {
251
231
  journalDir;
252
232
  tasks;
253
233
  sessions = new Map();
234
+ /** A failed recorded-build respawn keeps this binding even after no LiveSession remains. */
235
+ retainedRespawnBuilds = new Map();
254
236
  /** In-flight opens, keyed by sessionRef, so two racing agent_open frames cannot spawn two agents. */
255
237
  opening = new Map();
256
238
  /**
@@ -809,38 +791,6 @@ export class AgentSessionManager {
809
791
  for (let parked = frameRef !== undefined ? this.switching.get(frameRef) : undefined; parked; parked = this.switching.get(frameRef))
810
792
  await parked;
811
793
  switch (frame.t) {
812
- case 'agent_delegation_renew': {
813
- if (this.serverAgentWire < AGENT_WIRE_DELEGATION_RENEW_VERSION)
814
- return;
815
- const live = this.liveSession(frame.sessionRef);
816
- const now = Date.now();
817
- // Delegation's non-secret fields are processor state (Task 19): the checks read it, an accepted renewal is a note.
818
- const delegation = live?.session.delegation ?? undefined;
819
- if (!live || !delegation || live.closed || !live.session.liveness.agentAlive || live.session.liveness.turnRunning ||
820
- activityIsBusy(live.session.activityRaw) || delegation.refreshPending ||
821
- live.nativeSessionId !== frame.nativeSessionId || delegation.instanceId !== frame.instanceId ||
822
- delegation.bindingId !== frame.bindingId || !frame.bindingId ||
823
- !Number.isSafeInteger(frame.renewalRevision) || frame.renewalRevision < 1 ||
824
- !Number.isSafeInteger(frame.expiresAtEpochMs) || frame.expiresAtEpochMs <= now ||
825
- frame.expiresAtEpochMs > now + 330_000 || frame.renewalRevision < (delegation.renewalRevision ?? 0) ||
826
- frame.expiresAtEpochMs < delegation.expiresAtEpochMs ||
827
- (frame.renewalRevision === delegation.renewalRevision && frame.expiresAtEpochMs !== delegation.expiresAtEpochMs)) {
828
- this.emit({ t: 'agent_delegation_renewed', id: frame.id, ok: false, error: 'The original managed instance cannot renew' });
829
- return;
830
- }
831
- try {
832
- await live.session.note({ type: 'agent_delegation_renewed', expiresAtEpochMs: frame.expiresAtEpochMs, renewalRevision: frame.renewalRevision });
833
- }
834
- catch (err) {
835
- this.emit({ t: 'agent_delegation_renewed', id: frame.id, ok: false, error: errorMessage(err) });
836
- return;
837
- }
838
- const renewed = live.session.delegation ?? delegation;
839
- this.emit({ t: 'agent_delegation_renewed', id: frame.id, ok: true,
840
- nativeSessionId: live.nativeSessionId, instanceId: renewed.instanceId, bindingId: renewed.bindingId,
841
- renewalRevision: renewed.renewalRevision, expiresAtEpochMs: renewed.expiresAtEpochMs });
842
- return;
843
- }
844
794
  case 'agent_open': {
845
795
  const admission = this.reserveOpen(frame.id, frame.sessionRef);
846
796
  if (admission === 'rejected')
@@ -1490,20 +1440,6 @@ export class AgentSessionManager {
1490
1440
  return idle.map((live) => live.sessionRef);
1491
1441
  }
1492
1442
  async onOpen(frame) {
1493
- if (frame.delegationMcp && (this.serverAgentWire < AGENT_WIRE_DELEGATION_MCP_VERSION ||
1494
- (frame.delegationMcp.bindingId && this.serverAgentWire < AGENT_WIRE_DELEGATION_RENEW_VERSION))) {
1495
- this.emit({ t: 'agent_opened', id: frame.id, ok: false, error: 'Managed delegation requires a newer server' });
1496
- return;
1497
- }
1498
- if (frame.delegationMcp) {
1499
- try {
1500
- buildSessionMcpServers([], frame.delegationMcp);
1501
- }
1502
- catch {
1503
- this.emit({ t: 'agent_opened', id: frame.id, ok: false, error: 'Delegation MCP configuration is unavailable' });
1504
- return;
1505
- }
1506
- }
1507
1443
  // Rotation is server-driven. A busy reconnect resends the original process's outbox.
1508
1444
  let existing = this.liveSession(frame.sessionRef);
1509
1445
  // An open that would spawn the ref's adapter anew after it kept exiting on its own waits out the backoff (no crash loop).
@@ -1532,69 +1468,33 @@ export class AgentSessionManager {
1532
1468
  retireForRefresh = existing;
1533
1469
  existing = undefined;
1534
1470
  }
1471
+ // A keeper session's label records the build that created its native session. Preserve that
1472
+ // binding across an adapter exit: the agent's current pointer may have advanced meanwhile,
1473
+ // but a cached reconnect must retry the recorded package before any explicit switch.
1474
+ const recordedBuildId = retireForRefresh
1475
+ ? this.agents.placementOf?.(retireForRefresh.session)?.buildId
1476
+ : this.retainedRespawnBuilds.get(frame.sessionRef);
1535
1477
  if (existing) {
1536
- const delegation = existing.session.delegation ?? undefined;
1537
- if (delegation && !frame.delegationMcp) {
1538
- this.emit({ t: 'agent_opened', id: frame.id, ok: false, error: 'Managed delegation lease is required' });
1478
+ // Spec §6.3: a session with a switch pending (a persistent intent: another adapter build, or off an older keeper-acp
1479
+ // generation) moves at this open — the only moment its spawn secrets are at hand — when it is strictly idle.
1480
+ if (await this.switchAtOpen(existing, frame))
1539
1481
  return;
1540
- }
1541
- if (frame.delegationMcp && frame.nativeSessionId && frame.nativeSessionId !== existing.nativeSessionId) {
1542
- this.emit({ t: 'agent_opened', id: frame.id, ok: false, error: 'Managed delegation cannot reload the original session' });
1543
- return;
1544
- }
1545
- const refresh = !!frame.delegationMcp &&
1546
- (delegation?.leaseId !== delegationLeaseId(frame.delegationMcp) || delegation?.refreshPending === true);
1547
- if (refresh) {
1548
- if (!existing.nativeSessionId || (frame.nativeSessionId && frame.nativeSessionId !== existing.nativeSessionId)) {
1549
- this.emit({ t: 'agent_opened', id: frame.id, ok: false, error: 'Managed delegation cannot reload the original session' });
1550
- return;
1551
- }
1552
- if (existing.session.liveness.turnRunning || activityIsBusy(existing.session.activityRaw)) {
1553
- if (!delegation?.refreshPending) {
1554
- try {
1555
- await existing.session.note({ type: 'agent_delegation_refresh_pending' });
1556
- }
1557
- catch (err) {
1558
- this.emit({ t: 'agent_opened', id: frame.id, ok: false, error: errorMessage(err) });
1559
- return;
1560
- }
1561
- }
1562
- if (!await this.resumeAndReply(existing, frame, false))
1563
- return;
1564
- this.replyNoReplayNeeded(frame);
1565
- return;
1566
- }
1567
- openFrame = { ...frame, nativeSessionId: existing.nativeSessionId, requireSessionLoad: true };
1568
- retireForRefresh = existing;
1569
- }
1570
- else {
1571
- // Spec §6.3: a session with a switch pending (a persistent intent: another adapter build, or off an older keeper-acp
1572
- // generation) moves at this open — the only moment its spawn secrets are at hand — when it is strictly idle.
1573
- if (await this.switchAtOpen(existing, frame))
1574
- return;
1575
- // Reusing a live session: neither the process nor the native session moved.
1576
- if (!await this.resumeAndReply(existing, frame, false))
1577
- return;
1578
- // The session already exists, so any history it has was already streamed under whichever
1579
- // open first established it (or never will be, if that open didn't ask for it) — replaying it
1580
- // again here would duplicate what the platform already holds. But if THIS frame asked for
1581
- // backfill, the server is waiting on exactly one `agent_backfill_done` for THIS open's id:
1582
- // silence is not "no", it is a wait that never resolves. Answer it without replaying.
1583
- this.replyNoReplayNeeded(frame);
1482
+ // Reusing a live session: neither the process nor the native session moved.
1483
+ if (!await this.resumeAndReply(existing, frame, false))
1584
1484
  return;
1585
- }
1485
+ // The session already exists, so any history it has was already streamed under whichever
1486
+ // open first established it (or never will be, if that open didn't ask for it) — replaying it
1487
+ // again here would duplicate what the platform already holds. But if THIS frame asked for
1488
+ // backfill, the server is waiting on exactly one `agent_backfill_done` for THIS open's id:
1489
+ // silence is not "no", it is a wait that never resolves. Answer it without replaying.
1490
+ this.replyNoReplayNeeded(frame);
1491
+ return;
1586
1492
  }
1587
1493
  const inFlight = this.opening.get(frame.sessionRef);
1588
1494
  if (inFlight) {
1589
1495
  // Piggyback on the attempt already running, and report its real failure — not a generic one.
1590
1496
  const { live, error, agentRefused } = await inFlight;
1591
1497
  if (live) {
1592
- const delegation = live.session.delegation ?? undefined;
1593
- if (!!frame.delegationMcp !== !!delegation ||
1594
- (frame.delegationMcp && delegation?.leaseId !== delegationLeaseId(frame.delegationMcp))) {
1595
- this.emit({ t: 'agent_opened', id: frame.id, ok: false, error: 'Managed delegation lease does not match the live session' });
1596
- return;
1597
- }
1598
1498
  // Piggybacking on the attempt already running: a non-null `live` means that attempt was a
1599
1499
  // real open (`session/load` or `session/new`), which this frame is reporting secondhand.
1600
1500
  if (!await this.resumeAndReply(live, frame, true))
@@ -1623,9 +1523,9 @@ export class AgentSessionManager {
1623
1523
  const attempt = (async () => {
1624
1524
  if (retireForRefresh)
1625
1525
  await this.retire(retireForRefresh, 'keep');
1626
- return this.openSession(openFrame);
1526
+ return this.openSession(openFrame, recordedBuildId);
1627
1527
  })()
1628
- .catch((err) => ({ live: null, error: openFailureText(openFrame, err) }))
1528
+ .catch((err) => ({ live: null, error: errorMessage(err) }))
1629
1529
  .finally(() => releaseHold?.());
1630
1530
  this.opening.set(frame.sessionRef, attempt);
1631
1531
  let outcome;
@@ -1648,6 +1548,7 @@ export class AgentSessionManager {
1648
1548
  // tombstone without anything ever closing it. Nothing but microtasks separates `attempt`
1649
1549
  // resolving from the finally, and microtasks always drain before the next inbound frame.
1650
1550
  if (outcome.live) {
1551
+ this.retainedRespawnBuilds.delete(frame.sessionRef);
1651
1552
  // openSession returned successfully, having just done session/load or session/new.
1652
1553
  if (!await this.resumeAndReply(outcome.live, frame, true))
1653
1554
  return;
@@ -1658,6 +1559,8 @@ export class AgentSessionManager {
1658
1559
  await this.streamBackfill(frame.id, outcome.live.session);
1659
1560
  }
1660
1561
  else {
1562
+ if (recordedBuildId !== undefined)
1563
+ this.retainedRespawnBuilds.set(frame.sessionRef, recordedBuildId);
1661
1564
  this.emit({
1662
1565
  t: 'agent_opened', id: frame.id, ok: false, error: outcome.error,
1663
1566
  // Only when the agent is the one that said no, and then it is the one refusal that can be
@@ -1689,7 +1592,7 @@ export class AgentSessionManager {
1689
1592
  */
1690
1593
  async switchAtOpen(live, frame) {
1691
1594
  const agents = this.agents;
1692
- if (!agents.switchSession || !agents.pendingSwitchFor || frame.delegationMcp)
1595
+ if (!agents.switchSession || !agents.pendingSwitchFor)
1693
1596
  return false;
1694
1597
  // A switch spawns an adapter: new work, refused while the workstation drains (spec §8.2).
1695
1598
  if (this.tasks && !this.tasks.accepting)
@@ -1847,7 +1750,7 @@ export class AgentSessionManager {
1847
1750
  * "Move idle sessions" (Task 25's `agent_switch_idle`, spec §6.4): every held session of `agentId` that runs another build than
1848
1751
  * `toBuild` (else the agent's current build) gets a persistent intent to move there; those already strictly idle get their
1849
1752
  * `agent_reopen_request` at once, the others when they become idle. A session that can never be moved this way
1850
- * ({@link unmovableWhy}: a local-fallback session, a managed delegation, no native session, a target that already refused it) gets
1753
+ * ({@link unmovableWhy}: a local-fallback session, no native session, a target that already refused it) gets
1851
1754
  * no intent — it would only be asked to reopen again and again — and is listed in `unmovable` with the reason. A `toBuild` that is
1852
1755
  * not installed for the agent is refused (throws). Returns the refs registered, the ones a request went out for, and the unmovable.
1853
1756
  */
@@ -1889,7 +1792,7 @@ export class AgentSessionManager {
1889
1792
  }
1890
1793
  /**
1891
1794
  * Why `live` can never be moved to `target` by a switch (spec §6.3 "当前限制"), or `undefined`: a session keeper-acp does not host
1892
- * (the local fallback), a managed delegation, a session with no native session to load, or one a switch to the same build already
1795
+ * (the local fallback), a session with no native session to load, or one a switch to the same build already
1893
1796
  * found unmovable. `plain` is what the scan card and the result frame say (Task 25); `detail` is the switch's own words, for the
1894
1797
  * log, when there are any.
1895
1798
  */
@@ -1897,8 +1800,6 @@ export class AgentSessionManager {
1897
1800
  const placement = this.agents.placementOf?.call(this.agents, live.session);
1898
1801
  if (!placement?.keeper)
1899
1802
  return { plain: LOCAL_SESSION_UNMOVABLE };
1900
- if (live.session.delegation)
1901
- return { plain: 'it is a delegated session, which can\'t be moved to another build yet' };
1902
1803
  if (!live.nativeSessionId)
1903
1804
  return { plain: 'it has no saved conversation the new build could reload' };
1904
1805
  const refused = this.agents.unmovableFor?.call(this.agents, live.sessionRef);
@@ -2128,7 +2029,6 @@ export class AgentSessionManager {
2128
2029
  this.emit({ t: 'agent_opened', id: frame.id, ok: false, error: 'session was closed while opening' });
2129
2030
  return false;
2130
2031
  }
2131
- const delegation = live.session.delegation ?? undefined;
2132
2032
  const activityRaw = live.session.activityRaw;
2133
2033
  this.emit({
2134
2034
  t: 'agent_opened',
@@ -2140,13 +2040,6 @@ export class AgentSessionManager {
2140
2040
  // Emitted only when true, the same `supportsSteering` convention as below: absent is the
2141
2041
  // field's only "no", and it is also what an older peer sends.
2142
2042
  ...(reloaded ? { reloaded: true } : {}),
2143
- // Delegation's non-secret fields, from processor state (Task 19: `agent_opened` is assembled from state).
2144
- ...(delegation ? { delegationExpiresAtEpochMs: delegation.expiresAtEpochMs } : {}),
2145
- ...(delegation?.refreshPending ? { delegationRefreshPending: true } : {}),
2146
- ...(this.serverAgentWire >= AGENT_WIRE_DELEGATION_RENEW_VERSION && delegation?.instanceId ? {
2147
- delegationInstanceId: delegation.instanceId, delegationBindingId: delegation.bindingId,
2148
- delegationRenewalRevision: delegation.renewalRevision,
2149
- } : {}),
2150
2043
  // The server learns this here and nowhere else. It used to be published only on the roster in
2151
2044
  // the `agent_hello` handshake, which is a snapshot taken *before* any agent was connected to —
2152
2045
  // and the capability can only be observed on a live `initialize`, whose record
@@ -2223,7 +2116,7 @@ export class AgentSessionManager {
2223
2116
  });
2224
2117
  return true;
2225
2118
  }
2226
- async openSession(frame) {
2119
+ async openSession(frame, recordedBuildId) {
2227
2120
  // A previous incarnation of this ref may still be closing: its adapter stopped and its transport session (raw, outbox) kept or
2228
2121
  // released — the new spawn goes under the same key (see {@link retiring}). This is the first statement on purpose — before the
2229
2122
  // agent spawns, before anything touches the transport. The one-time legacy journal migration lands before any open too.
@@ -2272,13 +2165,14 @@ export class AgentSessionManager {
2272
2165
  });
2273
2166
  };
2274
2167
  try {
2275
- const result = await this.agents.open({ ...openSpecOf(frame), onInstallOutcome, seqBase: () => resumeFrom });
2168
+ const result = await this.agents.open({
2169
+ ...openSpecOf(frame),
2170
+ ...(recordedBuildId !== undefined ? { recordedBuildId } : {}),
2171
+ onInstallOutcome,
2172
+ seqBase: () => resumeFrom,
2173
+ });
2276
2174
  session = result.session;
2277
2175
  nativeSessionId = result.nativeSessionId;
2278
- if (frame.delegationMcp && !nativeSessionId) {
2279
- await session.close();
2280
- return { live: null, error: 'Managed delegation requires a native session ID' };
2281
- }
2282
2176
  }
2283
2177
  catch (err) {
2284
2178
  await reportFailedInstall();
@@ -2288,7 +2182,7 @@ export class AgentSessionManager {
2288
2182
  // on `session/new` arrives as; see {@link OpenOutcome.agentRefused}.
2289
2183
  return {
2290
2184
  live: null,
2291
- error: openFailureText(frame, err),
2185
+ error: errorMessage(err),
2292
2186
  // The code comes off the marker, not off `errorMessage(err)` above: the wrapper captured it
2293
2187
  // from the original rejection at the moment that object still existed.
2294
2188
  ...(err instanceof AgentRefusedSessionError
@@ -2843,6 +2737,7 @@ export class AgentSessionManager {
2843
2737
  void this.agents.cancelSwitch?.({ sessionRef }).catch(() => false);
2844
2738
  this.reopenRequests.delete(sessionRef);
2845
2739
  this.adapterCrashes.delete(sessionRef);
2740
+ this.retainedRespawnBuilds.delete(sessionRef);
2846
2741
  // Closing a ref that is still spawning: nothing to close yet, so tombstone it — openSession
2847
2742
  // closes the session (and destroys its journal) the moment the bridge hands it over.
2848
2743
  //
@@ -1,16 +1,22 @@
1
1
  import { deriveSessionKey, randomBytes } from '../crypto.js';
2
2
  import { connectEncryptedChannel } from '../connection/encryptedChannel.js';
3
3
  import { CLIENT_PACKAGE_NAME, CLIENT_VERSION } from '../version.js';
4
- import { decodeManagementFrame, encodeManagementFrame, MANAGEMENT_WIRE_DRAINING_VERSION, MANAGEMENT_WIRE_GPU_VERSION, MANAGEMENT_WIRE_KEEPERS_VERSION, MANAGEMENT_WIRE_LOAD_AVERAGES_VERSION, MANAGEMENT_WIRE_METRICS_VERSION, MANAGEMENT_WIRE_MIN_VERSION, MANAGEMENT_WIRE_PRESSURE_VERSION, MANAGEMENT_WIRE_VERSION, } from '../protocol/management.js';
4
+ import { decodeManagementFrame, encodeManagementFrame, MANAGEMENT_WIRE_DRAINING_VERSION, MANAGEMENT_WIRE_GPU_VERSION, MANAGEMENT_WIRE_KEEPERS_VERSION, MANAGEMENT_WIRE_LOAD_AVERAGES_VERSION, MANAGEMENT_WIRE_METRICS_VERSION, MANAGEMENT_WIRE_MIN_VERSION, MANAGEMENT_WIRE_PRESSURE_VERSION, MANAGEMENT_WIRE_VERSION, ManagementFrameAdmission, } from '../protocol/management.js';
5
5
  import { bridgesForWire, helloRoster } from '../protocol/common.js';
6
6
  import { AGENT_WIRE_MIN_VERSION, AGENT_WIRE_VERSION } from '../protocol/agent.js';
7
7
  import { osInfo } from '../osinfo.js';
8
8
  import { workstationChannelUrl } from './url.js';
9
+ import { DesktopCapabilityReporter } from '../desktopControl/capabilities.js';
9
10
  const protocols = {
10
11
  MANAGEMENT: { min: MANAGEMENT_WIRE_MIN_VERSION, max: MANAGEMENT_WIRE_VERSION },
11
12
  // COMPUTER v5: durable exec frames (exec_started/attach/status/ack, offsets on exec_output; spec §4.3).
12
- COMPUTER: { min: 1, max: 5 }, AGENT: { min: AGENT_WIRE_MIN_VERSION, max: AGENT_WIRE_VERSION },
13
+ // COMPUTER v6: this client refuses credential storage to the agent file tools (files/credentials.ts); the server sends
14
+ // file frames only to v6 clients.
15
+ COMPUTER: { min: 1, max: 6 }, AGENT: { min: AGENT_WIRE_MIN_VERSION, max: AGENT_WIRE_VERSION },
13
16
  };
17
+ function toolchainField(toolchain) {
18
+ return toolchain ? { toolchain: { python: toolchain.python, node: toolchain.node } } : {};
19
+ }
14
20
  /** Exported so tests can assert the wire frame's `clientPackage` without opening a socket. */
15
21
  export function buildHelloFrame(options, clientNonce) {
16
22
  return {
@@ -177,9 +183,28 @@ export class ManagementChannel {
177
183
  serverManagementWire;
178
184
  /** The open channel of the current connection, for frames this client sends unprompted ({@link pushRoster}). */
179
185
  channel;
186
+ desktopReporter;
187
+ connectionAttempt = 0;
188
+ generation;
180
189
  constructor(options) {
181
190
  this.options = options;
182
191
  }
192
+ refreshDesktopCapabilities(generation) {
193
+ if (generation === undefined || generation === this.generation)
194
+ this.desktopReporter?.refresh();
195
+ }
196
+ /** T10 reports only on its authenticated current management socket. */
197
+ sendDesktopState(generation, frame) {
198
+ if (generation !== this.generation || (this.serverManagementWire ?? 0) < 9 || this.channel?.state !== 'OPEN')
199
+ return false;
200
+ try {
201
+ this.channel.send(frame);
202
+ return true;
203
+ }
204
+ catch {
205
+ return false;
206
+ }
207
+ }
183
208
  /**
184
209
  * Tell the server about a roster that changed without a scan asking (adapter builds moved by Axom,
185
210
  * spec §6.1): the same `external_agents_scan_result` frame a scan answers with, under a client-made
@@ -199,14 +224,22 @@ export class ManagementChannel {
199
224
  }
200
225
  }
201
226
  async connect(signal) {
227
+ const attempt = ++this.connectionAttempt;
228
+ this.desktopReporter?.dispose();
229
+ this.desktopReporter = undefined;
230
+ this.generation = undefined;
231
+ this.channel = undefined;
202
232
  this.serverManagementWire = undefined;
203
233
  const nonce = randomBytes(16);
204
234
  let initialHeartbeat = true;
235
+ const admission = new ManagementFrameAdmission(() => attempt === this.connectionAttempt && !signal?.aborted);
205
236
  const channel = await connectEncryptedChannel({
206
237
  url: workstationChannelUrl(this.options.server, 'management', this.options.allowInsecureWs),
207
238
  signal,
208
239
  encode: encodeManagementFrame,
209
240
  decode: decodeManagementFrame,
241
+ decodeBytes: decodeManagementFrame,
242
+ admitFrame: frame => admission.admit(frame),
210
243
  heartbeat: (seq, sentAtEpochMs) => this.heartbeat(seq, sentAtEpochMs),
211
244
  initialFrame: () => {
212
245
  initialHeartbeat = false;
@@ -226,15 +259,40 @@ export class ManagementChannel {
226
259
  if (initialHeartbeat)
227
260
  throw new Error('Management proof heartbeat was not sent');
228
261
  const ready = await this.nextReady(channel);
262
+ if (attempt !== this.connectionAttempt || channel.state !== 'OPEN') {
263
+ channel.close();
264
+ throw new Error('Management connection superseded');
265
+ }
229
266
  this.serverManagementWire = ready.protocols.MANAGEMENT;
267
+ this.generation = ready.generation;
268
+ this.channel = channel;
269
+ const reporter = new DesktopCapabilityReporter({ selectedWire: this.serverManagementWire, generation: ready.generation,
270
+ provider: this.options.desktopProvider, send: report => {
271
+ if (attempt === this.connectionAttempt && this.channel === channel && channel.state === 'OPEN')
272
+ channel.send({ t: 'workstation_capabilities', ...report });
273
+ } });
274
+ this.desktopReporter = reporter;
275
+ // Starts before the existing awaited update hook; heartbeat and reporting never await discovery.
276
+ reporter.refresh();
277
+ const retire = () => {
278
+ admission.dispose();
279
+ reporter.dispose();
280
+ try {
281
+ this.options.onClosed?.(ready.generation);
282
+ }
283
+ catch { /* transport retirement must complete */ }
284
+ if (this.channel === channel) {
285
+ this.channel = undefined;
286
+ this.generation = undefined;
287
+ this.desktopReporter = undefined;
288
+ this.serverManagementWire = undefined;
289
+ }
290
+ };
291
+ void channel.closed.then(retire, retire);
230
292
  // A readiness record is evidence from this encrypted, proof-of-key management generation — it
231
293
  // is never driven by the local browser/device-authorization flow.
232
- await this.options.onReady?.({ generation: ready.generation, release: ready.release });
294
+ await this.options.onReady?.({ generation: ready.generation, selectedWire: ready.protocols.MANAGEMENT, release: ready.release });
233
295
  const pending = new Map();
234
- this.channel = channel;
235
- void channel.closed.then(() => { if (this.channel === channel)
236
- this.channel = undefined; }, () => { if (this.channel === channel)
237
- this.channel = undefined; });
238
296
  void this.routeMessages(channel, pending);
239
297
  return {
240
298
  generation: ready.generation,
@@ -255,6 +313,11 @@ export class ManagementChannel {
255
313
  };
256
314
  }
257
315
  async heartbeat(seq, sentAtEpochMs) {
316
+ const selectedWire = this.serverManagementWire;
317
+ // Availability may change without restarting the CLI (Desktop sharing is process-owned).
318
+ // Discovery stays bounded and never delays the heartbeat or creates a capture session.
319
+ this.desktopReporter?.refresh();
320
+ const revision = this.desktopReporter?.revision;
258
321
  const snapshot = await this.options.snapshot();
259
322
  return {
260
323
  t: 'heartbeat', seq, sentAtEpochMs, clientVersion: CLIENT_VERSION,
@@ -266,11 +329,13 @@ export class ManagementChannel {
266
329
  },
267
330
  activeShellTasks: snapshot.activeShellTasks, activeAgentTasks: snapshot.activeAgentTasks,
268
331
  protocols, os: osInfo(),
269
- ...(this.options.metrics && (this.serverManagementWire ?? 0) >= MANAGEMENT_WIRE_METRICS_VERSION
270
- ? { metrics: metricsForManagementWire(this.options.metrics(), this.serverManagementWire) }
332
+ ...toolchainField(this.options.toolchain?.() ?? null),
333
+ ...(this.options.metrics && (selectedWire ?? 0) >= MANAGEMENT_WIRE_METRICS_VERSION
334
+ ? { metrics: metricsForManagementWire(this.options.metrics(), selectedWire) }
271
335
  : {}),
272
- ...(snapshot.update ? { update: statusForManagementWire(snapshot.update, this.serverManagementWire) } : {}),
273
- ...await keepersForManagementWire(this.options.keepers, this.serverManagementWire, this.options.keepersCapMs),
336
+ ...(snapshot.update ? { update: statusForManagementWire(snapshot.update, selectedWire) } : {}),
337
+ ...await keepersForManagementWire(this.options.keepers, selectedWire, this.options.keepersCapMs),
338
+ ...((selectedWire ?? 0) >= 9 && revision !== undefined ? { capabilitiesRevision: revision } : {}),
274
339
  };
275
340
  }
276
341
  async nextReady(channel) {
@@ -282,6 +347,25 @@ export class ManagementChannel {
282
347
  async routeMessages(channel, pending) {
283
348
  try {
284
349
  for await (const frame of channel.messages) {
350
+ if (channel !== this.channel)
351
+ break;
352
+ if (frame.t === 'workstation_capabilities_request') {
353
+ if ((this.serverManagementWire ?? 0) >= 9 && frame.generation === this.generation)
354
+ this.desktopReporter?.refresh();
355
+ continue;
356
+ }
357
+ if ((frame.t === 'desktop_background' || frame.t === 'desktop_lobby' || frame.t === 'desktop_prepare' || frame.t === 'desktop_cancel') && (this.serverManagementWire ?? 0) >= 9) {
358
+ if (frame.t !== 'desktop_cancel' && frame.deviceGeneration !== this.generation)
359
+ continue;
360
+ if (this.options.onDesktopCommand && this.generation) {
361
+ // Capture the authenticated generation now: later callbacks cannot borrow a reconnect.
362
+ try {
363
+ void this.options.onDesktopCommand(frame, this.generation).catch(() => undefined);
364
+ }
365
+ catch { /* bounded desktop admission failure */ }
366
+ }
367
+ continue;
368
+ }
285
369
  if (frame.t === 'channel_ticket') {
286
370
  const queue = pending.get(frame.channel);
287
371
  queue?.shift()?.resolve(frame.ticket);
@@ -11,7 +11,7 @@ function ipv4Octets(hostname) {
11
11
  return octets.every((octet) => octet <= 255) ? octets : null;
12
12
  }
13
13
  /** Deliberately classifies literal names/IPs only: DNS answers must never downgrade transport security. */
14
- function isTrustedPlaintextHost(rawHostname) {
14
+ export function isTrustedPlaintextHost(rawHostname) {
15
15
  const hostname = rawHostname.toLowerCase().replace(/^\[|\]$/g, '');
16
16
  if (TRUSTED_PLAINTEXT_NAMES.has(hostname) || hostname.endsWith('.localhost'))
17
17
  return true;
@@ -1,20 +1,28 @@
1
+ import { loadManagedCliInstallSync, npmExecutionContext } from './managedInstall.js';
1
2
  import { resolveBrowserBinding } from './desktop/binding.js';
2
3
  import { promises as fs } from 'node:fs';
3
4
  import os from 'node:os';
4
5
  import path from 'node:path';
5
6
  import { fileURLToPath } from 'node:url';
6
7
  import yaml from 'js-yaml';
8
+ import { z } from 'zod';
9
+ const localPath = z.string().refine(p => path.isAbsolute(p) && !p.includes('\0'));
10
+ const nativeConfig = z.strictObject({ kind: z.literal('linux-native'), helperPath: localPath, runtimeDir: localPath });
7
11
  export function updateServiceContext(configDir = clientDir(), overrides = {}) {
8
12
  // config.ts lives in dist/workstation/ after packaging, so two levels up is the installed package root.
9
13
  const packageRoot = overrides.packageRoot ?? path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
10
- const nodePath = overrides.nodePath ?? process.execPath;
14
+ const managed = loadManagedCliInstallSync(packageRoot);
15
+ const nodePath = managed?.nodePath ?? overrides.nodePath ?? process.execPath;
16
+ const npmPrefix = managed?.prefix ?? overrides.npmPrefix ?? process.env.AXOM_WORKSTATION_NPM_PREFIX ?? path.resolve(packageRoot, '../../../..');
11
17
  return {
12
18
  configDir,
19
+ ...(managed ? { managed } : {}),
20
+ env: npmExecutionContext({ prefix: npmPrefix, nodePath }, process.env),
13
21
  platform: overrides.platform ?? (process.platform === 'darwin' ? 'darwin' : 'linux'),
14
22
  nodePath,
15
- npmPath: overrides.npmPath ?? process.env.AXOM_WORKSTATION_NPM_PATH ?? path.join(path.dirname(nodePath), 'npm'),
23
+ npmPath: managed?.npmPath ?? overrides.npmPath ?? process.env.AXOM_WORKSTATION_NPM_PATH ?? path.join(path.dirname(nodePath), 'npm'),
16
24
  // packageRoot is under lib/node_modules/@platless; climb to npm's global prefix.
17
- npmPrefix: overrides.npmPrefix ?? process.env.AXOM_WORKSTATION_NPM_PREFIX ?? path.resolve(packageRoot, '../../../..'),
25
+ npmPrefix,
18
26
  packageRoot,
19
27
  };
20
28
  }
@@ -35,7 +43,11 @@ export async function loadClientConfig(dir) {
35
43
  throw new Error('Invalid workstation config: server and workstationId are required; run axom auth again');
36
44
  }
37
45
  const browserBinding = parsed.browserBinding ? resolveBrowserBinding(parsed.browserBinding, {}) : undefined;
38
- return { ...(browserBinding ? { browserBinding } : {}), server: parsed.server, workstationId: parsed.workstationId, ...(Array.isArray(parsed.workspaces) ? { workspaces: parsed.workspaces } : {}) };
46
+ const desktopProvider = parsed.desktopProvider ? nativeConfig.parse({ ...parsed.desktopProvider,
47
+ helperPath: parsed.desktopProvider.helperPath === '/usr/local/bin/axom-desktop-helper' ? '/usr/local/bin/axom-desktop-keeper' : parsed.desktopProvider.helperPath,
48
+ }) : process.env.AXOM_DESKTOP_NATIVE_PROVIDER === '1' ?
49
+ nativeConfig.parse({ kind: 'linux-native', helperPath: '/usr/local/bin/axom-desktop-keeper', runtimeDir: '/run/axom-desktop' }) : undefined;
50
+ return { ...(browserBinding ? { browserBinding } : {}), ...(desktopProvider ? { desktopProvider } : {}), server: parsed.server, workstationId: parsed.workstationId, ...(typeof parsed.ownerUserId === 'string' && parsed.ownerUserId ? { ownerUserId: parsed.ownerUserId } : {}), ...(Array.isArray(parsed.workspaces) ? { workspaces: parsed.workspaces } : {}) };
39
51
  }
40
52
  export async function writeKeys(dir, clientKeyB64, serverPubB64) {
41
53
  await fs.mkdir(dir, { recursive: true });
@@ -279,7 +279,10 @@ export function connectEncryptedChannel(options) {
279
279
  if (!isBinary)
280
280
  return;
281
281
  try {
282
- const frame = options.decode(crypto.open(sessionKey, bytes).toString());
282
+ const plaintext = crypto.open(sessionKey, bytes);
283
+ const frame = options.decodeBytes ? options.decodeBytes(plaintext) : options.decode(plaintext.toString());
284
+ if (options.admitFrame && !options.admitFrame(frame))
285
+ return;
283
286
  const acknowledged = [...outstandingAcks].some((sequence) => options.isHeartbeatAck(frame, sequence));
284
287
  // A correctly encrypted and decoded frame proves the peer is still reachable. Heartbeat
285
288
  // acknowledgements are one such frame, but computer streaming/agent events may legitimately