@platless/axom-cli 0.5.9 → 0.6.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -5
- package/dist/axom-patch/cli/README.md +100 -12
- package/dist/axom-patch/cli/account-limits/generate.py +15 -12
- package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +20 -3
- package/dist/axom-patch/cli/claude-account-limits.json +9 -9
- package/dist/axom-patch/cli/codex-account-limits.json +11 -11
- package/dist/axom-patch/cli/codex-terminal-output.json +16 -0
- package/dist/axom-patch/cli/commandcode-account-limits.json +9 -9
- package/dist/axom-patch/cli/kimi-quota.mjs +12 -0
- package/dist/axom-patch/cli/kimi-subagents.mjs +173 -0
- package/dist/axom-patch/cli/nightly-task.md +55 -13
- package/dist/axom-patch/cli/probes/codex-terminal-output.mjs +33 -0
- package/dist/axom-patch/cli/probes/kimi-subagent-subwork.mjs +82 -0
- package/dist/axom-patch/cli/qoder/dist/axom-acp-compat.mjs +1591 -0
- package/dist/axom-patch/cli/qoder/releases/1.1.75.json +360 -0
- package/dist/axom-patch/cli/qoder-background-terminal.md +13 -6
- package/dist/axom-patch/cli/qoder-foreground-output.md +14 -8
- package/dist/axom-patch/cli/qoder-native-acp.json +227 -0
- package/dist/axom-patch/cli/qoder-steering.md +20 -25
- package/dist/axom-patch/cli/qoder-subagent-subwork.md +17 -68
- package/dist/axom-patch/cli/runtime.json +16 -18
- package/dist/axom-patch/cli/zcode-account-limits.json +10 -11
- package/dist/axom-patch/cli/zcode-models.json +1 -2
- package/dist/axom-patch/cli/zcode-subagent-subwork.json +9 -10
- package/dist/cli.js +4 -36
- package/dist/commands/knowledge.js +47 -0
- package/dist/commands/update.js +65 -3
- package/dist/commands/workstation.js +73 -7
- package/dist/install/claude/commands.js +13 -13
- package/dist/mcp.js +1 -1
- package/dist/native/darwin-arm64/axom-quic +0 -0
- package/dist/workstation/acp/acpAgents.js +102 -98
- package/dist/workstation/acp/acpClient.js +26 -0
- package/dist/workstation/acp/builds/install/npm.js +99 -10
- package/dist/workstation/acp/initializeResponse.js +6 -1
- package/dist/workstation/acp/managedPatches.js +189 -25
- package/dist/workstation/acp/managedRuntime.js +314 -0
- package/dist/workstation/acp/patchAssets.js +40 -0
- package/dist/workstation/acp/processor/checkpoint.js +25 -34
- package/dist/workstation/acp/processor/control.js +5 -78
- package/dist/workstation/acp/processor/runtime.js +5 -81
- package/dist/workstation/acp/processor/session.js +51 -80
- package/dist/workstation/acp/processor/state.js +6 -13
- package/dist/workstation/acp/processor/step.js +3 -20
- package/dist/workstation/acp/processor/test/fixtureLog.js +9 -16
- package/dist/workstation/acp/processor/updates.js +131 -105
- package/dist/workstation/acp/processorSession.js +8 -172
- package/dist/workstation/acp/registry/catalog.js +65 -39
- package/dist/workstation/acp/registry/launch.js +16 -1
- package/dist/workstation/acp/switching.js +0 -3
- package/dist/workstation/acp/test/fakeAgent.js +13 -0
- package/dist/workstation/acp/test/keeperWorld.js +28 -0
- package/dist/workstation/acp/test/raw/sources.js +16 -130
- package/dist/workstation/acp/test/realAdapter.js +1 -47
- package/dist/workstation/acp/test/scriptAgent.js +11 -3
- package/dist/workstation/agentFlow.js +1 -1
- package/dist/workstation/agentSessions.js +38 -143
- package/dist/workstation/channels/management.js +95 -11
- package/dist/workstation/channels/url.js +1 -1
- package/dist/workstation/config.js +16 -4
- package/dist/workstation/connection/encryptedChannel.js +4 -1
- package/dist/workstation/control/bindingStore.js +200 -0
- package/dist/workstation/control/commands.js +124 -0
- package/dist/workstation/control/daemonIdentity.js +53 -0
- package/dist/workstation/control/enrollment.js +106 -0
- package/dist/workstation/control/inspect.js +71 -0
- package/dist/workstation/control/locks.js +63 -0
- package/dist/workstation/control/protocol.js +40 -0
- package/dist/workstation/control/restartGuard.js +46 -0
- package/dist/workstation/control/runtimeClient.js +97 -0
- package/dist/workstation/control/runtimeHost.js +148 -0
- package/dist/workstation/control/serverIdentity.js +35 -0
- package/dist/workstation/daemon.js +106 -6
- package/dist/workstation/daemonctl.js +77 -14
- package/dist/workstation/desktopControl/audio.js +99 -0
- package/dist/workstation/desktopControl/capabilities.js +92 -0
- package/dist/workstation/desktopControl/captureModes.js +9 -0
- package/dist/workstation/desktopControl/captureTargetLabel.js +9 -0
- package/dist/workstation/desktopControl/deviceSockets.js +246 -0
- package/dist/workstation/desktopControl/helperProcess.js +353 -0
- package/dist/workstation/desktopControl/lobby.js +129 -0
- package/dist/workstation/desktopControl/macProvider.js +155 -0
- package/dist/workstation/desktopControl/media.js +331 -0
- package/dist/workstation/desktopControl/nativeProvider.js +1018 -0
- package/dist/workstation/desktopControl/networkEvidence.js +43 -0
- package/dist/workstation/desktopControl/overview.js +58 -0
- package/dist/workstation/desktopControl/protocol.js +302 -0
- package/dist/workstation/desktopControl/quicMedia.js +240 -0
- package/dist/workstation/desktopControl/rateController.js +226 -0
- package/dist/workstation/desktopControl/supervisor.js +1681 -0
- package/dist/workstation/desktopControl/transport.js +27 -0
- package/dist/workstation/desktopControl/wallpaper.js +78 -0
- package/dist/workstation/files/credentials.js +142 -0
- package/dist/workstation/files/text.js +42 -8
- package/dist/workstation/keepers/kit/bundle.js +21 -17
- package/dist/workstation/keepers/kit/launcher.js +47 -7
- package/dist/workstation/keepers/kit/paths.js +2 -1
- package/dist/workstation/managedInstall.js +57 -0
- package/dist/workstation/protocol/agent.js +2 -15
- package/dist/workstation/protocol/management.js +93 -4
- package/dist/workstation/protocol/runtimeDiagnostics.js +15 -0
- package/dist/workstation/rd/artifact.js +119 -0
- package/dist/workstation/rd/control.js +119 -0
- package/dist/workstation/rd/display.js +29 -0
- package/dist/workstation/rd/provider.js +222 -0
- package/dist/workstation/rd/sessions.js +391 -0
- package/dist/workstation/rd/state.js +45 -0
- package/dist/workstation/rd/update.js +171 -0
- package/dist/workstation/rd/upkeep.js +38 -0
- package/dist/workstation/register.js +27 -37
- package/dist/workstation/service/installer.js +104 -13
- package/dist/workstation/service/launchd.js +1 -0
- package/dist/workstation/storageMetrics.js +118 -0
- package/dist/workstation/systemMetrics.js +44 -92
- package/dist/workstation/toolchain.js +94 -0
- package/dist/workstation/tools/browser.js +28 -4
- package/dist/workstation/update/artifact.js +2 -1
- package/dist/workstation/update/globalInstall.js +116 -0
- package/dist/workstation/update/npmInstaller.js +49 -23
- package/dist/workstation/update/rdRollback.js +145 -0
- package/dist/workstation/update/runUpdate.js +3 -2
- package/dist/workstation/update/watchdog.js +16790 -334
- package/package.json +5 -5
- package/skills/axom/SKILL.md +11 -11
- package/skills/axom-definition/SKILL.md +3 -3
- package/skills/axom-platform/SKILL.md +3 -3
- package/dist/axom-patch/cli/probes/qoder-steering.mjs +0 -89
- package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +0 -61
- package/dist/axom-patch/cli/qoder-steering.json +0 -52
- package/dist/axom-patch/cli/qoder-subagent-origin.json +0 -73
- package/dist/commands/guide.js +0 -38
- package/dist/runtime-deps/qoder-sdk/package-lock.json +0 -1189
- package/dist/runtime-deps/qoder-sdk/package.json +0 -8
- package/dist/workstation/acp/delegationMcp.js +0 -24
- package/dist/workstation/acp/processor/sideChannel.js +0 -80
- package/dist/workstation/acp/qoderStartup.js +0 -45
- package/dist/workstation/acp/test/raw/qoderCaptures.js +0 -125
- package/dist/workstation/qoderAdapter/backgroundProcess.js +0 -118
- package/dist/workstation/qoderAdapter/backgroundTerminal.js +0 -193
- package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +0 -372
- package/dist/workstation/qoderAdapter/callbackContext.js +0 -76
- package/dist/workstation/qoderAdapter/completedContinuation.js +0 -139
- package/dist/workstation/qoderAdapter/contract.js +0 -78
- package/dist/workstation/qoderAdapter/foregroundOutput.js +0 -36
- package/dist/workstation/qoderAdapter/history.js +0 -85
- package/dist/workstation/qoderAdapter/identityIndex.js +0 -191
- package/dist/workstation/qoderAdapter/inputQueue.js +0 -42
- package/dist/workstation/qoderAdapter/main.js +0 -95
- package/dist/workstation/qoderAdapter/mapper.js +0 -117
- package/dist/workstation/qoderAdapter/observerPlugin.js +0 -245
- package/dist/workstation/qoderAdapter/observerReceiver.js +0 -187
- package/dist/workstation/qoderAdapter/permissions.js +0 -61
- package/dist/workstation/qoderAdapter/profile.js +0 -93
- package/dist/workstation/qoderAdapter/sdkDriver.js +0 -48
- package/dist/workstation/qoderAdapter/sdkRuntime.js +0 -93
- package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +0 -145
- package/dist/workstation/qoderAdapter/session.js +0 -319
- package/dist/workstation/qoderAdapter/subagentMeta.js +0 -28
- package/dist/workstation/qoderAdapter/subwork.js +0 -85
- package/dist/workstation/qoderAdapter/taskControl.js +0 -38
- package/dist/workstation/qoderAdapter/taskLedger.js +0 -163
- package/dist/workstation/qoderAdapter/usage.js +0 -37
- /package/dist/workstation/{qoderAdapter/types.js → desktopControl/provider.js} +0 -0
|
@@ -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`, `
|
|
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 {
|
|
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
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
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 (
|
|
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:
|
|
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
|
|
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,
|
|
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
|
|
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({
|
|
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:
|
|
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:
|
|
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.
|
|
270
|
-
|
|
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,
|
|
273
|
-
...await keepersForManagementWire(this.options.keepers,
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|