@platless/axom-cli 0.6.36 → 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 +7 -1
- package/dist/axom-patch/cli/README.md +31 -2
- package/dist/axom-patch/cli/account-limits/generate.py +2 -2
- package/dist/axom-patch/cli/claude-replay-lifecycle.json +16 -0
- package/dist/axom-patch/cli/codex-terminal-output.json +2 -2
- package/dist/axom-patch/cli/probes/claude-replay-lifecycle.mjs +31 -0
- package/dist/axom-patch/cli/probes/codex-terminal-output.mjs +1 -1
- 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 +137 -41
- package/dist/axom-patch/cli/qoder/releases/1.1.75.json +5 -88
- package/dist/axom-patch/cli/qoder-native-acp.json +23 -23
- package/dist/axom-patch/cli/runtime.json +3 -1
- 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 +7 -7
- package/dist/axom-patch/cli/zcode-env-account.json +58 -0
- package/dist/axom-patch/cli/zcode-models.json +9 -9
- package/dist/axom-patch/cli/zcode-subagent-subwork.json +10 -10
- package/dist/cli.js +9 -11
- package/dist/commands/auth.js +4 -1
- package/dist/commands/update.js +68 -7
- package/dist/commands/workstation.js +94 -223
- 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 +2 -2
- package/dist/keepers/terminal/keeper.mjs +1089 -673
- package/dist/keepers/terminal/manifest.json +4 -4
- package/dist/native/darwin-x64/axom-quic +0 -0
- package/dist/workstation/acp/acpAgents.js +11 -4
- package/dist/workstation/acp/outbox.js +12 -5
- package/dist/workstation/acp/permission.js +2 -2
- package/dist/workstation/acp/processor/checkpoint.js +4 -3
- package/dist/workstation/acp/processor/control.js +2 -2
- package/dist/workstation/acp/processor/jsonrpc.js +8 -0
- package/dist/workstation/acp/processor/session.js +3 -2
- package/dist/workstation/acp/processor/state.js +1 -0
- package/dist/workstation/acp/processor/transport/local.js +19 -3
- package/dist/workstation/acp/processor/updates.js +25 -0
- package/dist/workstation/acp/processorSession.js +12 -1
- package/dist/workstation/acp/registry/catalog.js +6 -6
- package/dist/workstation/acp/test/keeperWorld.js +4 -3
- package/dist/workstation/agentSessions.js +76 -23
- package/dist/workstation/channels/agent.js +9 -1
- package/dist/workstation/channels/computer.js +27 -4
- package/dist/workstation/channels/management.js +36 -5
- package/dist/workstation/config.js +18 -6
- package/dist/workstation/control/bindingStore.js +4 -0
- package/dist/workstation/control/commands.js +1 -0
- package/dist/workstation/control/daemonIdentity.js +90 -0
- package/dist/workstation/control/deviceCommands.js +191 -0
- package/dist/workstation/control/deviceProtocol.js +68 -0
- package/dist/workstation/control/locks.js +51 -4
- package/dist/workstation/control/rootFormat.js +16 -0
- package/dist/workstation/control/runtimeClient.js +60 -5
- package/dist/workstation/control/runtimeHost.js +96 -9
- package/dist/workstation/control/serverIdentity.js +2 -2
- package/dist/workstation/control/updateTransition.js +91 -0
- package/dist/workstation/daemon.js +262 -360
- package/dist/workstation/daemonctl.js +150 -12
- 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/automationPreparation.js +1 -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 +2 -1
- package/dist/workstation/desktopControl/deviceAdmission.js +229 -0
- package/dist/workstation/desktopControl/deviceOwnership.js +130 -0
- package/dist/workstation/desktopControl/deviceSockets.js +2 -2
- package/dist/workstation/desktopControl/helperProcess.js +250 -1
- package/dist/workstation/desktopControl/hostGrant.js +65 -0
- package/dist/workstation/desktopControl/lobby.js +4 -2
- package/dist/workstation/desktopControl/macProvider.js +41 -40
- package/dist/workstation/desktopControl/nativeDeviceAdmission.js +207 -0
- package/dist/workstation/desktopControl/nativeProvider.js +31 -5
- package/dist/workstation/desktopControl/ownershipProtocol.js +31 -0
- package/dist/workstation/desktopControl/protocol.js +15 -6
- package/dist/workstation/desktopControl/supervisor.js +110 -17
- package/dist/workstation/desktopControl/wallpaper.js +26 -6
- package/dist/workstation/exec/policy.js +5 -0
- package/dist/workstation/files/client.js +10 -1
- package/dist/workstation/files/host.js +9 -2
- 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/client.js +2 -2
- package/dist/workstation/keepers/kit/launcher.js +80 -4
- 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/oauth/deviceAuthorization.js +31 -17
- package/dist/workstation/protocol/management.js +23 -4
- package/dist/workstation/rd/artifact.js +4 -3
- package/dist/workstation/rd/control.js +3 -5
- package/dist/workstation/rd/host.js +213 -0
- package/dist/workstation/rd/migrationReadiness.js +46 -0
- package/dist/workstation/rd/provider.js +44 -10
- package/dist/workstation/rd/state.js +14 -2
- package/dist/workstation/rd/update.js +5 -8
- package/dist/workstation/rd/upkeep.js +44 -2
- 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 +93 -1
- package/dist/workstation/service/launchd.js +1 -0
- package/dist/workstation/service/systemd.js +1 -0
- package/dist/workstation/systemMetrics.js +3 -0
- 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/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 +80 -10
- package/dist/workstation/update/legacyBridge.js +414 -0
- package/dist/workstation/update/legacyOwner.js +39 -0
- package/dist/workstation/update/npmInstaller.js +2 -2
- package/dist/workstation/update/privateDurability.js +61 -0
- package/dist/workstation/update/recoveryPlan.js +124 -0
- package/dist/workstation/update/runUpdate.js +7 -1
- package/dist/workstation/update/taskRegistry.js +161 -3
- package/dist/workstation/update/watchdog.js +27427 -14410
- package/dist/workstation/update/watchdogProcess.js +72 -0
- package/package.json +7 -3
- package/dist/native/darwin-arm64/axom-quic +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'] });
|
|
@@ -118,6 +125,85 @@ export function createServiceManager(context, overrides = {}) {
|
|
|
118
125
|
assertSafeServiceContext(context);
|
|
119
126
|
const deps = { ...defaultDeps, ...overrides };
|
|
120
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
|
+
};
|
|
121
207
|
const contents = context.platform === 'darwin' ? renderLaunchAgent(context) : renderSystemdUnit(context);
|
|
122
208
|
const launchdDomain = `gui/${context.userId}/${SERVICE_LABEL}`;
|
|
123
209
|
// `gui/<uid>` exists only while someone is logged into the GUI. Switching this agent to
|
|
@@ -278,6 +364,8 @@ export function createServiceManager(context, overrides = {}) {
|
|
|
278
364
|
await deps.run('systemctl', ['--user', 'daemon-reload']);
|
|
279
365
|
if (!preserveDisabled)
|
|
280
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`]);
|
|
281
369
|
}
|
|
282
370
|
else {
|
|
283
371
|
// Reload an existing service before bootstrapping the replacement into the GUI domain.
|
|
@@ -296,6 +384,7 @@ export function createServiceManager(context, overrides = {}) {
|
|
|
296
384
|
};
|
|
297
385
|
return {
|
|
298
386
|
async install(options = {}) {
|
|
387
|
+
await deviceAdmission(true);
|
|
299
388
|
await withGuiSessionHint(() => performInstall(options));
|
|
300
389
|
},
|
|
301
390
|
/**
|
|
@@ -334,6 +423,7 @@ export function createServiceManager(context, overrides = {}) {
|
|
|
334
423
|
* longer on disk and surface launchd/systemd's raw error; reinstall first instead.
|
|
335
424
|
*/
|
|
336
425
|
async start() {
|
|
426
|
+
await deviceAdmission();
|
|
337
427
|
await withGuiSessionHint(async () => {
|
|
338
428
|
// An explicit Start is consent to re-enable; install/update alone preserves OFF.
|
|
339
429
|
if (context.platform === 'darwin')
|
|
@@ -346,8 +436,10 @@ export function createServiceManager(context, overrides = {}) {
|
|
|
346
436
|
});
|
|
347
437
|
},
|
|
348
438
|
async stop() { await withGuiSessionHint(() => lifecycle('stop')); },
|
|
349
|
-
async restart() { await withGuiSessionHint(() => lifecycle('restart')); },
|
|
439
|
+
async restart() { await deviceAdmission(); await withGuiSessionHint(() => lifecycle('restart')); },
|
|
350
440
|
async setEnabled(value) {
|
|
441
|
+
if (value)
|
|
442
|
+
await deviceAdmission();
|
|
351
443
|
await withGuiSessionHint(async () => {
|
|
352
444
|
if (context.platform === 'darwin') {
|
|
353
445
|
await deps.run('launchctl', [value ? 'enable' : 'disable', launchdDomain]);
|
|
@@ -28,6 +28,7 @@ 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,6 +35,7 @@ Type=simple
|
|
|
35
35
|
WorkingDirectory=${systemdToken(context.homeDir)}
|
|
36
36
|
${environment('HOME', context.homeDir)}
|
|
37
37
|
${environment('AXOM_WORKSTATION_CONFIG_DIR', context.configDir)}
|
|
38
|
+
${environment('AXOM_WORKSTATION_RUNTIME', 'device-v2')}
|
|
38
39
|
${environment('AXOM_WORKSTATION_NPM_PATH', context.npmPath)}
|
|
39
40
|
${environment('AXOM_WORKSTATION_NPM_PREFIX', context.npmPrefix)}
|
|
40
41
|
${environment('PATH', context.pathEnv)}
|
|
@@ -195,6 +195,9 @@ export class SystemMetricsSampler {
|
|
|
195
195
|
clearInterval(this.refreshTimer);
|
|
196
196
|
this.refreshTimer = null;
|
|
197
197
|
}
|
|
198
|
+
refreshWorkspaces() {
|
|
199
|
+
this.refreshTopologyInBackground();
|
|
200
|
+
}
|
|
198
201
|
snapshot() {
|
|
199
202
|
try {
|
|
200
203
|
this.refreshDarwinMemoryInBackground();
|
|
@@ -8,16 +8,13 @@ import { spawn } from 'node:child_process';
|
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
9
|
import { object, TOOL_REQUEST_BYTES, TOOL_RESULT_BYTES, validateRequest } from './validation.js';
|
|
10
10
|
/** Unix socket paths must remain below macOS/Linux's ~104-byte sockaddr_un limit. */
|
|
11
|
-
export function keeperPaths(options, temporaryDirectory = os.tmpdir(), platform = process.platform, kind = '
|
|
12
|
-
const identity = createHash('sha256').update(JSON.stringify([path.resolve(options.configDir), options.workstationId, options.server,
|
|
13
|
-
const namespace = `axom
|
|
14
|
-
// macOS /var/folders/.../T often exhausts sockaddr_un before adding a private namespace. Honor
|
|
15
|
-
// TMPDIR where it fits on Linux; the short system temporary root is checked for private ownership.
|
|
11
|
+
export function keeperPaths(options, temporaryDirectory = os.tmpdir(), platform = process.platform, kind = 'browser') {
|
|
12
|
+
const identity = createHash('sha256').update(JSON.stringify([path.resolve(options.configDir), options.workstationId, options.server, 'browser'])).digest('hex').slice(0, 32);
|
|
13
|
+
const namespace = `axom-browser-${process.getuid?.() ?? 'user'}`;
|
|
16
14
|
let dir = path.join(platform === 'darwin' ? '/tmp' : temporaryDirectory, namespace, identity);
|
|
17
15
|
if (Buffer.byteLength(path.join(dir, 'keeper.sock')) > 100)
|
|
18
16
|
dir = path.join('/tmp', namespace, identity);
|
|
19
|
-
|
|
20
|
-
return { dir, socket: path.join(dir, 'keeper.sock'), lock: path.join(dir, 'startup.lock'), pidFile: path.join(dir, 'keeper.pid.json'), lockPort: kind === 'browser' ? 10000 + Number.parseInt(identity.slice(0, 8), 16) % 8000 : 20000 + Number.parseInt(identity.slice(0, 8), 16) % 40000, unit: `axom-${kind === 'browser' ? 'browser' : 'tools'}-keeper-${identity}` };
|
|
17
|
+
return { dir, socket: path.join(dir, 'keeper.sock'), lock: path.join(dir, 'startup.lock'), pidFile: path.join(dir, 'keeper.pid.json'), lockPort: 10000 + Number.parseInt(identity.slice(0, 8), 16) % 8000, unit: `axom-browser-keeper-${identity}` };
|
|
21
18
|
}
|
|
22
19
|
export async function privateDirectory(dir) {
|
|
23
20
|
await fs.mkdir(dir, { recursive: true, mode: 0o700 });
|
|
@@ -84,22 +81,16 @@ function pidAlive(pid) {
|
|
|
84
81
|
return error.code === 'EPERM';
|
|
85
82
|
}
|
|
86
83
|
}
|
|
87
|
-
/**
|
|
88
|
-
* A tools keeper client. The daemon only ever makes the browser kind (`browser`). The kind is required — no default — so nothing can
|
|
89
|
-
* start the retired terminal-kind tools keeper by accident (spec §9; terminals run in keeper-terminal, files in the daemon's files
|
|
90
|
-
* host); an explicit `terminal` client exists only for the tests that pin how the retired keeper's startup and tools.info behave.
|
|
91
|
-
* (`keeperPaths` keeps its terminal default: `keepers/legacy.ts` finds a legacy tools keeper an older CLI left running by it.)
|
|
92
|
-
*/
|
|
84
|
+
/** Browser supervisor client. Native terminals use TerminalKeeperClient exclusively. */
|
|
93
85
|
export class ToolsClient {
|
|
94
86
|
options;
|
|
95
87
|
kind;
|
|
96
88
|
paths;
|
|
97
89
|
starting;
|
|
98
|
-
browser;
|
|
99
90
|
constructor(options, kind) {
|
|
100
91
|
this.options = options;
|
|
101
92
|
this.kind = kind;
|
|
102
|
-
if (kind !== '
|
|
93
|
+
if (kind !== 'browser')
|
|
103
94
|
throw new Error('ToolsClient needs an explicit kind (browser)');
|
|
104
95
|
this.paths = keeperPaths(options, undefined, undefined, kind);
|
|
105
96
|
}
|
|
@@ -121,26 +112,16 @@ export class ToolsClient {
|
|
|
121
112
|
// Desktop sharing is optional. Checking terminal/files must not start a
|
|
122
113
|
// separate browser or require its dependencies when no VNC was configured.
|
|
123
114
|
const metadata = { browser: false, browserMode: 'shared', browserBinding: browserCapability(undefined) };
|
|
124
|
-
return
|
|
115
|
+
return metadata;
|
|
125
116
|
}
|
|
126
117
|
if (binding && (method.startsWith('browser.') || method === 'tools.info')) {
|
|
127
118
|
const metadata = { browser: true, browserMode: 'shared', browserBinding: browserCapability(binding) };
|
|
128
119
|
if (method === 'browser.list')
|
|
129
120
|
return { ...metadata, sessions: [] };
|
|
130
121
|
if (method === 'tools.info')
|
|
131
|
-
return
|
|
122
|
+
return metadata;
|
|
132
123
|
throw new Error('Shared browser uses the live desktop viewer; legacy browser session operations are unavailable');
|
|
133
124
|
}
|
|
134
|
-
if (this.kind === 'terminal' && (method.startsWith('browser.') || method === 'tools.info')) {
|
|
135
|
-
this.browser ??= new ToolsClient(this.options, 'browser');
|
|
136
|
-
if (method.startsWith('browser.'))
|
|
137
|
-
return this.browser.request(method, params);
|
|
138
|
-
const [terminal, browser] = await Promise.all([this.localRequest(method, params), this.browser.request(method, params).catch(error => ({ browser: false, browserError: error.message }))]);
|
|
139
|
-
const result = { ...terminal, ...browser, browserMode: 'legacy', browserBinding: browserCapability(undefined) };
|
|
140
|
-
if (result.browser)
|
|
141
|
-
delete result.browserError;
|
|
142
|
-
return result;
|
|
143
|
-
}
|
|
144
125
|
return this.localRequest(method, params);
|
|
145
126
|
}
|
|
146
127
|
async localRequest(method, params) {
|
|
@@ -1,19 +1,10 @@
|
|
|
1
1
|
import { promises as fs } from 'node:fs';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* the legacy tools keeper gave it (tools/core.ts at the merge-base, since deleted, + tools/client.ts), and answering it never
|
|
9
|
-
* starts any keeper:
|
|
10
|
-
* - file fields come from the files host's last `ready` (or are computed here before the first one); `files: true`
|
|
11
|
-
* is reported even while the host restarts, so a probe never mistakes the machine for an old CLI;
|
|
12
|
-
* - terminal fields come from keeper-terminal's hello capabilities when that keeper is already running (connecting to it,
|
|
13
|
-
* never starting it: keepers/terminal/client.ts `terminalStatus`), otherwise from a cached node-pty load check
|
|
14
|
-
* (update/ptyPreflight.ts);
|
|
15
|
-
* - browser fields come from the browser-kind ToolsClient, whose `tools.info` only returns metadata.
|
|
16
|
-
*/
|
|
4
|
+
import { TERMINAL_GRID_SCHEMA, TERMINAL_GRID_CAPABILITY } from '../keepers/terminal/gridProtocol.js';
|
|
5
|
+
/* tools.info is read-only discovery. Schema/capability describe the native contract; terminal availability
|
|
6
|
+
* comes from a current running keeper hello. The daemon's absent-keeper source is false, never a load probe
|
|
7
|
+
* promising that a future keeper will start. Files/browser discovery retain their independent metadata. */
|
|
17
8
|
/** The legacy keeper always sent this (tools/core.ts at the merge-base); the browser metadata never removed it. */
|
|
18
9
|
export const BROWSER_DISABLED_ERROR = 'Browser tools are not enabled in this release';
|
|
19
10
|
export async function aggregateToolsInfo(sources) {
|
|
@@ -21,6 +12,8 @@ export async function aggregateToolsInfo(sources) {
|
|
|
21
12
|
return {
|
|
22
13
|
version: 1,
|
|
23
14
|
terminal: terminal.terminal,
|
|
15
|
+
terminalSchema: TERMINAL_GRID_SCHEMA,
|
|
16
|
+
terminalCapability: TERMINAL_GRID_CAPABILITY,
|
|
24
17
|
browser: false,
|
|
25
18
|
...files,
|
|
26
19
|
...(terminal.terminalError ? { terminalError: terminal.terminalError } : {}),
|