@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
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { DeviceAdmissionConnection } from '../desktopControl/deviceAdmission.js';
|
|
2
|
+
import { DeviceSockets } from '../desktopControl/deviceSockets.js';
|
|
3
|
+
import { DesktopSessionSupervisor } from '../desktopControl/supervisor.js';
|
|
4
|
+
import { AutomationDeviceConnections } from '../desktopControl/automationDevice.js';
|
|
5
|
+
import { randomBytes } from 'node:crypto';
|
|
6
|
+
import { bounded, NativeCommandError } from '../desktopControl/helperProcess.js';
|
|
7
|
+
export class ClosedRemoteDesktopAdapter {
|
|
8
|
+
report;
|
|
9
|
+
constructor(report) {
|
|
10
|
+
this.report = report;
|
|
11
|
+
}
|
|
12
|
+
async dispatch(frame, generation) {
|
|
13
|
+
if (frame.t === 'desktop_prepare')
|
|
14
|
+
this.report(generation, { t: 'desktop_prepare_result', commandId: frame.commandId, sessionId: frame.sessionId, state: 'failed', code: 'unavailable' });
|
|
15
|
+
else if (frame.t === 'desktop_lobby' || frame.t === 'desktop_background')
|
|
16
|
+
this.report(generation, { t: 'desktop_lobby_result', commandId: frame.commandId, lobbyId: frame.lobbyId, deviceGeneration: generation, plan: null, code: 'unavailable' });
|
|
17
|
+
}
|
|
18
|
+
async retire() {
|
|
19
|
+
throw new Error('Physical desktop isolation acknowledgement is unavailable until Computer Use integration');
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export class RegistrationDesktopAdapter {
|
|
23
|
+
admission;
|
|
24
|
+
provider;
|
|
25
|
+
viewer;
|
|
26
|
+
automation;
|
|
27
|
+
paused = false;
|
|
28
|
+
newWorkPaused = false;
|
|
29
|
+
initialized = false;
|
|
30
|
+
unavailable;
|
|
31
|
+
observerGenerations = new Set();
|
|
32
|
+
viewerClosed = false;
|
|
33
|
+
automationClosed = false;
|
|
34
|
+
cleanupStarted = false;
|
|
35
|
+
finalizers = new Map();
|
|
36
|
+
original;
|
|
37
|
+
constructor(options) {
|
|
38
|
+
this.unavailable = new ClosedRemoteDesktopAdapter(options.report);
|
|
39
|
+
this.original = { registrationId: options.registrationId, host: options.provider?.deviceAdmission };
|
|
40
|
+
this.admission = new DeviceAdmissionConnection(options.registrationId, options.owner, options.provider?.deviceAdmission, frame => options.report(frame.deviceGeneration, frame));
|
|
41
|
+
const provider = options.provider, admission = this.admission, host = this.original.host;
|
|
42
|
+
const preparationCurrent = (binding) => {
|
|
43
|
+
const original = admission.grant(binding.sessionId);
|
|
44
|
+
let begun = false;
|
|
45
|
+
return (beginEffect = false) => {
|
|
46
|
+
if (!original || this.paused || !host?.current() || !admission.current(original))
|
|
47
|
+
throw new NativeCommandError('control_lost');
|
|
48
|
+
if (!begun && this.newWorkPaused)
|
|
49
|
+
return false;
|
|
50
|
+
if (beginEffect)
|
|
51
|
+
begun = true;
|
|
52
|
+
return true;
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
if (provider && host)
|
|
56
|
+
this.provider = {
|
|
57
|
+
deviceAdmission: host, automaticDpi: provider.automaticDpi,
|
|
58
|
+
probe: async () => { if (this.paused)
|
|
59
|
+
return null; const caps = await provider.probe(); return caps && !this.paused && host.current() ? { ...caps, deviceOwnershipProtocol: 1 } : null; },
|
|
60
|
+
prepare: async (selection, binding, operationCurrent) => {
|
|
61
|
+
const deviceGrant = admission.grant(binding.sessionId);
|
|
62
|
+
if (this.paused || !deviceGrant)
|
|
63
|
+
throw new Error('control_lost');
|
|
64
|
+
const originalCurrent = operationCurrent ?? preparationCurrent(binding);
|
|
65
|
+
const current = (beginEffect = false) => {
|
|
66
|
+
if (this.paused || !host.current() || !admission.current(deviceGrant))
|
|
67
|
+
throw new NativeCommandError('control_lost');
|
|
68
|
+
return originalCurrent(beginEffect);
|
|
69
|
+
};
|
|
70
|
+
if (!current())
|
|
71
|
+
throw new NativeCommandError('busy');
|
|
72
|
+
const backend = await provider.prepare(selection, { ...binding, deviceGrant }, current);
|
|
73
|
+
try {
|
|
74
|
+
if (!current())
|
|
75
|
+
throw new NativeCommandError('busy');
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
await backend.stop();
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
return backend;
|
|
82
|
+
},
|
|
83
|
+
recoverDisplay: (binding, reserved) => {
|
|
84
|
+
const deviceGrant = admission.grant(binding.sessionId);
|
|
85
|
+
if (this.paused || !deviceGrant)
|
|
86
|
+
return Promise.reject(new Error('control_lost'));
|
|
87
|
+
return provider.recoverDisplay({ ...binding, deviceGrant }, reserved);
|
|
88
|
+
},
|
|
89
|
+
prepareAutomation: async (binding) => {
|
|
90
|
+
if (this.paused || this.newWorkPaused || !host.current() || !provider.prepareAutomation || !this.automationCurrent(binding))
|
|
91
|
+
throw new Error('unsupported');
|
|
92
|
+
const deviceGrant = admission.grant(binding.route.consumerId);
|
|
93
|
+
if (!deviceGrant) {
|
|
94
|
+
if (!host.isolateRegistration)
|
|
95
|
+
throw new Error('unsupported_observer_isolation');
|
|
96
|
+
this.observerGenerations.add(binding.route.deviceGeneration);
|
|
97
|
+
}
|
|
98
|
+
return provider.prepareAutomation({ ...binding, registrationId: options.registrationId, deviceGrant });
|
|
99
|
+
},
|
|
100
|
+
wallpaper: provider.wallpaper?.bind(provider),
|
|
101
|
+
};
|
|
102
|
+
if (this.provider)
|
|
103
|
+
this.viewer = new DesktopSessionSupervisor({ provider: this.provider,
|
|
104
|
+
sockets: new DeviceSockets(options.server, options.allowInsecureWs), report: options.report,
|
|
105
|
+
newWorkAllowed: () => !this.paused && !this.newWorkPaused,
|
|
106
|
+
preparationCurrent,
|
|
107
|
+
controllerCurrent: binding => !this.paused && !!admission.grant(binding.sessionId) });
|
|
108
|
+
this.automation = new AutomationDeviceConnections(options.server, this.provider, options.allowInsecureWs, options.stateDirectory, { registrationId: options.registrationId, current: binding => !this.paused && this.automationCurrent(binding) });
|
|
109
|
+
}
|
|
110
|
+
automationCurrent(binding) {
|
|
111
|
+
const authorization = binding.preparation?.authorization;
|
|
112
|
+
return authorization?.control === true ? !!this.admission.grant(binding.route.consumerId) :
|
|
113
|
+
authorization?.control === false && !authorization.launch && !authorization.clipboard;
|
|
114
|
+
}
|
|
115
|
+
initializeAdmission(open, held) {
|
|
116
|
+
if (this.initialized)
|
|
117
|
+
throw Error('desktop_adapter_already_initialized');
|
|
118
|
+
this.paused = !open;
|
|
119
|
+
this.newWorkPaused = held;
|
|
120
|
+
if (!open)
|
|
121
|
+
this.admission.closeAdmission();
|
|
122
|
+
if (held)
|
|
123
|
+
this.admission.holdNewAdmission();
|
|
124
|
+
else
|
|
125
|
+
this.admission.resumeNewAdmission();
|
|
126
|
+
}
|
|
127
|
+
ready(generation) { this.initialized = true; this.admission.ready(generation); if (this.paused)
|
|
128
|
+
this.admission.closeAdmission(); this.viewer?.managementReady(generation); this.automation.ready(generation); }
|
|
129
|
+
closed(generation) { this.admission.closed(generation); this.viewer?.managementClosed(generation); this.automation.retired(generation); void this.admission.isolateOriginal().catch(() => { }); }
|
|
130
|
+
async dispatch(frame, generation) {
|
|
131
|
+
if (frame.t === 'desktop_ownership')
|
|
132
|
+
return this.admission.dispatch(frame);
|
|
133
|
+
if (this.paused && frame.t !== 'desktop_cancel')
|
|
134
|
+
return;
|
|
135
|
+
if (this.viewer)
|
|
136
|
+
return this.viewer.dispatch(frame, generation);
|
|
137
|
+
return this.unavailable.dispatch(frame, generation);
|
|
138
|
+
}
|
|
139
|
+
async prepareAutomation(frame, generation) {
|
|
140
|
+
if (this.paused || this.newWorkPaused)
|
|
141
|
+
throw new Error('control_lost');
|
|
142
|
+
await this.automation.prepare(frame.grant, frame.binding, generation);
|
|
143
|
+
}
|
|
144
|
+
async closeAdmission() { this.paused = true; this.admission.closeAdmission(); }
|
|
145
|
+
async holdNewAdmission() { this.newWorkPaused = true; this.admission.holdNewAdmission(); }
|
|
146
|
+
resumeNewAdmission() { this.newWorkPaused = false; this.admission.resumeNewAdmission(); }
|
|
147
|
+
async isolateOriginal() {
|
|
148
|
+
this.cleanupStarted = true;
|
|
149
|
+
const operations = [() => this.admission.isolateOriginal()];
|
|
150
|
+
const host = this.original.host;
|
|
151
|
+
for (const generation of [...this.observerGenerations])
|
|
152
|
+
operations.push(() => this.finalize(`observer:${generation}`, async () => {
|
|
153
|
+
if (!host?.isolateRegistration)
|
|
154
|
+
throw new Error('observer_isolation_unconfirmed');
|
|
155
|
+
const commandId = randomBytes(16).toString('hex');
|
|
156
|
+
const receipt = await host.isolateRegistration(commandId, this.original.registrationId, generation);
|
|
157
|
+
if (receipt.commandId !== commandId || receipt.registrationId !== this.original.registrationId || receipt.deviceGeneration !== generation ||
|
|
158
|
+
receipt.hostLifetimeId !== host.hostLifetimeId || receipt.physical !== 'completed' || receipt.admission !== 'revoked')
|
|
159
|
+
throw new Error('observer_isolation_unconfirmed');
|
|
160
|
+
this.observerGenerations.delete(generation);
|
|
161
|
+
}));
|
|
162
|
+
if (!this.viewerClosed)
|
|
163
|
+
operations.push(() => this.finalize('viewer', async () => { await this.viewer?.close(); this.viewerClosed = true; }));
|
|
164
|
+
if (!this.automationClosed)
|
|
165
|
+
operations.push(() => this.finalize('automation', async () => { this.automation.close(); this.automationClosed = true; }));
|
|
166
|
+
const outcomes = await Promise.allSettled(operations.map(operation => Promise.resolve().then(operation)));
|
|
167
|
+
const failures = outcomes.flatMap(outcome => outcome.status === 'rejected' ? [outcome.reason] : []);
|
|
168
|
+
if (failures.length)
|
|
169
|
+
throw new AggregateError(failures, 'Original desktop cleanup remains unconfirmed');
|
|
170
|
+
}
|
|
171
|
+
finalize(key, operation) {
|
|
172
|
+
const pending = this.finalizers.get(key);
|
|
173
|
+
if (pending)
|
|
174
|
+
return bounded(pending, 15_000);
|
|
175
|
+
const result = operation().finally(() => { this.finalizers.delete(key); });
|
|
176
|
+
this.finalizers.set(key, result);
|
|
177
|
+
return bounded(result, 15_000);
|
|
178
|
+
}
|
|
179
|
+
resumeAdmission() { this.paused = false; this.admission.resumeAdmission(); }
|
|
180
|
+
activity() {
|
|
181
|
+
const admission = this.admission.activity();
|
|
182
|
+
const transports = this.cleanupStarted ? Number(!this.viewerClosed) + Number(!this.automationClosed) : 0;
|
|
183
|
+
return { known: admission.known && this.observerGenerations.size === 0 && transports === 0,
|
|
184
|
+
consumers: Math.max(admission.consumers, (this.viewer?.activeCount ?? 0) + this.automation.activeCount, this.observerGenerations.size, transports) };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { assertRemovalNamespace } from '../registrations/migration.js';
|
|
2
|
+
import { LocalControlError } from '../control/protocol.js';
|
|
3
|
+
const settlements = new WeakMap();
|
|
4
|
+
/** Prevent new work during removal. Only --force terminates existing work. */
|
|
5
|
+
export class TargetRemovalActivity {
|
|
6
|
+
options;
|
|
7
|
+
preparation = Object.freeze({});
|
|
8
|
+
phase = 'observing';
|
|
9
|
+
force = false;
|
|
10
|
+
desktopClosed = false;
|
|
11
|
+
settlement;
|
|
12
|
+
settlementNamespace;
|
|
13
|
+
operation;
|
|
14
|
+
constructor(options) {
|
|
15
|
+
this.options = options;
|
|
16
|
+
}
|
|
17
|
+
validate() {
|
|
18
|
+
this.options.validate();
|
|
19
|
+
if (this.phase === 'disposed' || this.phase === 'released')
|
|
20
|
+
throw new LocalControlError('BINDING_CONFLICT', 'Target preparation has finished');
|
|
21
|
+
}
|
|
22
|
+
namespace(namespace) {
|
|
23
|
+
assertRemovalNamespace(namespace);
|
|
24
|
+
const registration = this.options.registration;
|
|
25
|
+
if (namespace.registrationId !== registration.id || namespace.revision !== registration.revision || namespace.dataDirectory !== registration.dataDirectory || namespace.configDir !== registration.keeperConfigDirectory)
|
|
26
|
+
throw new LocalControlError('BINDING_CONFLICT', 'Target registration changed');
|
|
27
|
+
}
|
|
28
|
+
async exclusive(work) {
|
|
29
|
+
if (this.operation)
|
|
30
|
+
throw new LocalControlError('BUSY', 'Target removal is already running');
|
|
31
|
+
const operation = work();
|
|
32
|
+
this.operation = operation;
|
|
33
|
+
try {
|
|
34
|
+
return await operation;
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
this.operation = undefined;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async sample() {
|
|
41
|
+
this.validate();
|
|
42
|
+
const before = this.options.tasks.observeTargetPending();
|
|
43
|
+
const activity = await this.options.activity();
|
|
44
|
+
this.validate();
|
|
45
|
+
const after = this.options.tasks.observeTargetPending();
|
|
46
|
+
const background = this.options.background();
|
|
47
|
+
const desktop = this.options.desktopActivity?.() ?? { known: true, consumers: 0 };
|
|
48
|
+
const busy = before.pending + after.pending + activity.agents + activity.terminals + activity.commands + activity.transfers + activity.desktopConsumers + desktop.consumers + background.pending > 0;
|
|
49
|
+
const known = activity.known && before.known && after.known && background.known && desktop.known;
|
|
50
|
+
return { status: busy ? 'busy' : known ? 'candidate' : 'unknown', diagnostics: known ? [] : ['Target activity is not available yet'] };
|
|
51
|
+
}
|
|
52
|
+
observe() { return this.exclusive(() => this.sample()); }
|
|
53
|
+
seal(mode) {
|
|
54
|
+
return this.exclusive(async () => {
|
|
55
|
+
this.validate();
|
|
56
|
+
if (this.phase === 'sealed') {
|
|
57
|
+
if (mode === 'force')
|
|
58
|
+
this.force = true;
|
|
59
|
+
if (!this.desktopClosed) {
|
|
60
|
+
await this.options.closeDesktopAdmission?.();
|
|
61
|
+
this.desktopClosed = true;
|
|
62
|
+
}
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (this.phase !== 'observing')
|
|
66
|
+
throw new LocalControlError('BINDING_CONFLICT', 'Target is already being removed');
|
|
67
|
+
if (!mode) {
|
|
68
|
+
const observation = await this.sample();
|
|
69
|
+
if (observation.status !== 'candidate')
|
|
70
|
+
throw new LocalControlError(observation.status === 'busy' ? 'BUSY' : 'OUTCOME_UNKNOWN', 'Target still has work; use --force to terminate it');
|
|
71
|
+
}
|
|
72
|
+
this.force = mode === 'force';
|
|
73
|
+
this.options.beginFinal();
|
|
74
|
+
this.phase = 'sealed';
|
|
75
|
+
await this.options.closeDesktopAdmission?.();
|
|
76
|
+
this.desktopClosed = true;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
async prepareTermination() { if (this.phase === 'observing')
|
|
80
|
+
await this.seal('force');
|
|
81
|
+
else {
|
|
82
|
+
this.validate();
|
|
83
|
+
this.force = true;
|
|
84
|
+
} }
|
|
85
|
+
settle(namespace) {
|
|
86
|
+
return this.exclusive(async () => {
|
|
87
|
+
this.validate();
|
|
88
|
+
this.namespace(namespace);
|
|
89
|
+
if (this.phase !== 'sealed' && this.phase !== 'settled')
|
|
90
|
+
throw new LocalControlError('BINDING_CONFLICT', 'Target must stop accepting work before removal');
|
|
91
|
+
if (this.phase !== 'settled') {
|
|
92
|
+
if (!this.desktopClosed) {
|
|
93
|
+
await this.options.closeDesktopAdmission?.();
|
|
94
|
+
this.desktopClosed = true;
|
|
95
|
+
}
|
|
96
|
+
if (!this.force) {
|
|
97
|
+
const observation = await this.sample();
|
|
98
|
+
if (observation.status !== 'candidate')
|
|
99
|
+
throw new LocalControlError(observation.status === 'busy' ? 'BUSY' : 'OUTCOME_UNKNOWN', 'Target work has not finished');
|
|
100
|
+
}
|
|
101
|
+
if (this.force) {
|
|
102
|
+
this.options.tasks.closeAgentWorkLifetime();
|
|
103
|
+
await Promise.all([this.options.tasks.stopAll(), this.options.localTransport.dispose(), this.options.isolateDesktop?.()]);
|
|
104
|
+
this.validate();
|
|
105
|
+
this.namespace(namespace);
|
|
106
|
+
}
|
|
107
|
+
// Stop existing keepers first so accepted prompts/opens can observe their process exit.
|
|
108
|
+
const stopKeepers = async () => {
|
|
109
|
+
for (const launcher of this.options.launchers)
|
|
110
|
+
for (const kind of ['acp', 'terminal']) {
|
|
111
|
+
const generations = await launcher.liveGenerations(kind);
|
|
112
|
+
this.validate();
|
|
113
|
+
this.namespace(namespace);
|
|
114
|
+
for (const generation of generations) {
|
|
115
|
+
const result = await launcher.stop(kind, generation, this.force ? 'force' : 'graceful');
|
|
116
|
+
this.validate();
|
|
117
|
+
this.namespace(namespace);
|
|
118
|
+
if (!result.stopped)
|
|
119
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', result.reason ?? 'Target keeper has not stopped');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
await stopKeepers();
|
|
124
|
+
if (this.force) {
|
|
125
|
+
// Main closes an already-starting adapter when its open returns after stopAll. Wait for that
|
|
126
|
+
// handler before completing removal, then stop any keeper its delayed startup launched.
|
|
127
|
+
if (!await this.options.tasks.waitForAcceptedAgentWork(5_000))
|
|
128
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'Accepted agent work has not finished stopping');
|
|
129
|
+
this.validate();
|
|
130
|
+
this.namespace(namespace);
|
|
131
|
+
await stopKeepers();
|
|
132
|
+
}
|
|
133
|
+
await this.options.filesHost.stop();
|
|
134
|
+
this.validate();
|
|
135
|
+
this.namespace(namespace);
|
|
136
|
+
this.phase = 'settled';
|
|
137
|
+
}
|
|
138
|
+
this.settlementNamespace = namespace;
|
|
139
|
+
const token = this.settlement ??= Object.freeze({});
|
|
140
|
+
settlements.set(token, this);
|
|
141
|
+
return token;
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
assertSettlement(token, lifetime, namespace) {
|
|
145
|
+
this.validate();
|
|
146
|
+
this.namespace(namespace);
|
|
147
|
+
if (settlements.get(token) !== this || token !== this.settlement || lifetime !== this.options.lifetime || this.phase !== 'settled' || namespace !== this.settlementNamespace)
|
|
148
|
+
throw new LocalControlError('BINDING_CONFLICT', 'Invalid target removal settlement');
|
|
149
|
+
}
|
|
150
|
+
consume(token, lifetime, namespace) { this.assertSettlement(token, lifetime, namespace); this.phase = 'disposing'; }
|
|
151
|
+
continueDisposition(token, lifetime, namespace) {
|
|
152
|
+
if (this.phase !== 'disposing')
|
|
153
|
+
return false;
|
|
154
|
+
this.namespace(namespace);
|
|
155
|
+
if (token !== this.settlement || settlements.get(token) !== this || lifetime !== this.options.lifetime)
|
|
156
|
+
throw new LocalControlError('BINDING_CONFLICT', 'Invalid target removal continuation');
|
|
157
|
+
this.settlementNamespace = namespace;
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
async prepareDisposition(lifetime, namespace) {
|
|
161
|
+
if (lifetime !== this.options.lifetime)
|
|
162
|
+
throw new LocalControlError('BINDING_CONFLICT', 'Target runtime changed');
|
|
163
|
+
if (this.settlement && this.continueDisposition(this.settlement, lifetime, namespace))
|
|
164
|
+
return this.settlement;
|
|
165
|
+
return this.settle(namespace);
|
|
166
|
+
}
|
|
167
|
+
disposed() { if (this.phase !== 'disposing')
|
|
168
|
+
throw new LocalControlError('BINDING_CONFLICT', 'Target removal was not prepared'); this.phase = 'disposed'; }
|
|
169
|
+
async release() {
|
|
170
|
+
if (this.phase === 'released')
|
|
171
|
+
return;
|
|
172
|
+
this.validate();
|
|
173
|
+
if (this.phase !== 'observing' || this.operation)
|
|
174
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'Target removal has already started');
|
|
175
|
+
await this.options.restore();
|
|
176
|
+
this.phase = 'released';
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
export function consumeTargetResourceSettlement(token, lifetime, namespace) {
|
|
180
|
+
const activity = settlements.get(token);
|
|
181
|
+
if (!activity)
|
|
182
|
+
throw new LocalControlError('BINDING_CONFLICT', 'Target removal settlement is unavailable');
|
|
183
|
+
activity.consume(token, lifetime, namespace);
|
|
184
|
+
}
|
|
@@ -5,6 +5,13 @@ import path from 'node:path';
|
|
|
5
5
|
import { launchAgentPath, renderLaunchAgent } from './launchd.js';
|
|
6
6
|
import { renderSystemdUnit, systemdUnitPath } from './systemd.js';
|
|
7
7
|
import { assertSafeServiceContext, SERVICE_LABEL } from './types.js';
|
|
8
|
+
import { assertDeviceUpdateTransitionSafe } from '../control/daemonIdentity.js';
|
|
9
|
+
import { inspectRuntime, inspectDeviceRuntime } from '../control/runtimeClient.js';
|
|
10
|
+
import { LocalControlError } from '../control/protocol.js';
|
|
11
|
+
import { captureStartupPackageEvidence } from '../update/deviceHealth.js';
|
|
12
|
+
import { readDeviceBootstrapRecovery, readPrivateUpdateBytes, updateTransactionKey } from '../update/recoveryPlan.js';
|
|
13
|
+
import { launchWatchdogProcess } from '../update/watchdogProcess.js';
|
|
14
|
+
import { ensureCompatibleWatchdog, needsWatchdogCompatibility } from '../update/deviceCompatibility.js';
|
|
8
15
|
async function runProcess(command, args) {
|
|
9
16
|
return new Promise((resolve, reject) => {
|
|
10
17
|
const child = spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
@@ -58,6 +65,22 @@ async function assertWritable(directory) {
|
|
|
58
65
|
}
|
|
59
66
|
const defaultDeps = {
|
|
60
67
|
run: runProcess,
|
|
68
|
+
readLaunchAgent: async (file) => {
|
|
69
|
+
const handle = await fs.open(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
70
|
+
try {
|
|
71
|
+
const stat = await handle.stat();
|
|
72
|
+
if (!stat.isFile() || stat.size > 65536)
|
|
73
|
+
throw new Error('Invalid LaunchAgent');
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
await handle.close();
|
|
77
|
+
}
|
|
78
|
+
const result = await runProcess('/usr/bin/plutil', ['-convert', 'json', '-o', '-', file]);
|
|
79
|
+
const value = JSON.parse(result.stdout);
|
|
80
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
81
|
+
throw new Error('Invalid LaunchAgent');
|
|
82
|
+
return value;
|
|
83
|
+
},
|
|
61
84
|
writeTemp,
|
|
62
85
|
assertWritable,
|
|
63
86
|
ensureDirectory: async (directory) => { await fs.mkdir(directory, { recursive: true }); },
|
|
@@ -102,6 +125,85 @@ export function createServiceManager(context, overrides = {}) {
|
|
|
102
125
|
assertSafeServiceContext(context);
|
|
103
126
|
const deps = { ...defaultDeps, ...overrides };
|
|
104
127
|
const target = context.platform === 'darwin' ? launchAgentPath(context.homeDir) : systemdUnitPath(context.homeDir);
|
|
128
|
+
const deviceAdmission = async (install = false) => {
|
|
129
|
+
let device = false;
|
|
130
|
+
for (const name of ['device.json', 'legacy-migration.json'])
|
|
131
|
+
try {
|
|
132
|
+
await fs.lstat(path.join(context.configDir, name));
|
|
133
|
+
device = true;
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
if (error.code !== 'ENOENT')
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
if (!device)
|
|
140
|
+
return;
|
|
141
|
+
let startup;
|
|
142
|
+
if (install) {
|
|
143
|
+
let transaction = false;
|
|
144
|
+
try {
|
|
145
|
+
await fs.lstat(path.join(context.configDir, 'update/device-transaction.json'));
|
|
146
|
+
transaction = true;
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
if (error.code !== 'ENOENT')
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
if (transaction) {
|
|
153
|
+
const packageRoot = path.dirname(path.dirname(context.cliPath));
|
|
154
|
+
if (context.cliPath !== path.join(packageRoot, 'dist/cli.js'))
|
|
155
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'The update service must execute its exact installed dist/cli.js');
|
|
156
|
+
const metadata = JSON.parse(await fs.readFile(path.join(packageRoot, 'package.json'), 'utf8'));
|
|
157
|
+
if (typeof metadata.version !== 'string')
|
|
158
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'The installed update package version cannot be verified');
|
|
159
|
+
startup = await captureStartupPackageEvidence(packageRoot, metadata.version);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// Revalidate the actual installed package here too: the controller's earlier admission must
|
|
163
|
+
// not become an unqualified legacy-state check when the OS service manager is reached.
|
|
164
|
+
await assertDeviceUpdateTransitionSafe(context.configDir, startup);
|
|
165
|
+
try {
|
|
166
|
+
await inspectDeviceRuntime(context.configDir);
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
if ((await inspectRuntime(context.configDir)).state !== 'stopped')
|
|
170
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'Verify and stop the predecessor daemon before changing the device service');
|
|
171
|
+
}
|
|
172
|
+
if (install && startup) {
|
|
173
|
+
const { deviceTransactionSchema } = await import('../update/deviceCoordinator.js');
|
|
174
|
+
const transaction = deviceTransactionSchema.parse(JSON.parse((await readPrivateUpdateBytes(path.join(context.configDir, 'update/device-transaction.json'), 33_554_432)).toString()));
|
|
175
|
+
if (transaction.phase === 'RESTARTING' && !transaction.installedRollback) {
|
|
176
|
+
const recovery = await readDeviceBootstrapRecovery(context.configDir, startup);
|
|
177
|
+
if (recovery?.plan) {
|
|
178
|
+
if (needsWatchdogCompatibility(recovery.plan))
|
|
179
|
+
await ensureCompatibleWatchdog(context, recovery.plan, deps.run);
|
|
180
|
+
else if (context.platform === 'linux')
|
|
181
|
+
await launchWatchdogProcess({ nodePath: context.nodePath, program: recovery.plan.program.path, plan: path.join(context.configDir, 'update', `watchdog-${updateTransactionKey(recovery.plan.commandId)}.json`), lifecycle: 'managed', logPath: path.join(context.configDir, 'update/watchdog.log') }, { platform: context.platform, runSystemd: deps.run });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (install || !await deps.pathExists(target))
|
|
186
|
+
return;
|
|
187
|
+
let compatible = false;
|
|
188
|
+
if (context.platform === 'darwin') {
|
|
189
|
+
const definition = await deps.readLaunchAgent(target);
|
|
190
|
+
compatible = definition.EnvironmentVariables !== undefined && definition.EnvironmentVariables.AXOM_WORKSTATION_RUNTIME === 'device-v2';
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
const handle = await fs.open(target, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
|
|
194
|
+
try {
|
|
195
|
+
const stat = await handle.stat();
|
|
196
|
+
if (!stat.isFile() || stat.size > 65_536 || (process.getuid && stat.uid !== process.getuid()))
|
|
197
|
+
throw Error('Unsafe device service definition');
|
|
198
|
+
compatible = (await handle.readFile('utf8')).split('\n').includes('Environment=AXOM_WORKSTATION_RUNTIME=device-v2');
|
|
199
|
+
}
|
|
200
|
+
finally {
|
|
201
|
+
await handle.close();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (!compatible)
|
|
205
|
+
throw new LocalControlError('UNSUPPORTED_PROTOCOL', 'Legacy workstation service definition: run axom workstation install with this compatible CLI before starting the device owner');
|
|
206
|
+
};
|
|
105
207
|
const contents = context.platform === 'darwin' ? renderLaunchAgent(context) : renderSystemdUnit(context);
|
|
106
208
|
const launchdDomain = `gui/${context.userId}/${SERVICE_LABEL}`;
|
|
107
209
|
// `gui/<uid>` exists only while someone is logged into the GUI. Switching this agent to
|
|
@@ -219,24 +321,38 @@ export function createServiceManager(context, overrides = {}) {
|
|
|
219
321
|
// durable LaunchAgent plist. This keeps explicit stop meaningful across both service managers.
|
|
220
322
|
if (action === 'stop')
|
|
221
323
|
return deps.run('launchctl', ['bootout', launchdDomain]);
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
324
|
+
return launchdStart();
|
|
325
|
+
};
|
|
326
|
+
const enabled = async (plistDisabled = false) => {
|
|
327
|
+
if (context.platform === 'darwin') {
|
|
328
|
+
const result = await deps.run('launchctl', ['print-disabled', `gui/${context.userId}`]);
|
|
329
|
+
if (!result.stdout.includes('{') || !result.stdout.includes('}'))
|
|
330
|
+
throw new Error('Cannot determine launchd enablement');
|
|
331
|
+
const line = result.stdout.split('\n').find(line => line.includes(`"${SERVICE_LABEL}"`));
|
|
332
|
+
if (line) {
|
|
333
|
+
const override = /=>\s*(true|false|enabled|disabled)\b/.exec(line);
|
|
334
|
+
if (!override)
|
|
335
|
+
throw new Error('Cannot determine launchd enablement');
|
|
336
|
+
return override[1] === 'true' || override[1] === 'disabled' ? false : true;
|
|
231
337
|
}
|
|
232
|
-
return
|
|
338
|
+
return !plistDisabled;
|
|
339
|
+
}
|
|
340
|
+
try {
|
|
341
|
+
return (await deps.run('systemctl', ['--user', 'is-enabled', `${SERVICE_LABEL}.service`])).stdout.trim() === 'enabled';
|
|
342
|
+
}
|
|
343
|
+
catch (error) {
|
|
344
|
+
if (/disabled|not-found/.test(String(error)))
|
|
345
|
+
return false;
|
|
346
|
+
throw error;
|
|
233
347
|
}
|
|
234
|
-
return launchdStart();
|
|
235
348
|
};
|
|
236
349
|
// Extracted so `start()` can reuse it: reinstalling is the same work whether triggered
|
|
237
350
|
// explicitly (`axom workstation install`) or as `start()`'s self-heal for a missing definition
|
|
238
351
|
// file. Both platform branches leave the service running, so callers need no separate start.
|
|
239
352
|
const performInstall = async (_options = {}) => {
|
|
353
|
+
const present = await deps.pathExists(target);
|
|
354
|
+
const plistDisabled = context.platform === 'darwin' && present && (await deps.readLaunchAgent(target)).Disabled === true;
|
|
355
|
+
const preserveDisabled = (context.platform === 'darwin' || present) && !await enabled(plistDisabled);
|
|
240
356
|
await deps.assertWritable(context.npmPrefix);
|
|
241
357
|
await deps.ensureDirectory(path.dirname(target));
|
|
242
358
|
const temp = await deps.writeTemp(contents);
|
|
@@ -246,7 +362,10 @@ export function createServiceManager(context, overrides = {}) {
|
|
|
246
362
|
await deps.run('mv', [targetTemp, target]);
|
|
247
363
|
if (context.platform === 'linux') {
|
|
248
364
|
await deps.run('systemctl', ['--user', 'daemon-reload']);
|
|
249
|
-
|
|
365
|
+
if (!preserveDisabled)
|
|
366
|
+
await deps.run('systemctl', ['--user', 'enable', '--now', `${SERVICE_LABEL}.service`]);
|
|
367
|
+
if (!preserveDisabled && _options.restartRunning)
|
|
368
|
+
await deps.run('systemctl', ['--user', 'restart', `${SERVICE_LABEL}.service`]);
|
|
250
369
|
}
|
|
251
370
|
else {
|
|
252
371
|
// Reload an existing service before bootstrapping the replacement into the GUI domain.
|
|
@@ -255,7 +374,8 @@ export function createServiceManager(context, overrides = {}) {
|
|
|
255
374
|
// bounded probe/bootstrap path so Bootstrap failed: 5 is confirmed then kickstarted, and
|
|
256
375
|
// so a launchd that never releases the job is reported instead of leaving the machine
|
|
257
376
|
// with the bootout applied and nothing bootstrapped in its place.
|
|
258
|
-
|
|
377
|
+
if (!preserveDisabled)
|
|
378
|
+
await launchdStart();
|
|
259
379
|
}
|
|
260
380
|
}
|
|
261
381
|
finally {
|
|
@@ -264,6 +384,7 @@ export function createServiceManager(context, overrides = {}) {
|
|
|
264
384
|
};
|
|
265
385
|
return {
|
|
266
386
|
async install(options = {}) {
|
|
387
|
+
await deviceAdmission(true);
|
|
267
388
|
await withGuiSessionHint(() => performInstall(options));
|
|
268
389
|
},
|
|
269
390
|
/**
|
|
@@ -302,14 +423,76 @@ export function createServiceManager(context, overrides = {}) {
|
|
|
302
423
|
* longer on disk and surface launchd/systemd's raw error; reinstall first instead.
|
|
303
424
|
*/
|
|
304
425
|
async start() {
|
|
426
|
+
await deviceAdmission();
|
|
305
427
|
await withGuiSessionHint(async () => {
|
|
428
|
+
// An explicit Start is consent to re-enable; install/update alone preserves OFF.
|
|
429
|
+
if (context.platform === 'darwin')
|
|
430
|
+
await deps.run('launchctl', ['enable', launchdDomain]);
|
|
431
|
+
else
|
|
432
|
+
await deps.run('systemctl', ['--user', 'enable', `${SERVICE_LABEL}.service`]);
|
|
306
433
|
if (!(await deps.pathExists(target)))
|
|
307
434
|
await performInstall({});
|
|
308
435
|
await lifecycle('start');
|
|
309
436
|
});
|
|
310
437
|
},
|
|
311
438
|
async stop() { await withGuiSessionHint(() => lifecycle('stop')); },
|
|
312
|
-
async restart() { await withGuiSessionHint(() => lifecycle('restart')); },
|
|
439
|
+
async restart() { await deviceAdmission(); await withGuiSessionHint(() => lifecycle('restart')); },
|
|
440
|
+
async setEnabled(value) {
|
|
441
|
+
if (value)
|
|
442
|
+
await deviceAdmission();
|
|
443
|
+
await withGuiSessionHint(async () => {
|
|
444
|
+
if (context.platform === 'darwin') {
|
|
445
|
+
await deps.run('launchctl', [value ? 'enable' : 'disable', launchdDomain]);
|
|
446
|
+
if (value) {
|
|
447
|
+
if (!await deps.pathExists(target))
|
|
448
|
+
await performInstall({});
|
|
449
|
+
await launchdStart();
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
try {
|
|
453
|
+
await lifecycle('stop');
|
|
454
|
+
}
|
|
455
|
+
catch (error) {
|
|
456
|
+
if (!launchdAbsent(error))
|
|
457
|
+
throw error;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
else
|
|
462
|
+
await deps.run('systemctl', ['--user', value ? 'enable' : 'disable', '--now', `${SERVICE_LABEL}.service`]);
|
|
463
|
+
});
|
|
464
|
+
},
|
|
465
|
+
async enablement() {
|
|
466
|
+
const installed = await deps.pathExists(target);
|
|
467
|
+
if (!installed)
|
|
468
|
+
return { installed: false, enabled: false, loaded: false, running: false };
|
|
469
|
+
let value;
|
|
470
|
+
try {
|
|
471
|
+
let disabled = false;
|
|
472
|
+
if (context.platform === 'darwin') {
|
|
473
|
+
const plist = await deps.readLaunchAgent(target);
|
|
474
|
+
const args = plist.ProgramArguments;
|
|
475
|
+
const environment = plist.EnvironmentVariables;
|
|
476
|
+
if (plist.Label !== SERVICE_LABEL || !Array.isArray(args) || args.length !== 3 || args[0] !== context.nodePath || args[1] !== context.cliPath || args[2] !== '__daemon' || environment?.AXOM_WORKSTATION_CONFIG_DIR !== context.configDir) {
|
|
477
|
+
return { installed, enabled: null, loaded: false, running: false, reason: 'service_identity_mismatch' };
|
|
478
|
+
}
|
|
479
|
+
disabled = plist.Disabled === true;
|
|
480
|
+
}
|
|
481
|
+
value = await enabled(disabled);
|
|
482
|
+
}
|
|
483
|
+
catch {
|
|
484
|
+
return { installed, enabled: null, loaded: false, running: false, reason: 'service_state_unknown' };
|
|
485
|
+
}
|
|
486
|
+
try {
|
|
487
|
+
const result = await lifecycle('status');
|
|
488
|
+
return { installed, enabled: value, loaded: true, running: context.platform === 'darwin' ? launchdIsRunning(result.stdout) : true };
|
|
489
|
+
}
|
|
490
|
+
catch (error) {
|
|
491
|
+
if (launchdAbsent(error) || /inactive|not loaded|not-found/.test(String(error)))
|
|
492
|
+
return { installed, enabled: value, loaded: false, running: false };
|
|
493
|
+
return { installed, enabled: value, loaded: false, running: false, reason: 'runtime_state_unknown' };
|
|
494
|
+
}
|
|
495
|
+
},
|
|
313
496
|
async status() {
|
|
314
497
|
try {
|
|
315
498
|
const result = await lifecycle('status');
|
|
@@ -28,9 +28,11 @@ export function renderLaunchAgent(context) {
|
|
|
28
28
|
\t<dict>
|
|
29
29
|
${keyValue('HOME', context.homeDir)}
|
|
30
30
|
${keyValue('AXOM_WORKSTATION_CONFIG_DIR', context.configDir)}
|
|
31
|
+
${keyValue('AXOM_WORKSTATION_RUNTIME', 'device-v2')}
|
|
31
32
|
${keyValue('AXOM_WORKSTATION_NPM_PATH', context.npmPath)}
|
|
32
33
|
${keyValue('AXOM_WORKSTATION_NPM_PREFIX', context.npmPrefix)}
|
|
33
34
|
${keyValue('PATH', context.pathEnv)}
|
|
35
|
+
${context.npmConfigPrefix ? keyValue('NPM_CONFIG_PREFIX', context.npmConfigPrefix) : ''}
|
|
34
36
|
\t</dict>
|
|
35
37
|
\t<key>StandardOutPath</key>
|
|
36
38
|
\t<string>${xml(`${context.configDir}/daemon.log`)}</string>
|