@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.
Files changed (159) hide show
  1. package/README.md +7 -1
  2. package/dist/axom-patch/cli/README.md +31 -2
  3. package/dist/axom-patch/cli/account-limits/generate.py +2 -2
  4. package/dist/axom-patch/cli/claude-replay-lifecycle.json +16 -0
  5. package/dist/axom-patch/cli/codex-terminal-output.json +2 -2
  6. package/dist/axom-patch/cli/probes/claude-replay-lifecycle.mjs +31 -0
  7. package/dist/axom-patch/cli/probes/codex-terminal-output.mjs +1 -1
  8. package/dist/axom-patch/cli/probes/zcode-env-account.mjs +81 -0
  9. package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +7 -0
  10. package/dist/axom-patch/cli/qoder/dist/axom-acp-compat.mjs +137 -41
  11. package/dist/axom-patch/cli/qoder/releases/1.1.75.json +5 -88
  12. package/dist/axom-patch/cli/qoder-native-acp.json +23 -23
  13. package/dist/axom-patch/cli/runtime.json +3 -1
  14. package/dist/axom-patch/cli/zcode/env-account.mjs +13 -0
  15. package/dist/axom-patch/cli/zcode/generate-env-account.py +52 -0
  16. package/dist/axom-patch/cli/zcode-account-limits.json +7 -7
  17. package/dist/axom-patch/cli/zcode-env-account.json +58 -0
  18. package/dist/axom-patch/cli/zcode-models.json +9 -9
  19. package/dist/axom-patch/cli/zcode-subagent-subwork.json +10 -10
  20. package/dist/cli.js +9 -11
  21. package/dist/commands/auth.js +4 -1
  22. package/dist/commands/update.js +68 -7
  23. package/dist/commands/workstation.js +94 -223
  24. package/dist/commands/workstationRegistrations.js +116 -0
  25. package/dist/credentialBroker.js +13 -2
  26. package/dist/credentials.js +16 -0
  27. package/dist/install/claude/commands.js +2 -2
  28. package/dist/keepers/terminal/keeper.mjs +1089 -673
  29. package/dist/keepers/terminal/manifest.json +4 -4
  30. package/dist/native/darwin-x64/axom-quic +0 -0
  31. package/dist/workstation/acp/acpAgents.js +11 -4
  32. package/dist/workstation/acp/outbox.js +12 -5
  33. package/dist/workstation/acp/permission.js +2 -2
  34. package/dist/workstation/acp/processor/checkpoint.js +4 -3
  35. package/dist/workstation/acp/processor/control.js +2 -2
  36. package/dist/workstation/acp/processor/jsonrpc.js +8 -0
  37. package/dist/workstation/acp/processor/session.js +3 -2
  38. package/dist/workstation/acp/processor/state.js +1 -0
  39. package/dist/workstation/acp/processor/transport/local.js +19 -3
  40. package/dist/workstation/acp/processor/updates.js +25 -0
  41. package/dist/workstation/acp/processorSession.js +12 -1
  42. package/dist/workstation/acp/registry/catalog.js +6 -6
  43. package/dist/workstation/acp/test/keeperWorld.js +4 -3
  44. package/dist/workstation/agentSessions.js +76 -23
  45. package/dist/workstation/channels/agent.js +9 -1
  46. package/dist/workstation/channels/computer.js +27 -4
  47. package/dist/workstation/channels/management.js +36 -5
  48. package/dist/workstation/config.js +18 -6
  49. package/dist/workstation/control/bindingStore.js +4 -0
  50. package/dist/workstation/control/commands.js +1 -0
  51. package/dist/workstation/control/daemonIdentity.js +90 -0
  52. package/dist/workstation/control/deviceCommands.js +191 -0
  53. package/dist/workstation/control/deviceProtocol.js +68 -0
  54. package/dist/workstation/control/locks.js +51 -4
  55. package/dist/workstation/control/rootFormat.js +16 -0
  56. package/dist/workstation/control/runtimeClient.js +60 -5
  57. package/dist/workstation/control/runtimeHost.js +96 -9
  58. package/dist/workstation/control/serverIdentity.js +2 -2
  59. package/dist/workstation/control/updateTransition.js +91 -0
  60. package/dist/workstation/daemon.js +262 -360
  61. package/dist/workstation/daemonctl.js +150 -12
  62. package/dist/workstation/desktopControl/automationDevice.js +219 -0
  63. package/dist/workstation/desktopControl/automationJson.js +74 -0
  64. package/dist/workstation/desktopControl/automationLocalState.js +23 -0
  65. package/dist/workstation/desktopControl/automationPreparation.js +1 -0
  66. package/dist/workstation/desktopControl/automationProtocol.js +253 -0
  67. package/dist/workstation/desktopControl/automationSchema.generated.js +1 -0
  68. package/dist/workstation/desktopControl/automationSession.js +81 -0
  69. package/dist/workstation/desktopControl/automationTransport.js +138 -0
  70. package/dist/workstation/desktopControl/capabilities.js +2 -1
  71. package/dist/workstation/desktopControl/deviceAdmission.js +229 -0
  72. package/dist/workstation/desktopControl/deviceOwnership.js +130 -0
  73. package/dist/workstation/desktopControl/deviceSockets.js +2 -2
  74. package/dist/workstation/desktopControl/helperProcess.js +250 -1
  75. package/dist/workstation/desktopControl/hostGrant.js +65 -0
  76. package/dist/workstation/desktopControl/lobby.js +4 -2
  77. package/dist/workstation/desktopControl/macProvider.js +41 -40
  78. package/dist/workstation/desktopControl/nativeDeviceAdmission.js +207 -0
  79. package/dist/workstation/desktopControl/nativeProvider.js +31 -5
  80. package/dist/workstation/desktopControl/ownershipProtocol.js +31 -0
  81. package/dist/workstation/desktopControl/protocol.js +15 -6
  82. package/dist/workstation/desktopControl/supervisor.js +110 -17
  83. package/dist/workstation/desktopControl/wallpaper.js +26 -6
  84. package/dist/workstation/exec/policy.js +5 -0
  85. package/dist/workstation/files/client.js +10 -1
  86. package/dist/workstation/files/host.js +9 -2
  87. package/dist/workstation/journal.js +5 -2
  88. package/dist/workstation/keepers/acp/client.js +38 -2
  89. package/dist/workstation/keepers/kit/buildVersion.js +35 -11
  90. package/dist/workstation/keepers/kit/client.js +2 -2
  91. package/dist/workstation/keepers/kit/launcher.js +80 -4
  92. package/dist/workstation/keepers/legacy.js +16 -2
  93. package/dist/workstation/keepers/policy.js +5 -34
  94. package/dist/workstation/keepers/terminal/client.js +115 -23
  95. package/dist/workstation/keepers/terminal/exec.js +6 -3
  96. package/dist/workstation/keepers/terminal/frames.js +73 -0
  97. package/dist/workstation/keepers/terminal/generations.js +5 -8
  98. package/dist/workstation/keepers/terminal/gridProtocol.js +16 -0
  99. package/dist/workstation/keepers/terminal/input.js +146 -0
  100. package/dist/workstation/keepers/terminal/keeper.js +22 -8
  101. package/dist/workstation/keepers/terminal/main.js +1 -1
  102. package/dist/workstation/keepers/terminal/metadata.js +138 -0
  103. package/dist/workstation/keepers/terminal/protocol.js +4 -28
  104. package/dist/workstation/keepers/terminal/pty.js +1 -330
  105. package/dist/workstation/keepers/terminal/screen.js +246 -0
  106. package/dist/workstation/keepers/terminal/sessions.js +526 -0
  107. package/dist/workstation/oauth/deviceAuthorization.js +31 -17
  108. package/dist/workstation/protocol/management.js +23 -4
  109. package/dist/workstation/rd/artifact.js +4 -3
  110. package/dist/workstation/rd/control.js +3 -5
  111. package/dist/workstation/rd/host.js +213 -0
  112. package/dist/workstation/rd/migrationReadiness.js +46 -0
  113. package/dist/workstation/rd/provider.js +44 -10
  114. package/dist/workstation/rd/state.js +14 -2
  115. package/dist/workstation/rd/update.js +5 -8
  116. package/dist/workstation/rd/upkeep.js +44 -2
  117. package/dist/workstation/registrations/enrollment.js +250 -0
  118. package/dist/workstation/registrations/enrollmentPending.js +361 -0
  119. package/dist/workstation/registrations/migration.js +356 -0
  120. package/dist/workstation/registrations/model.js +34 -0
  121. package/dist/workstation/registrations/removal.js +222 -0
  122. package/dist/workstation/registrations/store.js +737 -0
  123. package/dist/workstation/runtime/desktopBinding.js +20 -0
  124. package/dist/workstation/runtime/deviceDesktop.js +193 -0
  125. package/dist/workstation/runtime/deviceRuntime.js +568 -0
  126. package/dist/workstation/runtime/deviceServices.js +117 -0
  127. package/dist/workstation/runtime/events.js +20 -0
  128. package/dist/workstation/runtime/ownership.js +100 -0
  129. package/dist/workstation/runtime/registrationRuntime.js +575 -0
  130. package/dist/workstation/runtime/remoteDesktopAdapter.js +186 -0
  131. package/dist/workstation/runtime/removalActivity.js +184 -0
  132. package/dist/workstation/service/installer.js +93 -1
  133. package/dist/workstation/service/launchd.js +1 -0
  134. package/dist/workstation/service/systemd.js +1 -0
  135. package/dist/workstation/systemMetrics.js +3 -0
  136. package/dist/workstation/tools/client.js +8 -27
  137. package/dist/workstation/tools/info.js +6 -13
  138. package/dist/workstation/tools/terminalRouter.js +17 -172
  139. package/dist/workstation/tools/validation.js +55 -1
  140. package/dist/workstation/update/archiveEvidence.js +206 -0
  141. package/dist/workstation/update/coordinator.js +34 -13
  142. package/dist/workstation/update/deviceCompatibility.js +119 -0
  143. package/dist/workstation/update/deviceCoordinator.js +369 -0
  144. package/dist/workstation/update/deviceHealth.js +104 -0
  145. package/dist/workstation/update/deviceHealthProtocol.js +26 -0
  146. package/dist/workstation/update/deviceRecoveryLifecycle.js +46 -0
  147. package/dist/workstation/update/deviceRollback.js +78 -0
  148. package/dist/workstation/update/globalInstall.js +80 -10
  149. package/dist/workstation/update/legacyBridge.js +414 -0
  150. package/dist/workstation/update/legacyOwner.js +39 -0
  151. package/dist/workstation/update/npmInstaller.js +2 -2
  152. package/dist/workstation/update/privateDurability.js +61 -0
  153. package/dist/workstation/update/recoveryPlan.js +124 -0
  154. package/dist/workstation/update/runUpdate.js +7 -1
  155. package/dist/workstation/update/taskRegistry.js +161 -3
  156. package/dist/workstation/update/watchdog.js +27427 -14410
  157. package/dist/workstation/update/watchdogProcess.js +72 -0
  158. package/package.json +7 -3
  159. 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 = 'terminal') {
12
- const identity = createHash('sha256').update(JSON.stringify([path.resolve(options.configDir), options.workstationId, options.server, ...(kind === 'browser' ? ['browser'] : [])])).digest('hex').slice(0, 32);
13
- const namespace = `axom-${kind === 'browser' ? 'browser' : 'tools'}-${process.getuid?.() ?? 'user'}`;
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
- // Browser ports are disjoint from terminal startup and the normal Linux ephemeral range.
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 !== 'terminal' && kind !== 'browser')
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 this.kind === 'browser' ? metadata : { ...await this.localRequest(method, params), ...metadata };
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 this.kind === 'browser' ? metadata : { ...await this.localRequest(method, params), ...metadata };
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
- * `tools.info`, aggregated by the daemon. Consumers (analysis C §3.6): Workbench app.mjs (version), CoworkService
6
- * (files / mediaPreviewVersion / fileFindVersion / home / userName), files.mjs (roots / downloads / terminal),
7
- * terminal.mjs (home, terminal / terminalError), browser.mjs (browser* fields). Every field keeps the value and meaning
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 } : {}),