@platless/axom-cli 0.5.9 → 0.6.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -6
- package/dist/axom-patch/cli/README.md +129 -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/claude-replay-lifecycle.json +16 -0
- 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/claude-replay-lifecycle.mjs +31 -0
- 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/probes/zcode-env-account.mjs +81 -0
- package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +7 -0
- package/dist/axom-patch/cli/qoder/dist/axom-acp-compat.mjs +1687 -0
- package/dist/axom-patch/cli/qoder/releases/1.1.75.json +277 -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 +19 -19
- package/dist/axom-patch/cli/zcode/env-account.mjs +13 -0
- package/dist/axom-patch/cli/zcode/generate-env-account.py +52 -0
- package/dist/axom-patch/cli/zcode-account-limits.json +10 -11
- package/dist/axom-patch/cli/zcode-env-account.json +58 -0
- package/dist/axom-patch/cli/zcode-models.json +9 -10
- package/dist/axom-patch/cli/zcode-subagent-subwork.json +10 -11
- package/dist/cli.js +11 -45
- package/dist/commands/auth.js +4 -1
- package/dist/commands/knowledge.js +47 -0
- package/dist/commands/update.js +126 -3
- package/dist/commands/workstation.js +101 -164
- package/dist/commands/workstationRegistrations.js +116 -0
- package/dist/credentialBroker.js +13 -2
- package/dist/credentials.js +16 -0
- package/dist/install/claude/commands.js +15 -15
- package/dist/keepers/terminal/keeper.mjs +1089 -673
- package/dist/keepers/terminal/manifest.json +4 -4
- package/dist/mcp.js +1 -1
- package/dist/native/darwin-x64/axom-quic +0 -0
- package/dist/workstation/acp/acpAgents.js +113 -102
- 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/outbox.js +12 -5
- package/dist/workstation/acp/patchAssets.js +40 -0
- package/dist/workstation/acp/permission.js +2 -2
- package/dist/workstation/acp/processor/checkpoint.js +26 -34
- package/dist/workstation/acp/processor/control.js +7 -80
- package/dist/workstation/acp/processor/jsonrpc.js +8 -0
- package/dist/workstation/acp/processor/runtime.js +5 -81
- package/dist/workstation/acp/processor/session.js +54 -82
- package/dist/workstation/acp/processor/state.js +7 -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/transport/local.js +19 -3
- package/dist/workstation/acp/processor/updates.js +156 -105
- package/dist/workstation/acp/processorSession.js +20 -173
- 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 +32 -3
- 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 +114 -166
- package/dist/workstation/channels/agent.js +9 -1
- package/dist/workstation/channels/computer.js +27 -4
- package/dist/workstation/channels/management.js +130 -15
- package/dist/workstation/channels/url.js +1 -1
- package/dist/workstation/config.js +33 -9
- package/dist/workstation/connection/encryptedChannel.js +4 -1
- package/dist/workstation/control/bindingStore.js +204 -0
- package/dist/workstation/control/commands.js +125 -0
- package/dist/workstation/control/daemonIdentity.js +143 -0
- package/dist/workstation/control/deviceCommands.js +191 -0
- package/dist/workstation/control/deviceProtocol.js +68 -0
- package/dist/workstation/control/enrollment.js +106 -0
- package/dist/workstation/control/inspect.js +71 -0
- package/dist/workstation/control/locks.js +110 -0
- package/dist/workstation/control/protocol.js +40 -0
- package/dist/workstation/control/restartGuard.js +46 -0
- package/dist/workstation/control/rootFormat.js +16 -0
- package/dist/workstation/control/runtimeClient.js +152 -0
- package/dist/workstation/control/runtimeHost.js +235 -0
- package/dist/workstation/control/serverIdentity.js +35 -0
- package/dist/workstation/control/updateTransition.js +91 -0
- package/dist/workstation/daemon.js +266 -264
- package/dist/workstation/daemonctl.js +226 -25
- package/dist/workstation/desktopControl/audio.js +99 -0
- package/dist/workstation/desktopControl/automationDevice.js +219 -0
- package/dist/workstation/desktopControl/automationJson.js +74 -0
- package/dist/workstation/desktopControl/automationLocalState.js +23 -0
- package/dist/workstation/desktopControl/automationProtocol.js +253 -0
- package/dist/workstation/desktopControl/automationSchema.generated.js +1 -0
- package/dist/workstation/desktopControl/automationSession.js +81 -0
- package/dist/workstation/desktopControl/automationTransport.js +138 -0
- package/dist/workstation/desktopControl/capabilities.js +93 -0
- package/dist/workstation/desktopControl/captureModes.js +9 -0
- package/dist/workstation/desktopControl/captureTargetLabel.js +9 -0
- package/dist/workstation/desktopControl/deviceAdmission.js +229 -0
- package/dist/workstation/desktopControl/deviceOwnership.js +130 -0
- package/dist/workstation/desktopControl/deviceSockets.js +246 -0
- package/dist/workstation/desktopControl/helperProcess.js +602 -0
- package/dist/workstation/desktopControl/hostGrant.js +65 -0
- package/dist/workstation/desktopControl/lobby.js +131 -0
- package/dist/workstation/desktopControl/macProvider.js +156 -0
- package/dist/workstation/desktopControl/media.js +331 -0
- package/dist/workstation/desktopControl/nativeDeviceAdmission.js +207 -0
- package/dist/workstation/desktopControl/nativeProvider.js +1044 -0
- package/dist/workstation/desktopControl/networkEvidence.js +43 -0
- package/dist/workstation/desktopControl/overview.js +58 -0
- package/dist/workstation/desktopControl/ownershipProtocol.js +31 -0
- package/dist/workstation/desktopControl/protocol.js +311 -0
- package/dist/workstation/desktopControl/provider.js +1 -0
- package/dist/workstation/desktopControl/quicMedia.js +240 -0
- package/dist/workstation/desktopControl/rateController.js +226 -0
- package/dist/workstation/desktopControl/supervisor.js +1774 -0
- package/dist/workstation/desktopControl/transport.js +27 -0
- package/dist/workstation/desktopControl/wallpaper.js +98 -0
- package/dist/workstation/exec/policy.js +5 -0
- package/dist/workstation/files/client.js +10 -1
- package/dist/workstation/files/credentials.js +142 -0
- package/dist/workstation/files/host.js +9 -2
- package/dist/workstation/files/text.js +42 -8
- package/dist/workstation/journal.js +5 -2
- package/dist/workstation/keepers/acp/client.js +38 -2
- package/dist/workstation/keepers/kit/buildVersion.js +35 -11
- package/dist/workstation/keepers/kit/bundle.js +21 -17
- package/dist/workstation/keepers/kit/client.js +2 -2
- package/dist/workstation/keepers/kit/launcher.js +123 -7
- package/dist/workstation/keepers/kit/paths.js +2 -1
- package/dist/workstation/keepers/legacy.js +16 -2
- package/dist/workstation/keepers/policy.js +5 -34
- package/dist/workstation/keepers/terminal/client.js +115 -23
- package/dist/workstation/keepers/terminal/exec.js +6 -3
- package/dist/workstation/keepers/terminal/frames.js +73 -0
- package/dist/workstation/keepers/terminal/generations.js +5 -8
- package/dist/workstation/keepers/terminal/gridProtocol.js +16 -0
- package/dist/workstation/keepers/terminal/input.js +146 -0
- package/dist/workstation/keepers/terminal/keeper.js +22 -8
- package/dist/workstation/keepers/terminal/main.js +1 -1
- package/dist/workstation/keepers/terminal/metadata.js +138 -0
- package/dist/workstation/keepers/terminal/protocol.js +4 -28
- package/dist/workstation/keepers/terminal/pty.js +1 -330
- package/dist/workstation/keepers/terminal/screen.js +246 -0
- package/dist/workstation/keepers/terminal/sessions.js +526 -0
- package/dist/workstation/managedInstall.js +57 -0
- package/dist/workstation/oauth/deviceAuthorization.js +31 -17
- package/dist/workstation/protocol/agent.js +2 -15
- package/dist/workstation/protocol/management.js +113 -5
- package/dist/workstation/protocol/runtimeDiagnostics.js +15 -0
- package/dist/workstation/rd/artifact.js +120 -0
- package/dist/workstation/rd/control.js +117 -0
- package/dist/workstation/rd/display.js +29 -0
- package/dist/workstation/rd/host.js +213 -0
- package/dist/workstation/rd/migrationReadiness.js +46 -0
- package/dist/workstation/rd/provider.js +256 -0
- package/dist/workstation/rd/sessions.js +391 -0
- package/dist/workstation/rd/state.js +57 -0
- package/dist/workstation/rd/update.js +168 -0
- package/dist/workstation/rd/upkeep.js +80 -0
- package/dist/workstation/register.js +27 -37
- package/dist/workstation/registrations/enrollment.js +250 -0
- package/dist/workstation/registrations/enrollmentPending.js +361 -0
- package/dist/workstation/registrations/migration.js +356 -0
- package/dist/workstation/registrations/model.js +34 -0
- package/dist/workstation/registrations/removal.js +222 -0
- package/dist/workstation/registrations/store.js +737 -0
- package/dist/workstation/runtime/desktopBinding.js +20 -0
- package/dist/workstation/runtime/deviceDesktop.js +193 -0
- package/dist/workstation/runtime/deviceRuntime.js +568 -0
- package/dist/workstation/runtime/deviceServices.js +117 -0
- package/dist/workstation/runtime/events.js +20 -0
- package/dist/workstation/runtime/ownership.js +100 -0
- package/dist/workstation/runtime/registrationRuntime.js +575 -0
- package/dist/workstation/runtime/remoteDesktopAdapter.js +186 -0
- package/dist/workstation/runtime/removalActivity.js +184 -0
- package/dist/workstation/service/installer.js +197 -14
- package/dist/workstation/service/launchd.js +2 -0
- package/dist/workstation/service/systemd.js +1 -0
- package/dist/workstation/storageMetrics.js +118 -0
- package/dist/workstation/systemMetrics.js +47 -92
- package/dist/workstation/toolchain.js +94 -0
- package/dist/workstation/tools/browser.js +28 -4
- package/dist/workstation/tools/client.js +8 -27
- package/dist/workstation/tools/info.js +6 -13
- package/dist/workstation/tools/terminalRouter.js +17 -172
- package/dist/workstation/tools/validation.js +55 -1
- package/dist/workstation/update/archiveEvidence.js +206 -0
- package/dist/workstation/update/artifact.js +2 -1
- package/dist/workstation/update/coordinator.js +34 -13
- package/dist/workstation/update/deviceCompatibility.js +119 -0
- package/dist/workstation/update/deviceCoordinator.js +369 -0
- package/dist/workstation/update/deviceHealth.js +104 -0
- package/dist/workstation/update/deviceHealthProtocol.js +26 -0
- package/dist/workstation/update/deviceRecoveryLifecycle.js +46 -0
- package/dist/workstation/update/deviceRollback.js +78 -0
- package/dist/workstation/update/globalInstall.js +186 -0
- package/dist/workstation/update/legacyBridge.js +414 -0
- package/dist/workstation/update/legacyOwner.js +39 -0
- package/dist/workstation/update/npmInstaller.js +51 -25
- package/dist/workstation/update/privateDurability.js +61 -0
- package/dist/workstation/update/rdRollback.js +145 -0
- package/dist/workstation/update/recoveryPlan.js +124 -0
- package/dist/workstation/update/runUpdate.js +10 -3
- package/dist/workstation/update/taskRegistry.js +161 -3
- package/dist/workstation/update/watchdog.js +29806 -333
- package/dist/workstation/update/watchdogProcess.js +72 -0
- package/package.json +11 -7
- 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/automationPreparation.js} +0 -0
|
@@ -1,188 +1,33 @@
|
|
|
1
1
|
import { accessSync, constants, statSync } from 'node:fs';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import {
|
|
5
|
-
import { PTY_SESSION_LIMIT } from '../keepers/terminal/protocol.js';
|
|
6
|
-
import { TERMINAL_NOT_FOUND } from '../keepers/legacy.js';
|
|
7
|
-
import { identifier, integer, keys } from './validation.js';
|
|
8
|
-
const LEGACY = 'legacy';
|
|
9
|
-
/** The single-keeper form of {@link KitGenerations}: one generation named `kit`, running when it answers a hello. */
|
|
10
|
-
function singleKit(kit) {
|
|
11
|
-
return {
|
|
12
|
-
servingGeneration: async () => ({ gen: 'kit', client: kit }),
|
|
13
|
-
running: async () => [{ gen: 'kit', client: kit }],
|
|
14
|
-
};
|
|
15
|
-
}
|
|
4
|
+
import { validateRequest } from './validation.js';
|
|
16
5
|
export class TerminalRouter {
|
|
17
6
|
deps;
|
|
18
|
-
owners = new Map();
|
|
19
|
-
kits;
|
|
20
|
-
/** Clients of the generations seen in the last listing, by generation. */
|
|
21
|
-
clients = new Map();
|
|
22
7
|
constructor(deps) {
|
|
23
8
|
this.deps = deps;
|
|
24
|
-
if (!deps.kits && !deps.kit)
|
|
25
|
-
throw new Error('TerminalRouter needs keeper-terminal');
|
|
26
|
-
this.kits = deps.kits ?? singleKit(deps.kit);
|
|
27
9
|
}
|
|
28
|
-
/** The current owner map (tests and diagnostics): `legacy`, or a generation (`kit` with a single keeper-terminal). */
|
|
29
|
-
ownerOf(id) { return this.owners.get(id); }
|
|
30
10
|
async request(method, params) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
11
|
+
validateRequest(method, params);
|
|
12
|
+
const op = method.slice('terminal.'.length);
|
|
13
|
+
const kit = this.deps.kit;
|
|
14
|
+
if (op === 'list')
|
|
15
|
+
return { ...await this.list() };
|
|
16
|
+
if (op === 'create') {
|
|
17
|
+
const existing = (await this.list()).sessions.find(session => session.id === params.id);
|
|
18
|
+
if (existing)
|
|
19
|
+
return { ...existing };
|
|
20
|
+
const launch = await this.deps.launch(params.cwd);
|
|
21
|
+
return { ...await kit.create({ id: params.id, ...launch }) };
|
|
35
22
|
}
|
|
36
|
-
if (
|
|
37
|
-
return { ...await this.create(params) };
|
|
38
|
-
if (!PTY_OPERATIONS.has(op))
|
|
23
|
+
if (!['attach', 'poll', 'resize', 'input', 'scroll', 'detach', 'close'].includes(op))
|
|
39
24
|
throw new Error('Unsupported workstation tool');
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
if (!owner)
|
|
43
|
-
throw new Error(TERMINAL_NOT_FOUND);
|
|
44
|
-
const kit = owner === LEGACY ? undefined : this.clients.get(owner);
|
|
45
|
-
if (owner !== LEGACY && !kit)
|
|
46
|
-
throw new Error(TERMINAL_NOT_FOUND);
|
|
47
|
-
try {
|
|
48
|
-
const result = owner === LEGACY
|
|
49
|
-
? await this.deps.legacy.request(method, params)
|
|
50
|
-
: { ...await kit[op].call(kit, params) };
|
|
51
|
-
if (op === 'close')
|
|
52
|
-
this.owners.delete(id);
|
|
53
|
-
return result;
|
|
54
|
-
}
|
|
55
|
-
catch (error) {
|
|
56
|
-
// The owner no longer has it (closed elsewhere, keeper restarted): forget it; the next request looks it up again.
|
|
57
|
-
if (error instanceof Error && error.message === TERMINAL_NOT_FOUND)
|
|
58
|
-
this.owners.delete(id);
|
|
59
|
-
throw error;
|
|
60
|
-
}
|
|
25
|
+
const call = kit[op];
|
|
26
|
+
return { ...await call.call(kit, params) };
|
|
61
27
|
}
|
|
62
28
|
async list() {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const out = [...(legacy ?? [])];
|
|
66
|
-
for (const sessions of kits.values())
|
|
67
|
-
for (const session of sessions ?? [])
|
|
68
|
-
if (!seen.has(session.id)) {
|
|
69
|
-
seen.add(session.id);
|
|
70
|
-
out.push(session);
|
|
71
|
-
}
|
|
72
|
-
return out;
|
|
73
|
-
}
|
|
74
|
-
/** Lists every side once (keeper-terminal generations only if running) and refreshes the owners of every side that answered. */
|
|
75
|
-
async refresh() {
|
|
76
|
-
const [legacy, kits] = await Promise.all([this.legacySessions(), this.kitRefresh()]);
|
|
77
|
-
if (legacy)
|
|
78
|
-
this.replaceOwners(LEGACY, legacy);
|
|
79
|
-
else if (!this.deps.legacy?.live)
|
|
80
|
-
this.replaceOwners(LEGACY, []);
|
|
81
|
-
return { legacy, kits };
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Lists every running keeper-terminal generation (never starts one) and refreshes their owners: a generation that answered gets
|
|
85
|
-
* exactly its sessions; one that did not answer keeps its owners; one that is no longer running loses them (its PTYs are gone).
|
|
86
|
-
*/
|
|
87
|
-
async kitRefresh() {
|
|
88
|
-
const listed = new Map();
|
|
89
|
-
let running;
|
|
90
|
-
try {
|
|
91
|
-
running = await this.kits.running();
|
|
92
|
-
}
|
|
93
|
-
catch {
|
|
94
|
-
return listed;
|
|
95
|
-
}
|
|
96
|
-
await Promise.all(running.map(async ({ gen, client }) => {
|
|
97
|
-
this.clients.set(gen, client);
|
|
98
|
-
listed.set(gen, await this.sessionsOf(client));
|
|
99
|
-
}));
|
|
100
|
-
for (const [gen, sessions] of listed)
|
|
101
|
-
if (sessions)
|
|
102
|
-
this.replaceOwners(gen, sessions);
|
|
103
|
-
for (const [id, owner] of [...this.owners])
|
|
104
|
-
if (owner !== LEGACY && !listed.has(owner))
|
|
105
|
-
this.owners.delete(id);
|
|
106
|
-
for (const gen of [...this.clients.keys()])
|
|
107
|
-
if (!listed.has(gen))
|
|
108
|
-
this.clients.delete(gen);
|
|
109
|
-
return listed;
|
|
110
|
-
}
|
|
111
|
-
replaceOwners(side, sessions) {
|
|
112
|
-
for (const [id, owner] of this.owners)
|
|
113
|
-
if (owner === side)
|
|
114
|
-
this.owners.delete(id);
|
|
115
|
-
for (const session of sessions)
|
|
116
|
-
if (side === LEGACY || !this.owners.has(session.id))
|
|
117
|
-
this.owners.set(session.id, side);
|
|
118
|
-
}
|
|
119
|
-
/** The legacy keeper's sessions; `sessions()` probes it first (read-only), so a daemon that just started finds it too. */
|
|
120
|
-
async legacySessions() {
|
|
121
|
-
return this.deps.legacy ? this.deps.legacy.sessions() : null;
|
|
122
|
-
}
|
|
123
|
-
/** One generation's sessions if it is already running; null when it is not (or did not answer): nothing is started here. */
|
|
124
|
-
async sessionsOf(client) {
|
|
125
|
-
try {
|
|
126
|
-
if (!await client.runningHello())
|
|
127
|
-
return null;
|
|
128
|
-
return (await client.list()).sessions;
|
|
129
|
-
}
|
|
130
|
-
catch {
|
|
131
|
-
return null;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
async resolveOwner(id) {
|
|
135
|
-
const known = this.owners.get(id);
|
|
136
|
-
if ((known !== undefined && known !== LEGACY && this.clients.has(known)) || (known === LEGACY && this.deps.legacy?.live))
|
|
137
|
-
return known;
|
|
138
|
-
await this.refresh();
|
|
139
|
-
return this.owners.get(id);
|
|
140
|
-
}
|
|
141
|
-
async create(params) {
|
|
142
|
-
// The legacy keeper's checks, wording and order (tools/terminal.ts at the merge-base: id, clientId, keys, cols, rows, name,
|
|
143
|
-
// then existing, limit, cwd), all before the cwd is resolved.
|
|
144
|
-
const id = identifier(params, 'id');
|
|
145
|
-
identifier(params, 'clientId');
|
|
146
|
-
keys(params, [...CREATE_KEYS]);
|
|
147
|
-
integer(params, 'cols', 20, 300, 100);
|
|
148
|
-
integer(params, 'rows', 5, 100, 30);
|
|
149
|
-
const name = params.name;
|
|
150
|
-
if (name !== undefined && (typeof name !== 'string' || !name.length || name.length > 128 || /[\x00-\x1f]/.test(name)))
|
|
151
|
-
throw new Error('Invalid terminal name');
|
|
152
|
-
const legacy = this.deps.legacy;
|
|
153
|
-
const knownOwner = this.owners.get(id);
|
|
154
|
-
if (legacy && (knownOwner === undefined || knownOwner === LEGACY)) {
|
|
155
|
-
// The legacy keeper never gains a session after the upgrade (creates go to keeper-terminal), so the ids of its first
|
|
156
|
-
// successful list answer "is this a retry of one of its sessions?" for good — even while it is busy or hung.
|
|
157
|
-
if (legacy.knownIds === null) {
|
|
158
|
-
const sessions = await legacy.sessions(); // bounded; null at once when it is busy or absent
|
|
159
|
-
if (sessions)
|
|
160
|
-
this.replaceOwners(LEGACY, sessions);
|
|
161
|
-
}
|
|
162
|
-
if (legacy.live && legacy.knownIds?.has(id))
|
|
163
|
-
return legacy.request('terminal.create', params);
|
|
164
|
-
}
|
|
165
|
-
// Duplicate and limit first (the legacy order), against the keeper-terminal generations already running: an idempotent create
|
|
166
|
-
// (in whichever generation has the id) costs no files-host round trip. keeper-terminal re-checks both when it really creates.
|
|
167
|
-
const listed = await this.kitRefresh();
|
|
168
|
-
for (const [gen, sessions] of listed) {
|
|
169
|
-
const existing = sessions?.find((session) => session.id === id);
|
|
170
|
-
if (existing) {
|
|
171
|
-
this.owners.set(id, gen);
|
|
172
|
-
return existing;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
const serving = await this.kits.servingGeneration();
|
|
176
|
-
const running = listed.get(serving.gen);
|
|
177
|
-
if (running && running.length >= PTY_SESSION_LIMIT)
|
|
178
|
-
throw new Error('Terminal session limit exceeded');
|
|
179
|
-
if (params.cwd !== undefined && typeof params.cwd !== 'string')
|
|
180
|
-
throw new Error('Invalid cwd');
|
|
181
|
-
const launch = await this.deps.launch(params.cwd);
|
|
182
|
-
const created = await serving.client.create({ ...params, ...launch });
|
|
183
|
-
this.clients.set(serving.gen, serving.client);
|
|
184
|
-
this.owners.set(id, serving.gen);
|
|
185
|
-
return created;
|
|
29
|
+
// The typed client is non-launching and distinguishes proven absence from an unreachable owner.
|
|
30
|
+
return this.deps.kit.list();
|
|
186
31
|
}
|
|
187
32
|
}
|
|
188
33
|
/**
|
|
@@ -5,7 +5,7 @@ export const CHUNK_BYTES = 64 * 1024;
|
|
|
5
5
|
export const TOOL_METHODS = new Set([
|
|
6
6
|
'browser.list', 'browser.create', 'browser.attach', 'browser.frame', 'browser.navigate', 'browser.input', 'browser.takeover', 'browser.detach', 'browser.close',
|
|
7
7
|
'tools.info', 'terminal.list', 'terminal.create', 'terminal.attach', 'terminal.poll',
|
|
8
|
-
'terminal.
|
|
8
|
+
'terminal.input', 'terminal.resize', 'terminal.scroll', 'terminal.detach', 'terminal.close',
|
|
9
9
|
'files.media.prepare', 'files.media.read', 'files.media.cancel',
|
|
10
10
|
'files.media.open', 'files.media.status', 'files.media.seek', 'files.media.interest', 'files.media.object', 'files.media.release',
|
|
11
11
|
'files.list', 'files.find', 'files.read', 'files.mkdir', 'files.rename', 'files.delete',
|
|
@@ -49,6 +49,8 @@ export function validateRequest(method, params) {
|
|
|
49
49
|
throw new Error('Unsupported workstation tool');
|
|
50
50
|
if (!object(params) || Buffer.byteLength(JSON.stringify(params)) > TOOL_REQUEST_BYTES)
|
|
51
51
|
throw new Error('Invalid or oversized tool parameters');
|
|
52
|
+
if (method.startsWith('terminal.'))
|
|
53
|
+
validateTerminal(method, params);
|
|
52
54
|
const shape = MEDIA_SHAPES[method];
|
|
53
55
|
if (shape) {
|
|
54
56
|
keys(params, [...shape, 'readOnly', 'filesystem']);
|
|
@@ -84,3 +86,55 @@ const MEDIA_SHAPES = {
|
|
|
84
86
|
'files.media.object': ['path', 'version', 'consumerId', 'objectId', 'offset', 'length'],
|
|
85
87
|
'files.media.release': ['path', 'version', 'consumerId'],
|
|
86
88
|
};
|
|
89
|
+
/** Native terminal kinds have exact flat shapes. Geometry freshness is checked against the keeper's live grid. */
|
|
90
|
+
function validateTerminal(method, params) {
|
|
91
|
+
const ref = ['id', 'incarnation', 'clientId'];
|
|
92
|
+
const kinds = { key: ['usage', 'down', 'modifiers'], text: ['text'], paste: ['text'], pointer: ['x', 'y', 'geometry', 'modifiers'], button: ['x', 'y', 'geometry', 'button', 'down', 'modifiers'], release: [] };
|
|
93
|
+
const shapes = {
|
|
94
|
+
list: [], create: ['id', 'cwd'], attach: [...ref, 'cols', 'rows'], poll: [...ref, 'after'], resize: [...ref, 'cols', 'rows'],
|
|
95
|
+
scroll: [...ref, 'lines', 'x', 'y', 'modifiers', 'geometry'], detach: ref, close: ['id', 'incarnation'],
|
|
96
|
+
};
|
|
97
|
+
const op = method.slice('terminal.'.length);
|
|
98
|
+
let shape = shapes[op];
|
|
99
|
+
if (op === 'input') {
|
|
100
|
+
if (typeof params.kind !== 'string' || !Object.hasOwn(kinds, params.kind))
|
|
101
|
+
throw new Error('Invalid kind');
|
|
102
|
+
shape = [...ref, 'kind', ...kinds[params.kind]];
|
|
103
|
+
}
|
|
104
|
+
if (!shape)
|
|
105
|
+
throw new Error('Unsupported workstation tool');
|
|
106
|
+
keys(params, shape);
|
|
107
|
+
for (const name of shape) {
|
|
108
|
+
if (name === 'cwd') {
|
|
109
|
+
if (params.cwd !== undefined)
|
|
110
|
+
string(params, name);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (!(name in params))
|
|
114
|
+
throw new Error(`Missing ${name}`);
|
|
115
|
+
if (['id', 'incarnation', 'clientId'].includes(name))
|
|
116
|
+
identifier(params, name);
|
|
117
|
+
else if (name === 'cols')
|
|
118
|
+
integer(params, name, 20, 300);
|
|
119
|
+
else if (name === 'rows')
|
|
120
|
+
integer(params, name, 5, 100);
|
|
121
|
+
else if (name === 'after' || name === 'geometry')
|
|
122
|
+
integer(params, name, name === 'geometry' ? 1 : 0, Number.MAX_SAFE_INTEGER);
|
|
123
|
+
else if (name === 'x')
|
|
124
|
+
integer(params, name, 0, 299);
|
|
125
|
+
else if (name === 'y')
|
|
126
|
+
integer(params, name, 0, 99);
|
|
127
|
+
else if (name === 'lines')
|
|
128
|
+
integer(params, name, -Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER);
|
|
129
|
+
else if (name === 'usage')
|
|
130
|
+
integer(params, name, 0, 65535);
|
|
131
|
+
else if (name === 'button')
|
|
132
|
+
integer(params, name, 0, 2);
|
|
133
|
+
else if (name === 'modifiers')
|
|
134
|
+
integer(params, name, 0, 15);
|
|
135
|
+
else if (name === 'down' && typeof params[name] !== 'boolean')
|
|
136
|
+
throw new Error('Invalid down');
|
|
137
|
+
else if (name === 'text' && (typeof params.text !== 'string' || Buffer.byteLength(params.text) > 65536))
|
|
138
|
+
throw new Error('Invalid text');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { constants, promises as fs } from 'node:fs';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { gunzipSync } from 'node:zlib';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
export const rootReaderSchema = z.object({ schemaVersion: z.literal(1), rootFormat: z.literal(2), legacySourceMapping: z.literal(true), coverageEvidence: z.boolean().optional() }).strict();
|
|
6
|
+
export const archiveEvidenceSchema = z.object({
|
|
7
|
+
name: z.literal('@platless/axom-cli'), version: z.string().min(1).max(128), size: z.number().int().positive().max(536_870_912), sha512: z.string().regex(/^[a-f0-9]{128}$/), reader: rootReaderSchema,
|
|
8
|
+
runtimeFiles: z.array(z.object({ path: z.string().min(1).max(4096).refine(value => (value === 'package.json' || value.startsWith('dist/')) && !/[^\x20-\x7e]|\\/.test(value) && value.split('/').every(part => part && part !== '.' && part !== '..')), size: z.number().int().min(0).max(134_217_728), sha256: z.string().regex(/^[a-f0-9]{64}$/) }).strict()).min(2).max(65_536),
|
|
9
|
+
}).strict().refine(value => value.runtimeFiles.some(file => file.path === 'package.json') && value.runtimeFiles.some(file => file.path === 'dist/cli.js') && new Set(value.runtimeFiles.map(file => file.path.toLowerCase())).size === value.runtimeFiles.length);
|
|
10
|
+
const archiveLimit = 536_870_912;
|
|
11
|
+
const expandedLimit = 1_073_741_824;
|
|
12
|
+
const memberLimit = 134_217_728;
|
|
13
|
+
const metadataLimit = 1_048_576;
|
|
14
|
+
const memberCountLimit = 65_536;
|
|
15
|
+
const recovery = 'Compatible root-format-2 reader recovery required; preserve the archive and update evidence and install an explicitly compatible CLI';
|
|
16
|
+
function invalid() { throw new Error(recovery); }
|
|
17
|
+
function stringField(header, start, length) {
|
|
18
|
+
const field = header.subarray(start, start + length);
|
|
19
|
+
const end = field.indexOf(0);
|
|
20
|
+
const bytes = end < 0 ? field : field.subarray(0, end);
|
|
21
|
+
const value = bytes.toString('utf8');
|
|
22
|
+
if (!Buffer.from(value).equals(bytes))
|
|
23
|
+
invalid();
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
function octal(header, start, length) {
|
|
27
|
+
const value = stringField(header, start, length).trim();
|
|
28
|
+
if (!/^[0-7]+$/.test(value))
|
|
29
|
+
invalid();
|
|
30
|
+
const parsed = Number.parseInt(value, 8);
|
|
31
|
+
if (!Number.isSafeInteger(parsed) || parsed < 0)
|
|
32
|
+
invalid();
|
|
33
|
+
return parsed;
|
|
34
|
+
}
|
|
35
|
+
function canonical(value, directory) {
|
|
36
|
+
if (directory && value.endsWith('/'))
|
|
37
|
+
value = value.slice(0, -1);
|
|
38
|
+
if (!value || value.length > 4096 || /[^\x20-\x7e]|\\/.test(value) || !value.startsWith('package/') && value !== 'package')
|
|
39
|
+
invalid();
|
|
40
|
+
if (value.split('/').length > 64 || value.split('/').some(part => !part || part === '.' || part === '..'))
|
|
41
|
+
invalid();
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
function paxFields(content) {
|
|
45
|
+
const fields = new Map();
|
|
46
|
+
let offset = 0;
|
|
47
|
+
while (offset < content.length) {
|
|
48
|
+
const space = content.indexOf(32, offset);
|
|
49
|
+
if (space < offset || space - offset > 10)
|
|
50
|
+
invalid();
|
|
51
|
+
const declared = content.subarray(offset, space).toString('ascii');
|
|
52
|
+
if (!/^[1-9][0-9]*$/.test(declared))
|
|
53
|
+
invalid();
|
|
54
|
+
const length = Number(declared);
|
|
55
|
+
if (!Number.isSafeInteger(length) || length <= space - offset + 2 || offset + length > content.length || content[offset + length - 1] !== 10)
|
|
56
|
+
invalid();
|
|
57
|
+
const field = content.subarray(space + 1, offset + length - 1);
|
|
58
|
+
const equals = field.indexOf(61);
|
|
59
|
+
if (equals < 1)
|
|
60
|
+
invalid();
|
|
61
|
+
const key = field.subarray(0, equals).toString('utf8');
|
|
62
|
+
const valueBytes = field.subarray(equals + 1);
|
|
63
|
+
const value = valueBytes.toString('utf8');
|
|
64
|
+
if (!Buffer.from(value).equals(valueBytes) || fields.has(key) || !['path', 'size', 'mtime', 'atime', 'ctime', 'uid', 'gid', 'uname', 'gname', 'SCHILY.dev', 'SCHILY.ino', 'SCHILY.nlink'].includes(key))
|
|
65
|
+
invalid();
|
|
66
|
+
fields.set(key, value);
|
|
67
|
+
offset += length;
|
|
68
|
+
}
|
|
69
|
+
return fields;
|
|
70
|
+
}
|
|
71
|
+
function decimal(value) {
|
|
72
|
+
if (!/^(0|[1-9][0-9]*)$/.test(value))
|
|
73
|
+
invalid();
|
|
74
|
+
const parsed = Number(value);
|
|
75
|
+
if (!Number.isSafeInteger(parsed) || parsed < 0)
|
|
76
|
+
invalid();
|
|
77
|
+
return parsed;
|
|
78
|
+
}
|
|
79
|
+
export async function readArchiveEvidence(archive) {
|
|
80
|
+
const handle = await fs.open(archive, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
81
|
+
let bytes;
|
|
82
|
+
try {
|
|
83
|
+
const stat = await handle.stat();
|
|
84
|
+
if (!stat.isFile() || stat.nlink !== 1 || stat.size < 1 || stat.size > archiveLimit)
|
|
85
|
+
invalid();
|
|
86
|
+
bytes = await handle.readFile();
|
|
87
|
+
const after = await handle.stat();
|
|
88
|
+
if (bytes.length !== stat.size || stat.size !== after.size || stat.mtimeMs !== after.mtimeMs || stat.ctimeMs !== after.ctimeMs)
|
|
89
|
+
invalid();
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
await handle.close();
|
|
93
|
+
}
|
|
94
|
+
let tar;
|
|
95
|
+
try {
|
|
96
|
+
tar = gunzipSync(bytes, { maxOutputLength: expandedLimit });
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
invalid();
|
|
100
|
+
}
|
|
101
|
+
if (tar.length % 512)
|
|
102
|
+
invalid();
|
|
103
|
+
const seen = new Set();
|
|
104
|
+
const files = new Set();
|
|
105
|
+
const directories = new Set();
|
|
106
|
+
const spellings = new Map();
|
|
107
|
+
const runtimeFiles = [];
|
|
108
|
+
let metadata;
|
|
109
|
+
let pending;
|
|
110
|
+
let longName;
|
|
111
|
+
let offset = 0;
|
|
112
|
+
let ended = false;
|
|
113
|
+
while (offset + 512 <= tar.length) {
|
|
114
|
+
const header = tar.subarray(offset, offset + 512);
|
|
115
|
+
if (header.every(value => value === 0)) {
|
|
116
|
+
if (pending || longName || offset + 1024 > tar.length || !tar.subarray(offset).every(value => value === 0))
|
|
117
|
+
invalid();
|
|
118
|
+
ended = true;
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
let checksum = 0;
|
|
122
|
+
for (let index = 0; index < 512; index += 1)
|
|
123
|
+
checksum += index >= 148 && index < 156 ? 32 : header[index];
|
|
124
|
+
if (checksum !== octal(header, 148, 8))
|
|
125
|
+
invalid();
|
|
126
|
+
if (!['ustar', 'ustar '].includes(stringField(header, 257, 6)))
|
|
127
|
+
invalid();
|
|
128
|
+
const type = stringField(header, 156, 1);
|
|
129
|
+
if (!['', '0', '5', 'x', 'L'].includes(type))
|
|
130
|
+
invalid();
|
|
131
|
+
const rawSize = octal(header, 124, 12);
|
|
132
|
+
const contentSize = type === 'x' || type === 'L' ? rawSize : pending?.has('size') ? decimal(pending.get('size')) : rawSize;
|
|
133
|
+
if (contentSize > memberLimit || (type === 'x' || type === 'L') && contentSize > metadataLimit)
|
|
134
|
+
invalid();
|
|
135
|
+
const start = offset + 512;
|
|
136
|
+
const end = start + contentSize;
|
|
137
|
+
if (end > tar.length)
|
|
138
|
+
invalid();
|
|
139
|
+
const content = tar.subarray(start, end);
|
|
140
|
+
offset = start + Math.ceil(contentSize / 512) * 512;
|
|
141
|
+
if (type === 'x') {
|
|
142
|
+
if (pending || longName)
|
|
143
|
+
invalid();
|
|
144
|
+
pending = paxFields(content);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (type === 'L') {
|
|
148
|
+
if (pending || longName || !content.length || content[content.length - 1] !== 0)
|
|
149
|
+
invalid();
|
|
150
|
+
longName = stringField(content, 0, content.length);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const prefix = stringField(header, 345, 155);
|
|
154
|
+
const name = canonical(pending?.get('path') ?? longName ?? `${prefix ? prefix + '/' : ''}${stringField(header, 0, 100)}`, type === '5');
|
|
155
|
+
pending = undefined;
|
|
156
|
+
longName = undefined;
|
|
157
|
+
if (seen.has(name) || seen.size >= memberCountLimit || type === '5' && contentSize !== 0 || name === 'package' && type !== '5')
|
|
158
|
+
invalid();
|
|
159
|
+
const parts = name.split('/');
|
|
160
|
+
for (let index = 1; index <= parts.length; index += 1) {
|
|
161
|
+
const prefixName = parts.slice(0, index).join('/');
|
|
162
|
+
const alias = prefixName.toLowerCase();
|
|
163
|
+
const previous = spellings.get(alias);
|
|
164
|
+
if (previous && previous !== prefixName || index < parts.length && files.has(alias))
|
|
165
|
+
invalid();
|
|
166
|
+
spellings.set(alias, prefixName);
|
|
167
|
+
if (index < parts.length)
|
|
168
|
+
directories.add(alias);
|
|
169
|
+
}
|
|
170
|
+
const alias = name.toLowerCase();
|
|
171
|
+
if (type === '5') {
|
|
172
|
+
if (files.has(alias))
|
|
173
|
+
invalid();
|
|
174
|
+
directories.add(alias);
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
if (directories.has(alias))
|
|
178
|
+
invalid();
|
|
179
|
+
files.add(alias);
|
|
180
|
+
}
|
|
181
|
+
seen.add(name);
|
|
182
|
+
if (type === '5')
|
|
183
|
+
continue;
|
|
184
|
+
if (name === 'package/package.json') {
|
|
185
|
+
if (contentSize > metadataLimit)
|
|
186
|
+
invalid();
|
|
187
|
+
try {
|
|
188
|
+
metadata = JSON.parse(content.toString('utf8'));
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
invalid();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (name === 'package/package.json' || name.startsWith('package/dist/'))
|
|
195
|
+
runtimeFiles.push({ path: name.slice(8), size: contentSize, sha256: createHash('sha256').update(content).digest('hex') });
|
|
196
|
+
}
|
|
197
|
+
if (!ended || typeof metadata !== 'object' || !metadata || Array.isArray(metadata))
|
|
198
|
+
invalid();
|
|
199
|
+
const value = metadata;
|
|
200
|
+
const reader = rootReaderSchema.safeParse(value.axomWorkstationRootReader);
|
|
201
|
+
if (value.name !== '@platless/axom-cli' || typeof value.version !== 'string' || !value.version || value.version.length > 128 || !reader.success)
|
|
202
|
+
invalid();
|
|
203
|
+
if (!value.bin || typeof value.bin !== 'object' || Array.isArray(value.bin) || value.bin.axom !== 'dist/cli.js' || !runtimeFiles.some(entry => entry.path === 'dist/cli.js'))
|
|
204
|
+
invalid();
|
|
205
|
+
return { name: value.name, version: value.version, size: bytes.length, sha512: createHash('sha512').update(bytes).digest('hex'), reader: reader.data, runtimeFiles: runtimeFiles.sort((left, right) => left.path.localeCompare(right.path)) };
|
|
206
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { deploymentDownloadURL } from '../control/serverIdentity.js';
|
|
1
2
|
import { createHash, timingSafeEqual } from 'node:crypto';
|
|
2
3
|
import { createWriteStream, promises as fs } from 'node:fs';
|
|
3
4
|
import { tmpdir } from 'node:os';
|
|
@@ -16,7 +17,7 @@ function safeRelease(release, server) {
|
|
|
16
17
|
if ((configured.protocol !== 'http:' && configured.protocol !== 'https:') || configured.username || configured.password) {
|
|
17
18
|
throw new Error('Configured server URL must be an HTTP(S) origin without credentials');
|
|
18
19
|
}
|
|
19
|
-
const url = new URL(release.url
|
|
20
|
+
const url = new URL(deploymentDownloadURL(server, release.url));
|
|
20
21
|
// The release descriptor arrives over the authenticated management connection, but keeping the
|
|
21
22
|
// download on that exact origin prevents an injected relative/absolute URL from turning the
|
|
22
23
|
// client into a credentialed downloader. HTTP remains supported for private-network development
|
|
@@ -22,6 +22,11 @@ export class UpdateCoordinator {
|
|
|
22
22
|
constructor(options) {
|
|
23
23
|
this.options = options;
|
|
24
24
|
}
|
|
25
|
+
legacyTasks() {
|
|
26
|
+
if (this.options.deviceAdmission || !this.options.tasks)
|
|
27
|
+
throw new Error('Legacy task admission is not available for a device update');
|
|
28
|
+
return this.options.tasks;
|
|
29
|
+
}
|
|
25
30
|
snapshot() { return { ...this.status }; }
|
|
26
31
|
async snapshotAsync() { await this.ensureInitialized(); return this.snapshot(); }
|
|
27
32
|
async handle(command) {
|
|
@@ -70,6 +75,8 @@ export class UpdateCoordinator {
|
|
|
70
75
|
*/
|
|
71
76
|
async managementReady(release, generation) {
|
|
72
77
|
await this.ensureInitialized();
|
|
78
|
+
if (this.options.deviceAdmission)
|
|
79
|
+
return;
|
|
73
80
|
const readiness = this.options.readiness;
|
|
74
81
|
if (!release)
|
|
75
82
|
return;
|
|
@@ -149,6 +156,7 @@ export class UpdateCoordinator {
|
|
|
149
156
|
async transition(status) {
|
|
150
157
|
this.status = status;
|
|
151
158
|
await this.options.state.save(status);
|
|
159
|
+
await this.options.persistDevicePhase?.(status);
|
|
152
160
|
// After the persist, and swallowed: a logger is a witness to the update, never a participant
|
|
153
161
|
// that can fail one.
|
|
154
162
|
try {
|
|
@@ -167,16 +175,23 @@ export class UpdateCoordinator {
|
|
|
167
175
|
try {
|
|
168
176
|
await this.transition(this.base(command, 'REQUESTED'));
|
|
169
177
|
let drained = null;
|
|
170
|
-
if (
|
|
171
|
-
|
|
178
|
+
if (this.options.deviceAdmission) {
|
|
179
|
+
if (command.force)
|
|
180
|
+
throw new Error('Destructive all-device force requires a separately confirmed local action');
|
|
181
|
+
await this.transition(this.base(command, 'DRAINING'));
|
|
182
|
+
if (!await this.options.deviceAdmission.observe())
|
|
183
|
+
return this.transition(this.base(command, 'BUSY'));
|
|
184
|
+
}
|
|
185
|
+
else if (command.force) {
|
|
186
|
+
await this.legacyTasks().stopAll();
|
|
172
187
|
}
|
|
173
188
|
else {
|
|
174
189
|
// Close admission before the persisted transition can yield: no command may enter in the
|
|
175
190
|
// gap between announcing DRAINING and establishing the barrier it promises.
|
|
176
|
-
this.
|
|
191
|
+
this.legacyTasks().accepting = false;
|
|
177
192
|
await this.transition(this.base(command, 'DRAINING'));
|
|
178
193
|
drainCancellation = new AbortController();
|
|
179
|
-
drained = this.
|
|
194
|
+
drained = this.legacyTasks().waitForIdle(this.options.drainTimeoutMs ?? DEFAULT_UPDATE_DRAIN_TIMEOUT_MS, drainCancellation.signal);
|
|
180
195
|
}
|
|
181
196
|
await this.transition(this.base(command, 'DOWNLOADING'));
|
|
182
197
|
if (this.options.downloader)
|
|
@@ -187,10 +202,13 @@ export class UpdateCoordinator {
|
|
|
187
202
|
artifact = await downloadVerified(command.release, this.options.artifact);
|
|
188
203
|
}
|
|
189
204
|
await this.transition(this.base(command, 'VERIFYING'));
|
|
205
|
+
await this.options.verifyArtifact?.(artifact, command);
|
|
190
206
|
if (drained && !await drained) {
|
|
191
|
-
this.
|
|
192
|
-
return this.transition({ ...this.base(command, 'BUSY'), ...this.
|
|
207
|
+
this.legacyTasks().accepting = true;
|
|
208
|
+
return this.transition({ ...this.base(command, 'BUSY'), ...this.legacyTasks().snapshot() });
|
|
193
209
|
}
|
|
210
|
+
if (this.options.deviceAdmission && !await this.options.deviceAdmission.observe())
|
|
211
|
+
return this.transition(this.base(command, 'BUSY'));
|
|
194
212
|
// This is intentionally after a verified new archive exists but before npm mutates the
|
|
195
213
|
// installed package. A missing/invalid last-known-good archive must leave the client alone.
|
|
196
214
|
rollback = await this.options.rollback?.prepare(command) ?? null;
|
|
@@ -214,6 +232,8 @@ export class UpdateCoordinator {
|
|
|
214
232
|
// the successor loading INSTALLING and heartbeating a non-terminal phase forever, which the
|
|
215
233
|
// server never resolves and which blocks every later update command.
|
|
216
234
|
const restarting = await this.transition(this.base(command, 'RESTARTING'));
|
|
235
|
+
if (this.options.deviceAdmission && this.options.restart)
|
|
236
|
+
await this.options.deviceAdmission.handoff();
|
|
217
237
|
// Deliberately not inside the try/catch that restores the package: a service definition we
|
|
218
238
|
// failed to rewrite does not make the installed package wrong, and rolling npm back over a
|
|
219
239
|
// provisioning failure would be a much bigger hammer than the problem.
|
|
@@ -228,8 +248,8 @@ export class UpdateCoordinator {
|
|
|
228
248
|
}
|
|
229
249
|
catch (provisionError) {
|
|
230
250
|
this.options.onProvisionFailure?.(provisionError);
|
|
231
|
-
if (!command.force)
|
|
232
|
-
this.
|
|
251
|
+
if (!command.force && !this.options.deviceAdmission)
|
|
252
|
+
this.legacyTasks().accepting = true;
|
|
233
253
|
return this.transition({ ...this.base(command, 'FAILED'), error: failure(provisionError) });
|
|
234
254
|
}
|
|
235
255
|
if (!this.options.restart) {
|
|
@@ -249,19 +269,20 @@ export class UpdateCoordinator {
|
|
|
249
269
|
// SUCCEEDED is the honest phase: the install did succeed. What has not happened is the
|
|
250
270
|
// restart, which the caller is the one positioned to say — `axom update` prints it, and
|
|
251
271
|
// the server-driven case sees the new version only after the operator restarts.
|
|
252
|
-
if (!command.force)
|
|
253
|
-
this.
|
|
272
|
+
if (!command.force && !this.options.deviceAdmission)
|
|
273
|
+
this.legacyTasks().accepting = true;
|
|
254
274
|
return this.transition(this.base(command, 'SUCCEEDED'));
|
|
255
275
|
}
|
|
256
276
|
// Keeper-hosted agent sessions outlive this process: detach them first (head checkpoint, adapter untouched — no
|
|
257
277
|
// `session/cancel`, no failed turn), so the next daemon adopts them where they are (spec §5.5.5).
|
|
258
|
-
|
|
278
|
+
if (!this.options.deviceAdmission)
|
|
279
|
+
await this.legacyTasks().detachDurableSessions();
|
|
259
280
|
await this.options.restart();
|
|
260
281
|
return restarting;
|
|
261
282
|
}
|
|
262
283
|
catch (error) {
|
|
263
|
-
if (!command.force)
|
|
264
|
-
this.
|
|
284
|
+
if (!command.force && !this.options.deviceAdmission)
|
|
285
|
+
this.legacyTasks().accepting = true;
|
|
265
286
|
return this.transition({ ...this.base(command, 'FAILED'), error: failure(error) });
|
|
266
287
|
}
|
|
267
288
|
finally {
|