@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.
Files changed (258) hide show
  1. package/README.md +25 -6
  2. package/dist/axom-patch/cli/README.md +129 -12
  3. package/dist/axom-patch/cli/account-limits/generate.py +15 -12
  4. package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +20 -3
  5. package/dist/axom-patch/cli/claude-account-limits.json +9 -9
  6. package/dist/axom-patch/cli/claude-replay-lifecycle.json +16 -0
  7. package/dist/axom-patch/cli/codex-account-limits.json +11 -11
  8. package/dist/axom-patch/cli/codex-terminal-output.json +16 -0
  9. package/dist/axom-patch/cli/commandcode-account-limits.json +9 -9
  10. package/dist/axom-patch/cli/kimi-quota.mjs +12 -0
  11. package/dist/axom-patch/cli/kimi-subagents.mjs +173 -0
  12. package/dist/axom-patch/cli/nightly-task.md +55 -13
  13. package/dist/axom-patch/cli/probes/claude-replay-lifecycle.mjs +31 -0
  14. package/dist/axom-patch/cli/probes/codex-terminal-output.mjs +33 -0
  15. package/dist/axom-patch/cli/probes/kimi-subagent-subwork.mjs +82 -0
  16. package/dist/axom-patch/cli/probes/zcode-env-account.mjs +81 -0
  17. package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +7 -0
  18. package/dist/axom-patch/cli/qoder/dist/axom-acp-compat.mjs +1687 -0
  19. package/dist/axom-patch/cli/qoder/releases/1.1.75.json +277 -0
  20. package/dist/axom-patch/cli/qoder-background-terminal.md +13 -6
  21. package/dist/axom-patch/cli/qoder-foreground-output.md +14 -8
  22. package/dist/axom-patch/cli/qoder-native-acp.json +227 -0
  23. package/dist/axom-patch/cli/qoder-steering.md +20 -25
  24. package/dist/axom-patch/cli/qoder-subagent-subwork.md +17 -68
  25. package/dist/axom-patch/cli/runtime.json +19 -19
  26. package/dist/axom-patch/cli/zcode/env-account.mjs +13 -0
  27. package/dist/axom-patch/cli/zcode/generate-env-account.py +52 -0
  28. package/dist/axom-patch/cli/zcode-account-limits.json +10 -11
  29. package/dist/axom-patch/cli/zcode-env-account.json +58 -0
  30. package/dist/axom-patch/cli/zcode-models.json +9 -10
  31. package/dist/axom-patch/cli/zcode-subagent-subwork.json +10 -11
  32. package/dist/cli.js +11 -45
  33. package/dist/commands/auth.js +4 -1
  34. package/dist/commands/knowledge.js +47 -0
  35. package/dist/commands/update.js +126 -3
  36. package/dist/commands/workstation.js +101 -164
  37. package/dist/commands/workstationRegistrations.js +116 -0
  38. package/dist/credentialBroker.js +13 -2
  39. package/dist/credentials.js +16 -0
  40. package/dist/install/claude/commands.js +15 -15
  41. package/dist/keepers/terminal/keeper.mjs +1089 -673
  42. package/dist/keepers/terminal/manifest.json +4 -4
  43. package/dist/mcp.js +1 -1
  44. package/dist/native/darwin-x64/axom-quic +0 -0
  45. package/dist/workstation/acp/acpAgents.js +113 -102
  46. package/dist/workstation/acp/acpClient.js +26 -0
  47. package/dist/workstation/acp/builds/install/npm.js +99 -10
  48. package/dist/workstation/acp/initializeResponse.js +6 -1
  49. package/dist/workstation/acp/managedPatches.js +189 -25
  50. package/dist/workstation/acp/managedRuntime.js +314 -0
  51. package/dist/workstation/acp/outbox.js +12 -5
  52. package/dist/workstation/acp/patchAssets.js +40 -0
  53. package/dist/workstation/acp/permission.js +2 -2
  54. package/dist/workstation/acp/processor/checkpoint.js +26 -34
  55. package/dist/workstation/acp/processor/control.js +7 -80
  56. package/dist/workstation/acp/processor/jsonrpc.js +8 -0
  57. package/dist/workstation/acp/processor/runtime.js +5 -81
  58. package/dist/workstation/acp/processor/session.js +54 -82
  59. package/dist/workstation/acp/processor/state.js +7 -13
  60. package/dist/workstation/acp/processor/step.js +3 -20
  61. package/dist/workstation/acp/processor/test/fixtureLog.js +9 -16
  62. package/dist/workstation/acp/processor/transport/local.js +19 -3
  63. package/dist/workstation/acp/processor/updates.js +156 -105
  64. package/dist/workstation/acp/processorSession.js +20 -173
  65. package/dist/workstation/acp/registry/catalog.js +65 -39
  66. package/dist/workstation/acp/registry/launch.js +16 -1
  67. package/dist/workstation/acp/switching.js +0 -3
  68. package/dist/workstation/acp/test/fakeAgent.js +13 -0
  69. package/dist/workstation/acp/test/keeperWorld.js +32 -3
  70. package/dist/workstation/acp/test/raw/sources.js +16 -130
  71. package/dist/workstation/acp/test/realAdapter.js +1 -47
  72. package/dist/workstation/acp/test/scriptAgent.js +11 -3
  73. package/dist/workstation/agentFlow.js +1 -1
  74. package/dist/workstation/agentSessions.js +114 -166
  75. package/dist/workstation/channels/agent.js +9 -1
  76. package/dist/workstation/channels/computer.js +27 -4
  77. package/dist/workstation/channels/management.js +130 -15
  78. package/dist/workstation/channels/url.js +1 -1
  79. package/dist/workstation/config.js +33 -9
  80. package/dist/workstation/connection/encryptedChannel.js +4 -1
  81. package/dist/workstation/control/bindingStore.js +204 -0
  82. package/dist/workstation/control/commands.js +125 -0
  83. package/dist/workstation/control/daemonIdentity.js +143 -0
  84. package/dist/workstation/control/deviceCommands.js +191 -0
  85. package/dist/workstation/control/deviceProtocol.js +68 -0
  86. package/dist/workstation/control/enrollment.js +106 -0
  87. package/dist/workstation/control/inspect.js +71 -0
  88. package/dist/workstation/control/locks.js +110 -0
  89. package/dist/workstation/control/protocol.js +40 -0
  90. package/dist/workstation/control/restartGuard.js +46 -0
  91. package/dist/workstation/control/rootFormat.js +16 -0
  92. package/dist/workstation/control/runtimeClient.js +152 -0
  93. package/dist/workstation/control/runtimeHost.js +235 -0
  94. package/dist/workstation/control/serverIdentity.js +35 -0
  95. package/dist/workstation/control/updateTransition.js +91 -0
  96. package/dist/workstation/daemon.js +266 -264
  97. package/dist/workstation/daemonctl.js +226 -25
  98. package/dist/workstation/desktopControl/audio.js +99 -0
  99. package/dist/workstation/desktopControl/automationDevice.js +219 -0
  100. package/dist/workstation/desktopControl/automationJson.js +74 -0
  101. package/dist/workstation/desktopControl/automationLocalState.js +23 -0
  102. package/dist/workstation/desktopControl/automationProtocol.js +253 -0
  103. package/dist/workstation/desktopControl/automationSchema.generated.js +1 -0
  104. package/dist/workstation/desktopControl/automationSession.js +81 -0
  105. package/dist/workstation/desktopControl/automationTransport.js +138 -0
  106. package/dist/workstation/desktopControl/capabilities.js +93 -0
  107. package/dist/workstation/desktopControl/captureModes.js +9 -0
  108. package/dist/workstation/desktopControl/captureTargetLabel.js +9 -0
  109. package/dist/workstation/desktopControl/deviceAdmission.js +229 -0
  110. package/dist/workstation/desktopControl/deviceOwnership.js +130 -0
  111. package/dist/workstation/desktopControl/deviceSockets.js +246 -0
  112. package/dist/workstation/desktopControl/helperProcess.js +602 -0
  113. package/dist/workstation/desktopControl/hostGrant.js +65 -0
  114. package/dist/workstation/desktopControl/lobby.js +131 -0
  115. package/dist/workstation/desktopControl/macProvider.js +156 -0
  116. package/dist/workstation/desktopControl/media.js +331 -0
  117. package/dist/workstation/desktopControl/nativeDeviceAdmission.js +207 -0
  118. package/dist/workstation/desktopControl/nativeProvider.js +1044 -0
  119. package/dist/workstation/desktopControl/networkEvidence.js +43 -0
  120. package/dist/workstation/desktopControl/overview.js +58 -0
  121. package/dist/workstation/desktopControl/ownershipProtocol.js +31 -0
  122. package/dist/workstation/desktopControl/protocol.js +311 -0
  123. package/dist/workstation/desktopControl/provider.js +1 -0
  124. package/dist/workstation/desktopControl/quicMedia.js +240 -0
  125. package/dist/workstation/desktopControl/rateController.js +226 -0
  126. package/dist/workstation/desktopControl/supervisor.js +1774 -0
  127. package/dist/workstation/desktopControl/transport.js +27 -0
  128. package/dist/workstation/desktopControl/wallpaper.js +98 -0
  129. package/dist/workstation/exec/policy.js +5 -0
  130. package/dist/workstation/files/client.js +10 -1
  131. package/dist/workstation/files/credentials.js +142 -0
  132. package/dist/workstation/files/host.js +9 -2
  133. package/dist/workstation/files/text.js +42 -8
  134. package/dist/workstation/journal.js +5 -2
  135. package/dist/workstation/keepers/acp/client.js +38 -2
  136. package/dist/workstation/keepers/kit/buildVersion.js +35 -11
  137. package/dist/workstation/keepers/kit/bundle.js +21 -17
  138. package/dist/workstation/keepers/kit/client.js +2 -2
  139. package/dist/workstation/keepers/kit/launcher.js +123 -7
  140. package/dist/workstation/keepers/kit/paths.js +2 -1
  141. package/dist/workstation/keepers/legacy.js +16 -2
  142. package/dist/workstation/keepers/policy.js +5 -34
  143. package/dist/workstation/keepers/terminal/client.js +115 -23
  144. package/dist/workstation/keepers/terminal/exec.js +6 -3
  145. package/dist/workstation/keepers/terminal/frames.js +73 -0
  146. package/dist/workstation/keepers/terminal/generations.js +5 -8
  147. package/dist/workstation/keepers/terminal/gridProtocol.js +16 -0
  148. package/dist/workstation/keepers/terminal/input.js +146 -0
  149. package/dist/workstation/keepers/terminal/keeper.js +22 -8
  150. package/dist/workstation/keepers/terminal/main.js +1 -1
  151. package/dist/workstation/keepers/terminal/metadata.js +138 -0
  152. package/dist/workstation/keepers/terminal/protocol.js +4 -28
  153. package/dist/workstation/keepers/terminal/pty.js +1 -330
  154. package/dist/workstation/keepers/terminal/screen.js +246 -0
  155. package/dist/workstation/keepers/terminal/sessions.js +526 -0
  156. package/dist/workstation/managedInstall.js +57 -0
  157. package/dist/workstation/oauth/deviceAuthorization.js +31 -17
  158. package/dist/workstation/protocol/agent.js +2 -15
  159. package/dist/workstation/protocol/management.js +113 -5
  160. package/dist/workstation/protocol/runtimeDiagnostics.js +15 -0
  161. package/dist/workstation/rd/artifact.js +120 -0
  162. package/dist/workstation/rd/control.js +117 -0
  163. package/dist/workstation/rd/display.js +29 -0
  164. package/dist/workstation/rd/host.js +213 -0
  165. package/dist/workstation/rd/migrationReadiness.js +46 -0
  166. package/dist/workstation/rd/provider.js +256 -0
  167. package/dist/workstation/rd/sessions.js +391 -0
  168. package/dist/workstation/rd/state.js +57 -0
  169. package/dist/workstation/rd/update.js +168 -0
  170. package/dist/workstation/rd/upkeep.js +80 -0
  171. package/dist/workstation/register.js +27 -37
  172. package/dist/workstation/registrations/enrollment.js +250 -0
  173. package/dist/workstation/registrations/enrollmentPending.js +361 -0
  174. package/dist/workstation/registrations/migration.js +356 -0
  175. package/dist/workstation/registrations/model.js +34 -0
  176. package/dist/workstation/registrations/removal.js +222 -0
  177. package/dist/workstation/registrations/store.js +737 -0
  178. package/dist/workstation/runtime/desktopBinding.js +20 -0
  179. package/dist/workstation/runtime/deviceDesktop.js +193 -0
  180. package/dist/workstation/runtime/deviceRuntime.js +568 -0
  181. package/dist/workstation/runtime/deviceServices.js +117 -0
  182. package/dist/workstation/runtime/events.js +20 -0
  183. package/dist/workstation/runtime/ownership.js +100 -0
  184. package/dist/workstation/runtime/registrationRuntime.js +575 -0
  185. package/dist/workstation/runtime/remoteDesktopAdapter.js +186 -0
  186. package/dist/workstation/runtime/removalActivity.js +184 -0
  187. package/dist/workstation/service/installer.js +197 -14
  188. package/dist/workstation/service/launchd.js +2 -0
  189. package/dist/workstation/service/systemd.js +1 -0
  190. package/dist/workstation/storageMetrics.js +118 -0
  191. package/dist/workstation/systemMetrics.js +47 -92
  192. package/dist/workstation/toolchain.js +94 -0
  193. package/dist/workstation/tools/browser.js +28 -4
  194. package/dist/workstation/tools/client.js +8 -27
  195. package/dist/workstation/tools/info.js +6 -13
  196. package/dist/workstation/tools/terminalRouter.js +17 -172
  197. package/dist/workstation/tools/validation.js +55 -1
  198. package/dist/workstation/update/archiveEvidence.js +206 -0
  199. package/dist/workstation/update/artifact.js +2 -1
  200. package/dist/workstation/update/coordinator.js +34 -13
  201. package/dist/workstation/update/deviceCompatibility.js +119 -0
  202. package/dist/workstation/update/deviceCoordinator.js +369 -0
  203. package/dist/workstation/update/deviceHealth.js +104 -0
  204. package/dist/workstation/update/deviceHealthProtocol.js +26 -0
  205. package/dist/workstation/update/deviceRecoveryLifecycle.js +46 -0
  206. package/dist/workstation/update/deviceRollback.js +78 -0
  207. package/dist/workstation/update/globalInstall.js +186 -0
  208. package/dist/workstation/update/legacyBridge.js +414 -0
  209. package/dist/workstation/update/legacyOwner.js +39 -0
  210. package/dist/workstation/update/npmInstaller.js +51 -25
  211. package/dist/workstation/update/privateDurability.js +61 -0
  212. package/dist/workstation/update/rdRollback.js +145 -0
  213. package/dist/workstation/update/recoveryPlan.js +124 -0
  214. package/dist/workstation/update/runUpdate.js +10 -3
  215. package/dist/workstation/update/taskRegistry.js +161 -3
  216. package/dist/workstation/update/watchdog.js +29806 -333
  217. package/dist/workstation/update/watchdogProcess.js +72 -0
  218. package/package.json +11 -7
  219. package/skills/axom/SKILL.md +11 -11
  220. package/skills/axom-definition/SKILL.md +3 -3
  221. package/skills/axom-platform/SKILL.md +3 -3
  222. package/dist/axom-patch/cli/probes/qoder-steering.mjs +0 -89
  223. package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +0 -61
  224. package/dist/axom-patch/cli/qoder-steering.json +0 -52
  225. package/dist/axom-patch/cli/qoder-subagent-origin.json +0 -73
  226. package/dist/commands/guide.js +0 -38
  227. package/dist/runtime-deps/qoder-sdk/package-lock.json +0 -1189
  228. package/dist/runtime-deps/qoder-sdk/package.json +0 -8
  229. package/dist/workstation/acp/delegationMcp.js +0 -24
  230. package/dist/workstation/acp/processor/sideChannel.js +0 -80
  231. package/dist/workstation/acp/qoderStartup.js +0 -45
  232. package/dist/workstation/acp/test/raw/qoderCaptures.js +0 -125
  233. package/dist/workstation/qoderAdapter/backgroundProcess.js +0 -118
  234. package/dist/workstation/qoderAdapter/backgroundTerminal.js +0 -193
  235. package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +0 -372
  236. package/dist/workstation/qoderAdapter/callbackContext.js +0 -76
  237. package/dist/workstation/qoderAdapter/completedContinuation.js +0 -139
  238. package/dist/workstation/qoderAdapter/contract.js +0 -78
  239. package/dist/workstation/qoderAdapter/foregroundOutput.js +0 -36
  240. package/dist/workstation/qoderAdapter/history.js +0 -85
  241. package/dist/workstation/qoderAdapter/identityIndex.js +0 -191
  242. package/dist/workstation/qoderAdapter/inputQueue.js +0 -42
  243. package/dist/workstation/qoderAdapter/main.js +0 -95
  244. package/dist/workstation/qoderAdapter/mapper.js +0 -117
  245. package/dist/workstation/qoderAdapter/observerPlugin.js +0 -245
  246. package/dist/workstation/qoderAdapter/observerReceiver.js +0 -187
  247. package/dist/workstation/qoderAdapter/permissions.js +0 -61
  248. package/dist/workstation/qoderAdapter/profile.js +0 -93
  249. package/dist/workstation/qoderAdapter/sdkDriver.js +0 -48
  250. package/dist/workstation/qoderAdapter/sdkRuntime.js +0 -93
  251. package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +0 -145
  252. package/dist/workstation/qoderAdapter/session.js +0 -319
  253. package/dist/workstation/qoderAdapter/subagentMeta.js +0 -28
  254. package/dist/workstation/qoderAdapter/subwork.js +0 -85
  255. package/dist/workstation/qoderAdapter/taskControl.js +0 -38
  256. package/dist/workstation/qoderAdapter/taskLedger.js +0 -163
  257. package/dist/workstation/qoderAdapter/usage.js +0 -37
  258. /package/dist/workstation/{qoderAdapter/types.js → desktopControl/automationPreparation.js} +0 -0
@@ -0,0 +1,71 @@
1
+ import os from 'node:os';
2
+ import { createServiceManager } from '../service/installer.js';
3
+ import { serviceContext } from '../daemonctl.js';
4
+ import { promises as fs } from 'node:fs';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { BindingStore } from './bindingStore.js';
8
+ import { inspectRuntime } from './runtimeClient.js';
9
+ import { localInspectionSchema, operationRecordSchema } from './protocol.js';
10
+ import { readControlFile } from './runtimeHost.js';
11
+ import { updateServiceContext } from '../config.js';
12
+ import { CLIENT_VERSION } from '../version.js';
13
+ import { readPidManifest, manifestProcessLive } from '../keepers/kit/lock.js';
14
+ export async function inspectKeepers(directory) {
15
+ try {
16
+ let running = false;
17
+ for (const kind of ['acp', 'terminal']) {
18
+ const root = path.join(directory, 'keepers', kind, 'run');
19
+ let entries;
20
+ try {
21
+ entries = await fs.readdir(root);
22
+ }
23
+ catch (e) {
24
+ if (e.code === 'ENOENT')
25
+ continue;
26
+ throw e;
27
+ }
28
+ for (const entry of entries) {
29
+ const manifest = await readPidManifest(path.join(root, entry, 'keeper.pid.json'));
30
+ if (manifest && await manifestProcessLive(manifest))
31
+ running = true;
32
+ }
33
+ }
34
+ return running ? 'running' : 'stopped';
35
+ }
36
+ catch {
37
+ return 'unknown';
38
+ }
39
+ }
40
+ export async function inspectWorkstation(directory, deps = {}) {
41
+ const identity = await new BindingStore(directory).inspect();
42
+ let pending = null;
43
+ try {
44
+ const operation = operationRecordSchema.parse(JSON.parse(await readControlFile(path.join(directory, 'operation.json'))));
45
+ if (operation.outcome !== 'succeeded')
46
+ pending = operation;
47
+ }
48
+ catch (e) {
49
+ if (e.code !== 'ENOENT')
50
+ throw e;
51
+ }
52
+ const cliPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../cli.js');
53
+ const context = updateServiceContext(directory);
54
+ let runtime = await inspectRuntime(directory);
55
+ const standardHome = os.userInfo().homedir;
56
+ const standardDirectory = path.join(standardHome, '.axom/workstation');
57
+ const probeLegacy = deps.legacyServiceRunning ?? (process.platform === 'darwin' && path.resolve(directory) === standardDirectory && os.homedir() === standardHome
58
+ ? async () => (await createServiceManager(serviceContext()).status()).running : undefined);
59
+ if (runtime.state === 'stopped' && probeLegacy) {
60
+ try {
61
+ if (await probeLegacy())
62
+ runtime = { ...runtime, state: 'unknown', connection: 'unknown' };
63
+ }
64
+ catch {
65
+ runtime = { ...runtime, state: 'unknown', connection: 'unknown' };
66
+ }
67
+ }
68
+ const service = (process.platform === 'darwin' || process.platform === 'linux') && path.resolve(directory) === standardDirectory && os.homedir() === standardHome
69
+ ? await createServiceManager(serviceContext()).enablement() : undefined;
70
+ return localInspectionSchema.parse({ schemaVersion: 1, ...identity, installation: { source: context.managed ? 'managed' : 'existing', version: CLIENT_VERSION, nodeVersion: process.versions.node, nodePath: context.nodePath, cliPath, npmPath: context.npmPath, npmPrefix: context.npmPrefix }, runtime, ...(service ? { service } : {}), keepers: await inspectKeepers(directory), pending });
71
+ }
@@ -0,0 +1,110 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { AsyncLocalStorage } from 'node:async_hooks';
6
+ import lockfile from 'proper-lockfile';
7
+ import { privateDirectory } from '../keepers/kit/paths.js';
8
+ import { pidAlive, processCommand } from '../keepers/kit/lock.js';
9
+ import { LocalControlError } from './protocol.js';
10
+ const ownership = new AsyncLocalStorage();
11
+ export function assertLockOwnership() {
12
+ let lease = ownership.getStore();
13
+ while (lease) {
14
+ if (!lease.valid || !lease.active)
15
+ throw new LocalControlError('BUSY', 'Workstation lock ownership was lost or released');
16
+ lease = lease.parent;
17
+ }
18
+ }
19
+ function operationLease(directory) {
20
+ const expected = path.join(path.resolve(directory), 'control/operation');
21
+ let lease = ownership.getStore();
22
+ let held;
23
+ while (lease) {
24
+ if (!lease.valid || !lease.active)
25
+ throw new LocalControlError('BUSY', 'Workstation operation lock ownership was lost or released');
26
+ if (lease.stem === expected)
27
+ held = lease;
28
+ lease = lease.parent;
29
+ }
30
+ if (!held)
31
+ throw new LocalControlError('BUSY', 'The workstation operation lock is required');
32
+ return held;
33
+ }
34
+ export function assertWorkstationOperationLock(directory) { return operationLease(directory); }
35
+ export function assertOutsideOperationLease() {
36
+ let lease = ownership.getStore();
37
+ while (lease) {
38
+ if (path.basename(lease.stem) === 'operation')
39
+ throw new LocalControlError('BUSY', 'Owner control requires an outside-operation context');
40
+ lease = lease.parent;
41
+ }
42
+ }
43
+ export function registerOperationLeaseFinalizer(directory, finalizer) {
44
+ operationLease(directory).finalizers.add(finalizer);
45
+ }
46
+ function finalize(lease) {
47
+ lease.active = false;
48
+ const callbacks = [...lease.finalizers];
49
+ lease.finalizers.clear();
50
+ for (const callback of callbacks) {
51
+ try {
52
+ callback();
53
+ }
54
+ catch (error) {
55
+ lease.finalizerErrors.push(error);
56
+ }
57
+ }
58
+ }
59
+ export async function withPrivateLock(stem, work) {
60
+ await privateDirectory(path.dirname(stem));
61
+ const ownerFile = stem + '.owner.json';
62
+ // proper-lockfile's time lease alone is insufficient: a suspended live app still owns its operation.
63
+ let lockPresent = false;
64
+ try {
65
+ const stat = await fs.lstat(stem + '.lock');
66
+ lockPresent = true;
67
+ if (!stat.isDirectory() || stat.isSymbolicLink())
68
+ throw Error('Unsafe lock');
69
+ if (Date.now() - stat.mtimeMs > 30_000) {
70
+ const owner = JSON.parse(await fs.readFile(ownerFile, 'utf8'));
71
+ if (!Number.isSafeInteger(owner.pid) || owner.pid <= 0 || typeof owner.command !== 'string')
72
+ throw Error('Unknown owner');
73
+ if (pidAlive(owner.pid))
74
+ throw Error('Live or unverifiable owner');
75
+ }
76
+ }
77
+ catch (error) {
78
+ if (lockPresent || error.code !== 'ENOENT')
79
+ throw new LocalControlError('BUSY', 'Another workstation operation is active');
80
+ }
81
+ const lease = { valid: true, active: true, stem: path.resolve(stem), parent: ownership.getStore(), finalizers: new Set(), finalizerErrors: [] };
82
+ const nonce = randomUUID();
83
+ let release;
84
+ try {
85
+ release = await lockfile.lock(stem, { realpath: false, stale: 30_000, update: 2_000, retries: 0, onCompromised: () => { lease.valid = false; finalize(lease); } });
86
+ }
87
+ catch {
88
+ throw new LocalControlError('BUSY', 'Another workstation operation is active');
89
+ }
90
+ try {
91
+ await fs.writeFile(ownerFile, JSON.stringify({ schemaVersion: 1, pid: process.pid, command: await processCommand(process.pid), nonce }), { mode: 0o600 });
92
+ return await ownership.run(lease, async () => { const result = await work(); assertLockOwnership(); return result; });
93
+ }
94
+ finally {
95
+ finalize(lease);
96
+ if (lease.valid) {
97
+ try {
98
+ if (JSON.parse(await fs.readFile(ownerFile, 'utf8')).nonce === nonce)
99
+ await fs.rm(ownerFile);
100
+ }
101
+ catch { /* release still required */ }
102
+ await release();
103
+ }
104
+ if (lease.finalizerErrors.length)
105
+ throw new AggregateError(lease.finalizerErrors, 'Workstation lease finalizers failed');
106
+ }
107
+ }
108
+ export function withWorkstationLock(directory, name, work) {
109
+ return withPrivateLock(name === 'operation' ? path.join(directory, 'control/operation') : path.join(os.homedir(), '.axom/locks/cli-install'), work);
110
+ }
@@ -0,0 +1,40 @@
1
+ import { z } from 'zod';
2
+ export const bindingSchema = z.object({ server: z.string().min(1), workstationId: z.string().min(1), ownerUserId: z.string().min(1).optional() });
3
+ export const bindingGuardSchema = z.object({ revision: z.string().min(1), runtimeGeneration: z.string().nullable() });
4
+ const activitySchema = z.object({ commands: z.number().int().nonnegative(), terminals: z.number().int().nonnegative(), agents: z.number().int().nonnegative(), transfers: z.number().int().nonnegative() });
5
+ export const runtimeSnapshotSchema = z.object({
6
+ state: z.enum(['stopped', 'running', 'unknown']), binding: bindingSchema.nullable(), generation: z.string().nullable(),
7
+ mode: z.enum(['launchd', 'foreground', 'unknown']), connection: z.enum(['offline', 'connecting', 'online', 'reconnecting', 'rejected', 'unknown']), activity: activitySchema.nullable(),
8
+ });
9
+ export const cliInstallationSchema = z.object({
10
+ source: z.enum(['managed', 'existing']), version: z.string(), nodeVersion: z.string(),
11
+ nodePath: z.string(), cliPath: z.string(), npmPath: z.string(), npmPrefix: z.string(),
12
+ });
13
+ export const operationRecordSchema = z.object({
14
+ id: z.string().min(1), kind: z.enum(['register', 'switch', 'start', 'stop', 'remove', 'update']), phase: z.string().min(1).max(80),
15
+ target: z.object({ server: z.string().min(1), ownerUserId: z.string().min(1).nullable() }).strict(),
16
+ workstationId: z.string().min(1).optional(), outcome: z.enum(['pending', 'succeeded', 'failed', 'unknown']),
17
+ }).strict();
18
+ export const serviceSnapshotSchema = z.object({ installed: z.boolean(), enabled: z.boolean().nullable(), loaded: z.boolean(), running: z.boolean(), reason: z.string().optional() });
19
+ export const localInspectionSchema = z.object({
20
+ schemaVersion: z.literal(1), installation: cliInstallationSchema, binding: bindingSchema.nullable(), revision: z.string().min(1),
21
+ identityState: z.enum(['absent', 'ready', 'incomplete', 'corrupt']), runtime: runtimeSnapshotSchema,
22
+ service: serviceSnapshotSchema.optional(),
23
+ keepers: z.enum(['stopped', 'running', 'unknown']), pending: operationRecordSchema.nullable(),
24
+ });
25
+ export const localErrorCodeSchema = z.enum(['UNSUPPORTED_INSTALLATION', 'UNSUPPORTED_PROTOCOL', 'BINDING_CONFLICT', 'INSTANCE_OCCUPIED', 'AUTHORIZATION_REQUIRED', 'OWNER_MISMATCH', 'NETWORK_FAILED', 'CORRUPT_IDENTITY', 'SERVICE_FAILED', 'BUSY', 'OUTCOME_UNKNOWN']);
26
+ export const cliResultSchema = z.object({
27
+ status: z.enum(['succeeded', 'busy', 'pending', 'failed', 'conflict']), inspection: localInspectionSchema.nullable(),
28
+ error: z.object({ code: localErrorCodeSchema, message: z.string().max(1024) }).optional(),
29
+ }).refine(value => value.status !== 'succeeded' || value.inspection !== null, 'Successful operations require inspection');
30
+ export class LocalControlError extends Error {
31
+ code;
32
+ constructor(code, message) {
33
+ super(message);
34
+ this.code = code;
35
+ this.name = 'LocalControlError';
36
+ }
37
+ }
38
+ export function stoppedRuntime() {
39
+ return { state: 'stopped', binding: null, generation: null, mode: 'unknown', connection: 'offline', activity: null };
40
+ }
@@ -0,0 +1,46 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promises as fs } from 'node:fs';
3
+ import { promisify } from 'node:util';
4
+ import { LocalControlError } from './protocol.js';
5
+ import { readControlFile, runtimeOwnerLive, runtimePaths } from './runtimeHost.js';
6
+ const run = promisify(execFile);
7
+ async function parentPid(pid) {
8
+ let value;
9
+ if (process.platform === 'linux') {
10
+ const stat = await fs.readFile(`/proc/${pid}/stat`, 'utf8');
11
+ value = stat.slice(stat.lastIndexOf(')') + 1).trim().split(/\s+/)[1];
12
+ }
13
+ else
14
+ value = (await run('ps', ['-o', 'ppid=', '-p', String(pid)], { timeout: 1000 })).stdout.trim();
15
+ const parent = Number(value);
16
+ if (!value || !Number.isSafeInteger(parent) || parent < 0)
17
+ throw new Error('Unknown process parent');
18
+ return parent;
19
+ }
20
+ export async function assertRuntimeRestartSafe(directory, generation, overrides = {}) {
21
+ const deps = { owner: async () => JSON.parse(await readControlFile(runtimePaths(directory).ownerFile)),
22
+ live: runtimeOwnerLive, parent: parentPid, pid: process.pid, ...overrides };
23
+ try {
24
+ const owner = await deps.owner();
25
+ if (owner.generation !== generation)
26
+ throw new LocalControlError('BINDING_CONFLICT', 'Workstation changed before restart');
27
+ if (!Number.isSafeInteger(owner.pid) || owner.pid <= 1 || !await deps.live(owner))
28
+ throw new Error('Unknown runtime process');
29
+ const seen = new Set();
30
+ const deadline = Date.now() + 3000;
31
+ let current = deps.pid;
32
+ while (current > 1 && !seen.has(current) && seen.size < 128 && Date.now() < deadline) {
33
+ if (current === owner.pid)
34
+ throw new LocalControlError('BUSY', 'This command is running inside the workstation daemon. Restarting it would terminate the current agent or command. Finish the session or move it to an independent keeper before restarting.');
35
+ seen.add(current);
36
+ current = await deps.parent(current);
37
+ }
38
+ if (current > 1)
39
+ throw new Error('Unknown process ancestry');
40
+ }
41
+ catch (error) {
42
+ if (error instanceof LocalControlError)
43
+ throw error;
44
+ throw new LocalControlError('OUTCOME_UNKNOWN', 'Cannot verify that workstation restart will preserve the calling session');
45
+ }
46
+ }
@@ -0,0 +1,16 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { LocalControlError } from './protocol.js';
4
+ export async function assertLegacyRoot(directory) {
5
+ for (const name of ['device.json', 'legacy-migration.json']) {
6
+ try {
7
+ await fs.lstat(path.join(directory, name));
8
+ }
9
+ catch (error) {
10
+ if (error.code === 'ENOENT')
11
+ continue;
12
+ throw error;
13
+ }
14
+ throw new LocalControlError('CORRUPT_IDENTITY', 'This workstation uses registrations; legacy identity writes and startup are unsupported');
15
+ }
16
+ }
@@ -0,0 +1,152 @@
1
+ import net from 'node:net';
2
+ import { runtimePaths, readControlFile, runtimeOwnerLive } from './runtimeHost.js';
3
+ import { runtimeSnapshotSchema, stoppedRuntime, LocalControlError, localErrorCodeSchema } from './protocol.js';
4
+ import { pidAlive } from '../keepers/kit/lock.js';
5
+ import { parseDeviceInspection, deviceResultSchema, deviceRuntimeUpdateRequestSchema, removeRegistrationRequestSchema } from './deviceProtocol.js';
6
+ export async function removeDeviceRuntimeRegistration(directory, value) {
7
+ const request = removeRegistrationRequestSchema.parse(value);
8
+ let response;
9
+ try {
10
+ response = await runtimeRequest(directory, 'remove-registration', request.guard.runtimeGeneration, { request }, 15_000);
11
+ }
12
+ catch (error) {
13
+ if (error instanceof LocalControlError)
14
+ throw error;
15
+ throw new LocalControlError('OUTCOME_UNKNOWN', 'Original running owner could not confirm removal');
16
+ }
17
+ const result = deviceResultSchema.parse(response);
18
+ if (result.operationId !== request.operationId || result.registrationId !== request.guard.registrationId)
19
+ throw new LocalControlError('OUTCOME_UNKNOWN', 'Original removal reply does not match');
20
+ return result;
21
+ }
22
+ export async function inspectDeviceRuntime(directory) {
23
+ return parseDeviceInspection(await runtimeRequest(directory, 'inspect'));
24
+ }
25
+ export async function prepareDeviceRuntimeStop(directory, generation, stopMode, operationId) {
26
+ const receipt = await runtimeRequest(directory, 'prepare-stop', generation, { stopMode, operationId }, 15_000);
27
+ if (receipt.prepared !== true || receipt.generation !== generation || receipt.stopMode !== stopMode)
28
+ throw new LocalControlError('OUTCOME_UNKNOWN', 'Device shutdown preparation was not confirmed');
29
+ }
30
+ export async function stopDeviceRuntime(directory, generation, stopMode) {
31
+ await runtimeRequest(directory, 'stop', generation, { stopMode });
32
+ const deadline = Date.now() + 10_000;
33
+ while (Date.now() < deadline) {
34
+ try {
35
+ const current = await inspectDeviceRuntime(directory);
36
+ if (current.runtime.generation !== generation)
37
+ throw new LocalControlError('BINDING_CONFLICT', 'A new device daemon started');
38
+ }
39
+ catch (error) {
40
+ if (error instanceof LocalControlError && error.code === 'BINDING_CONFLICT')
41
+ throw error;
42
+ if ((await inspectRuntime(directory)).state === 'stopped')
43
+ return;
44
+ }
45
+ await new Promise(resolve => setTimeout(resolve, 50));
46
+ }
47
+ throw new LocalControlError('OUTCOME_UNKNOWN', 'Device shutdown has not been confirmed');
48
+ }
49
+ export async function runtimeRequest(directory, action, expectedGeneration, extra = {}, timeoutMs = 3000) {
50
+ const paths = runtimePaths(directory);
51
+ const owner = JSON.parse(await readControlFile(paths.ownerFile));
52
+ if (typeof owner.generation !== 'string' || !pidAlive(owner.pid))
53
+ throw new LocalControlError('OUTCOME_UNKNOWN', 'Runtime owner cannot be verified');
54
+ if (expectedGeneration && owner.generation !== expectedGeneration)
55
+ throw new LocalControlError('BINDING_CONFLICT', 'Runtime changed');
56
+ const token = await readControlFile(paths.tokenFile);
57
+ return new Promise((resolve, reject) => {
58
+ const socket = net.createConnection(paths.socket);
59
+ let data = '';
60
+ let done = false;
61
+ const fail = (error) => { if (!done) {
62
+ done = true;
63
+ socket.destroy();
64
+ reject(error);
65
+ } };
66
+ socket.setTimeout(timeoutMs, () => fail(new LocalControlError('OUTCOME_UNKNOWN', 'Runtime did not respond')));
67
+ socket.on('error', () => fail(new LocalControlError('OUTCOME_UNKNOWN', 'Cannot contact runtime')));
68
+ socket.on('end', () => { if (!done)
69
+ fail(new LocalControlError('OUTCOME_UNKNOWN', 'Incomplete runtime response')); });
70
+ socket.on('connect', () => socket.write(JSON.stringify({ ...extra, action, generation: owner.generation, token }) + '\n'));
71
+ socket.on('data', chunk => {
72
+ data += chunk.toString();
73
+ if (Buffer.byteLength(data) > 65_536) {
74
+ fail(Error('Oversized response'));
75
+ return;
76
+ }
77
+ if (!data.includes('\n') || done)
78
+ return;
79
+ try {
80
+ const result = JSON.parse(data.split('\n')[0]);
81
+ if (!result.ok) {
82
+ const code = localErrorCodeSchema.safeParse(result.code);
83
+ throw new LocalControlError(code.success ? code.data : 'SERVICE_FAILED', 'Runtime rejected the request');
84
+ }
85
+ done = true;
86
+ socket.destroy();
87
+ resolve(result.value);
88
+ }
89
+ catch (error) {
90
+ fail(error);
91
+ }
92
+ });
93
+ });
94
+ }
95
+ export async function inspectRuntime(directory) {
96
+ try {
97
+ return runtimeSnapshotSchema.parse(await runtimeRequest(directory, 'inspect'));
98
+ }
99
+ catch (error) {
100
+ try {
101
+ const owner = JSON.parse(await readControlFile(runtimePaths(directory).ownerFile));
102
+ if (Number.isSafeInteger(owner.pid) && owner.pid > 0 && typeof owner.command === 'string' && typeof owner.generation === 'string' && !await runtimeOwnerLive(owner))
103
+ return stoppedRuntime();
104
+ }
105
+ catch { /* Partial ownership evidence remains unknown. */ }
106
+ if (error.code === 'ENOENT') {
107
+ // No control files at all is stopped; partial state must remain uncertain.
108
+ const p = runtimePaths(directory);
109
+ const { promises: fs } = await import('node:fs');
110
+ try {
111
+ await fs.lstat(p.socket);
112
+ }
113
+ catch (e) {
114
+ if (e.code === 'ENOENT') {
115
+ try {
116
+ await fs.lstat(p.ownerFile);
117
+ }
118
+ catch (e2) {
119
+ if (e2.code === 'ENOENT')
120
+ return stoppedRuntime();
121
+ }
122
+ }
123
+ }
124
+ }
125
+ return { ...stoppedRuntime(), state: 'unknown', connection: 'unknown' };
126
+ }
127
+ }
128
+ export async function stopRuntime(directory, expectedGeneration) {
129
+ await runtimeRequest(directory, 'stop', expectedGeneration);
130
+ const deadline = Date.now() + 10_000;
131
+ while (Date.now() < deadline) {
132
+ const state = await inspectRuntime(directory);
133
+ if (state.state === 'stopped')
134
+ return;
135
+ if (state.generation && state.generation !== expectedGeneration)
136
+ throw new LocalControlError('BINDING_CONFLICT', 'A new daemon started');
137
+ await new Promise(resolve => setTimeout(resolve, 50));
138
+ }
139
+ throw new LocalControlError('OUTCOME_UNKNOWN', 'Workstation shutdown has not been confirmed');
140
+ }
141
+ export async function requestRuntimeUpdate(directory, release, operationId) {
142
+ return await runtimeRequest(directory, 'update', undefined, { release, operationId });
143
+ }
144
+ export async function requestDeviceRuntimeUpdate(directory, raw) {
145
+ const request = deviceRuntimeUpdateRequestSchema.parse(raw);
146
+ if (!request.guard.runtimeGeneration)
147
+ throw new LocalControlError('OUTCOME_UNKNOWN', 'An original live device owner is required for update admission');
148
+ const status = await runtimeRequest(directory, 'update', request.guard.runtimeGeneration, { request }, 30_000);
149
+ if (!status || status.commandId !== request.operationId || !['REQUESTED', 'DRAINING', 'BUSY', 'DOWNLOADING', 'VERIFYING', 'INSTALLING', 'RESTARTING', 'SUCCEEDED', 'FAILED', 'ROLLED_BACK', 'UPDATE_IN_PROGRESS'].includes(status.phase))
150
+ throw new LocalControlError('OUTCOME_UNKNOWN', 'Original device update reply does not match its operation');
151
+ return status;
152
+ }