@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
@@ -1,237 +1,108 @@
1
- import { assertIdentityCanStart, recoverStoppedIdentity } from '../workstation/control/daemonIdentity.js';
2
- import { withWorkstationLock } from '../workstation/control/locks.js';
3
- import { inspectWorkstation } from '../workstation/control/inspect.js';
4
- import { runLocalJsonCommand } from '../workstation/control/commands.js';
5
- import { hostname } from 'node:os';
6
- import { basename } from 'node:path';
7
- import { loadConfig, resolveServer, UsageError } from '../config.js';
8
- import { accessTokenFor } from '../credentials.js';
9
- import { ensureFreshToken } from '../mcp.js';
1
+ import { randomUUID } from 'node:crypto';
2
+ import { UsageError } from '../config.js';
10
3
  import { runCommand } from '../output.js';
11
- import { clientDir, loadClientConfig } from '../workstation/config.js';
12
- import { resolveWorkstationName, sanitizeName } from '../workstation/names.js';
4
+ import { clientDir } from '../workstation/config.js';
13
5
  import { configureRd, rdStatus } from '../workstation/rd/control.js';
14
6
  import { runManagedDesktop } from '../workstation/rd/sessions.js';
15
- /**
16
- * Registers this workstation with the server if it has no credential yet, then leaves it alone.
17
- * Returns whether it just registered (`false` when an existing credential meant there was nothing
18
- * to do), so `start` can gate `install()` on that — see its action below for why that gate matters.
19
- *
20
- * Shared by `start` (which then hands off to the OS service) and the bare `axom workstation`
21
- * command (which runs the daemon directly): both need the exact same credential chain, so it lives
22
- * here once rather than twice.
23
- *
24
- * An existing workstation credential boots directly — the common case, and the only sane one for a
25
- * container that gets recreated on every restart: re-registering each boot would rotate keys for no
26
- * reason. Otherwise a usable access token registers this machine — `AXOM_ACCESS_TOKEN` (set by a
27
- * container with no human and no browser) or a stored user token (from `axom auth`), refreshing it
28
- * first if it's stale. With neither available, an interactive terminal falls back to the device
29
- * flow; a headless one gets a hard stop naming the fix.
30
- */
31
- async function ensureRegistered(deps, serverFlag, nameFlag) {
32
- const registered = await loadClientConfig(clientDir()).then(() => true, () => false);
33
- if (registered)
34
- return false;
35
- const config = await loadConfig();
36
- const server = resolveServer(config, serverFlag);
37
- let token = await accessTokenFor(server);
38
- if (!token) {
39
- if (!deps.isInteractive()) {
40
- throw new UsageError('No credential for this workstation. Set AXOM_ACCESS_TOKEN (create one under Settings → Security → ' +
41
- 'Access Tokens), or run `axom auth` on a machine with a browser.');
42
- }
43
- await deps.authenticate(server);
44
- token = await ensureFreshToken(server);
45
- }
46
- const cwd = process.cwd();
47
- await deps.registerWorkstation(server, resolveWorkstationName(nameFlag, process.env, hostname()), token, {
48
- name: sanitizeName(basename(cwd), 'default'),
49
- cwd,
50
- shell: process.env.SHELL ?? '/bin/bash',
51
- });
52
- return true;
53
- }
54
- /**
55
- * Removes `AXOM_ACCESS_TOKEN` from *this process's* copy of the environment, right before the bare
56
- * `axom workstation` command becomes a long-lived daemon.
57
- *
58
- * The daemon spawns agents and shell commands with `{ ...process.env, ... }` (exec.ts, acpClient.ts),
59
- * so anything still in here reaches every model-authored command and every dependency postinstall
60
- * that runs in `/workspace` — a container is precisely where this product runs untrusted code. What
61
- * would leak is not the workstation's own key pair (scoped to one workstation, on disk) but a
62
- * credential that authenticates *as the user*: it can register further workstations under their
63
- * account and, with business scopes, call granted MCP tools as them, until somebody notices and
64
- * revokes it. Fixed once here rather than filtered at each of the two spawn sites, so the next spawn site
65
- * inherits the fix instead of having to remember it.
66
- *
67
- * Nothing after this point needs the value. The daemon authenticates with the file credential
68
- * written at registration (`clientKey`/`serverPub`), and the `container` update lifecycle downloads
69
- * its release over that same authenticated management channel.
70
- *
71
- * Unconditional, not gated on "we just registered": a container that still has its `/data/ws` volume
72
- * boots straight past `ensureRegistered`, and that is exactly the long-lived container this protects.
73
- *
74
- * The token stays in the *container's* environment, so a restarted container still registers — only
75
- * this process's copy is dropped. Every reader of it lives before this line: `accessTokenFor`
76
- * (credentials.ts) reads `process.env.AXOM_ACCESS_TOKEN` on its first line, and its only caller here
77
- * is `ensureRegistered` above; `insufficientScopeError`/`envAccessTokenRejectedError` (mcp.ts) read
78
- * it too, but only from the one-shot `axom execute`/`axom struct` CLI processes, which are separate
79
- * processes with their own untouched copy.
80
- *
81
- * `workstation start` deliberately does not call this: it registers and hands off to launchd/systemd
82
- * rather than becoming the daemon, so dropping the variable would affect only a CLI process that is
83
- * about to exit.
84
- */
85
- function dropAccessTokenFromEnvironment() {
86
- delete process.env.AXOM_ACCESS_TOKEN;
87
- }
88
- /**
89
- * Registers `axom workstation start|stop|restart|status|install|uninstall`, the bare
90
- * `axom workstation` (Docker ENTRYPOINT / debugging foreground run), and the hidden top-level
91
- * `axom __daemon`.
92
- *
93
- * `__daemon` is registered on `program` itself, not on the `workstation` subcommand group: the
94
- * generated launchd/systemd service definitions invoke it as `<cliPath> __daemon`, a bare
95
- * top-level argument (see service/launchd.ts and service/systemd.ts).
96
- */
7
+ import { executeDeviceCommand, inspectLocalDevice } from '../workstation/control/deviceCommands.js';
8
+ import { readLocalRequest } from '../workstation/control/commands.js';
9
+ import { LocalControlError } from '../workstation/control/protocol.js';
10
+ import { deviceEventSchema } from '../workstation/control/deviceProtocol.js';
11
+ import { operationIdSchema } from '../workstation/registrations/enrollmentPending.js';
12
+ import { registrationIdSchema } from '../workstation/registrations/model.js';
13
+ import { addWorkstationRegistration, listWorkstationRegistrations, removeWorkstationRegistration } from './workstationRegistrations.js';
97
14
  export function registerWorkstationCommands(program, deps) {
98
- const localJson = deps.localJson ?? runLocalJsonCommand;
99
- const workstation = program.command('workstation').description('Manage the local Axomwork workstation daemon');
100
- // Same idiom as `registerUpdateCommand`: the global `--server` is read at action time, not at
101
- // registration time, so `axom --server <url> workstation start` works. `axom auth` only writes
102
- // `defaultServer` when it is unset, so a user who authenticated against localhost first and
103
- // production second would otherwise have no way to keep `start` off localhost.
15
+ const workstation = program.command('workstation').description('Manage this device and its workstation registration collection');
16
+ const registrations = deps.registrations ?? { add: addWorkstationRegistration, list: listWorkstationRegistrations, remove: removeWorkstationRegistration };
104
17
  const serverOption = () => program.opts().server;
105
- // `workstation` (this group) and its `start` subcommand both declare `--server`/`--name` — the
106
- // group needs its own copy so the *bare* command accepts them, and `start` keeps its copy for its
107
- // own `--help`. But for `axom workstation start --server/--name <value>`, only one of the two
108
- // declarations ever actually captures the flag, and it's not always the same one: `program`
109
- // parses its own options over the *entire* remaining argv before `workstation` gets a look, so
110
- // `--server` — also declared on `program`, globally — is always captured there, leaving both
111
- // `options.server` and `workstationOption().server` dead; `--name` has no `program`-level
112
- // declaration, so it falls through to `workstation`, which *does* capture it, leaving `start`'s
113
- // own `options.name` the dead one instead. `start`'s action below reads the whole fallback chain
114
- // so it doesn't matter which link is live for a given flag.
115
- const workstationOption = () => workstation.opts();
116
18
  const applyRd = deps.configureRd ?? configureRd;
117
- workstation
118
- .command('start')
119
- .option('--json', 'Emit local management protocol JSON')
120
- .option('--input-stdin', 'Read the guarded request from stdin')
121
- .option('--server <url>', 'Axomwork server URL to register this workstation against')
122
- .option('--name <name>', 'Workstation name (default: AXOM_WORKSTATION_NAME, then the hostname)')
123
- .option('--enable-rd', 'Install and enable Linux remote desktop')
124
- .option('--disable-rd', 'Disable remote desktop without changing workstation registration')
125
- .option('--rd-session <mode>', 'Desktop selection: auto, existing, or virtual')
126
- .option('--rd-display <display>', 'Choose an existing local X11 display, such as :10')
127
- .description('Start the workstation user service')
128
- .action((options) => runCommand(async () => {
129
- const rd = { ...workstationOption(), ...options, server: options.server ?? workstationOption().server ?? serverOption() };
130
- if (options.json && (rd.enableRd || rd.disableRd || rd.rdSession || rd.rdDisplay))
131
- throw new UsageError('RD flags cannot be combined with the JSON management protocol');
132
- if (options.json) {
133
- if (!options.inputStdin)
134
- throw new UsageError('--json requires --input-stdin');
135
- await localJson('start');
19
+ const rejectSelectors = (options = {}) => {
20
+ const group = workstation.opts();
21
+ if (options.server !== undefined || options.name !== undefined || group.server !== undefined || group.name !== undefined || serverOption() !== undefined)
22
+ throw new UsageError('Workstation lifecycle commands apply to the whole device. Use `axom workstation add [address]` to add a server/account registration, or `axom workstation remove <registrationId>` to remove one.');
23
+ };
24
+ const removed = () => runCommand(async () => { throw new UsageError('This workstation command was removed. Use `axom workstation add [address]`, `list`, or `remove <registrationId>`; start/stop operate on the whole device.'); });
25
+ const dispatch = async (action, options, task, registrationId) => {
26
+ const target = registrationIdSchema.safeParse(registrationId);
27
+ const operation = { operationId: randomUUID(), registrationId: target.success ? target.data : null };
28
+ const captureOperationId = (value) => {
29
+ const parsed = operationIdSchema.safeParse(value);
30
+ if (!parsed.success)
31
+ throw new LocalControlError('UNSUPPORTED_PROTOCOL', 'Invalid workstation operation correlation ID');
32
+ operation.operationId = parsed.data;
33
+ };
34
+ if (!options.json) {
35
+ if (options.inputStdin)
36
+ throw new UsageError('--input-stdin requires --json');
37
+ if (options.operationId !== undefined)
38
+ captureOperationId(options.operationId);
39
+ await task(operation);
136
40
  return;
137
41
  }
138
- await applyRd(clientDir(), rd);
139
- // `install()` runs only when this call just registered the workstation, not on every
140
- // `start` — it's not the free idempotent no-op it looks like. On macOS it's bootout, write
141
- // plist, bootstrap, and the `deps.start()` right after it is `kickstart -k`: on an
142
- // already-registered machine that's two restart windows where there used to be one, plus a
143
- // bootout window with a documented stranding mode (`launchdStuck`: the service is not
144
- // registered with launchd right now, so nothing will start it). It would also re-pin the
145
- // service's `node` execPath to whatever ran this `start` — which repairs a machine stranded
146
- // by a removed nvm version only if that's a *good* node; run from an ad-hoc one and it
147
- // manufactures the exact stranding it's meant to cure. That repair belongs to the explicit
148
- // verb that already exists for it, `axom workstation install`, not as a side effect here.
149
- await withWorkstationLock(clientDir(), 'operation', async () => { await recoverStoppedIdentity(clientDir()); await assertIdentityCanStart(clientDir()); });
150
- const justRegistered = await ensureRegistered(deps, options.server ?? workstationOption().server ?? serverOption(), options.name ?? workstationOption().name);
151
- await withWorkstationLock(clientDir(), 'operation', async () => { await assertIdentityCanStart(clientDir()); if (justRegistered)
152
- await deps.install(); await deps.start(); });
153
- }));
154
- workstation
155
- .command('stop')
156
- .option('--json', 'Emit local management protocol JSON')
157
- .option('--input-stdin', 'Read the guarded request from stdin')
158
- .description('Stop the workstation user service')
159
- .action((options) => runCommand(async () => { if (options.json) {
160
- if (!options.inputStdin)
161
- throw new UsageError('--json requires --input-stdin');
162
- await localJson('stop');
163
- }
164
- else
165
- await withWorkstationLock(clientDir(), 'operation', () => deps.stop()); }));
166
- workstation
167
- .command('restart')
168
- .option('--json', 'Emit local management protocol JSON')
169
- .option('--input-stdin', 'Read the guarded request from stdin')
170
- .description('Restart the workstation user service')
171
- .action((options) => runCommand(async () => { if (options.json) {
172
- if (!options.inputStdin)
173
- throw new UsageError('--json requires --input-stdin');
174
- await localJson('restart');
175
- }
176
- else
177
- await withWorkstationLock(clientDir(), 'operation', async () => { await recoverStoppedIdentity(clientDir()); await assertIdentityCanStart(clientDir()); await deps.restart(); }); }));
178
- workstation
179
- .command('status')
180
- .description('Report whether the workstation daemon is running')
181
- .action(() => runCommand(async () => { deps.log(await deps.status()); }));
182
- workstation
183
- .command('install')
184
- .option('--json', 'Emit local management protocol JSON')
185
- .option('--input-stdin', 'Read the guarded request from stdin')
186
- .description('Install and enable the operating-system service')
187
- .action((options) => runCommand(async () => { if (options.json) {
188
- if (!options.inputStdin)
189
- throw new UsageError('--json requires --input-stdin');
190
- await localJson('install');
191
- }
192
- else
193
- await withWorkstationLock(clientDir(), 'operation', async () => { await recoverStoppedIdentity(clientDir()); await assertIdentityCanStart(clientDir()); await deps.install(); }); }));
194
- workstation
195
- .command('uninstall')
196
- .description('Disable and remove the operating-system service')
197
- .action(() => runCommand(async () => { await withWorkstationLock(clientDir(), 'operation', () => deps.uninstall()); }));
198
- workstation.command('inspect').requiredOption('--json', 'Emit local management protocol JSON')
199
- .action(() => runCommand(async () => { process.stdout.write(JSON.stringify(await inspectWorkstation(clientDir())) + '\n'); }));
200
- workstation.command('set-enabled').description('Persistently enable or disable this workstation service')
201
- .requiredOption('--json').requiredOption('--input-stdin').action(() => localJson('set-enabled'));
202
- for (const verb of ['register', 'forget', 'begin-remove', 'adopt-owner', 'recover']) {
203
- workstation.command(verb, { hidden: verb === 'forget' }).requiredOption('--json', 'Emit local management protocol JSON')
204
- .requiredOption('--input-stdin', 'Read the guarded request from stdin').action(() => localJson(verb));
42
+ try {
43
+ if (options.operationId !== undefined)
44
+ captureOperationId(options.operationId);
45
+ if (!options.inputStdin) {
46
+ await task(operation);
47
+ return;
48
+ }
49
+ const request = await readLocalRequest(process.stdin);
50
+ if (request && typeof request === 'object' && 'operationId' in request)
51
+ captureOperationId(request.operationId);
52
+ if (!request || typeof request !== 'object' || !('action' in request) || request.action !== action)
53
+ throw new LocalControlError('UNSUPPORTED_PROTOCOL', 'Use a matching v2 device command request');
54
+ if (registrationId !== undefined && (!('guard' in request) || !request.guard || typeof request.guard !== 'object' || !('registrationId' in request.guard) || request.guard.registrationId !== registrationId))
55
+ throw new LocalControlError('BINDING_CONFLICT', 'The guarded removal must match the exact registration ID');
56
+ const result = await executeDeviceCommand(request, clientDir());
57
+ if (result.operationId !== operation.operationId)
58
+ throw new LocalControlError('OUTCOME_UNKNOWN', 'Original workstation operation reply does not match');
59
+ deps.log(JSON.stringify(deviceEventSchema.parse({ schemaVersion: 2, kind: 'result', operationId: result.operationId, result })));
60
+ if (result.status !== 'succeeded')
61
+ process.exitCode = 1;
62
+ }
63
+ catch (error) {
64
+ const code = error instanceof LocalControlError ? error.code : 'SERVICE_FAILED';
65
+ deps.log(JSON.stringify(deviceEventSchema.parse({ schemaVersion: 2, kind: 'result', operationId: operation.operationId, result: { operationId: operation.operationId, registrationId: operation.registrationId, status: code === 'BUSY' ? 'busy' : code === 'OUTCOME_UNKNOWN' ? 'pending' : ['BINDING_CONFLICT', 'OWNER_MISMATCH'].includes(code) ? 'conflict' : 'failed', inspection: null, error: { code, message: error instanceof LocalControlError ? error.message : 'The workstation operation could not be confirmed' } } })));
66
+ process.exitCode = 1;
67
+ }
68
+ };
69
+ workstation.command('list').description('List local workstation registrations and device state').option('--json', 'Emit the v2 device collection').action(options => runCommand(() => dispatch('inspect', options, () => registrations.list(options))));
70
+ workstation.command('add [address]').description('Add an exact server/account registration without changing ordinary CLI login').option('--name <name>', 'Workstation name').option('--json', 'Emit a v2 correlated result').option('--input-stdin', 'Read a protected v2 enrollment request from stdin').option('--operation-id <id>', 'Operation correlation ID').option('--owner <userId>', 'Require this account identity')
71
+ .action((address, options) => runCommand(() => dispatch('add-registration', options, operation => registrations.add({ ...options, operation, name: options.name ?? workstation.opts().name, server: address ?? serverOption(), expectedOwnerUserId: options.owner }))));
72
+ workstation.command('remove <registrationId>').description('Remove this exact local registration; busy resources require explicit --force').option('--force', 'Retire this registration’s resources').option('--json', 'Emit a v2 correlated result').option('--input-stdin', 'Read the original guarded v2 removal request from stdin').option('--operation-id <id>', 'Retry the original operation ID')
73
+ .action((id, options) => runCommand(() => dispatch('remove-registration', options, operation => registrations.remove(id, { ...options, operation }), id)));
74
+ for (const verb of ['start', 'stop', 'restart', 'install', 'uninstall']) {
75
+ const command = workstation.command(verb).description(`${verb} the whole-device service`).option('--name <name>', 'Removed selector; use workstation add').option('--server <url>', 'Removed selector; use workstation add').option('--json', 'Emit a v2 correlated result').option('--input-stdin', 'Read a guarded v2 whole-device request from stdin');
76
+ if (verb === 'start')
77
+ command.option('--enable-rd', 'Install and enable Linux remote desktop').option('--disable-rd', 'Disable remote desktop').option('--rd-session <mode>', 'Desktop selection: auto, existing, or virtual').option('--rd-display <display>', 'Choose a local X11 display');
78
+ command.action(options => runCommand(async () => {
79
+ rejectSelectors(options);
80
+ const rdOptions = { ...workstation.opts(), ...options };
81
+ const hasRd = rdOptions.enableRd || rdOptions.disableRd || rdOptions.rdSession || rdOptions.rdDisplay;
82
+ if (hasRd && (verb !== 'start' || options.json))
83
+ throw new UsageError('RD flags require a plain `axom workstation start` or foreground command');
84
+ if (options.json && !options.inputStdin)
85
+ throw new UsageError('--json requires --input-stdin for device lifecycle commands');
86
+ if (hasRd)
87
+ await applyRd(clientDir(), rdOptions);
88
+ await dispatch(verb, options, () => deps[verb]());
89
+ }));
205
90
  }
206
- const rd = workstation.command('rd').description('Manage Linux remote desktop independently of the workstation service');
91
+ workstation.command('status').description('Report the whole-device service status').action(() => runCommand(async () => { rejectSelectors(); deps.log(await deps.status()); }));
92
+ workstation.command('inspect').requiredOption('--json', 'Emit the v2 device collection').action(() => runCommand(async () => { deps.log(JSON.stringify(await inspectLocalDevice(clientDir()))); }));
93
+ for (const verb of ['auth', 'register', 'forget', 'begin-remove', 'adopt-owner', 'recover', 'set-enabled'])
94
+ workstation.command(verb, { hidden: true }).allowUnknownOption().allowExcessArguments().action(removed);
95
+ const rd = workstation.command('rd').description('Manage Linux remote desktop');
207
96
  rd.command('status').action(() => runCommand(async () => { deps.log(JSON.stringify(await rdStatus(clientDir()), null, 2)); }));
208
- rd.command('disable').action(() => runCommand(async () => { await applyRd(clientDir(), { disableRd: true }); }));
209
- workstation
210
- .option('--server <url>', 'Axomwork server URL to register against (default: AXOM_SERVER, then the stored default)')
211
- .option('--name <name>', 'Workstation name (default: AXOM_WORKSTATION_NAME, then the hostname)')
212
- .option('--enable-rd', 'Install and enable Linux remote desktop')
213
- .option('--disable-rd', 'Disable remote desktop without changing workstation registration')
214
- .option('--rd-session <mode>', 'Desktop selection: auto, existing, or virtual')
215
- .option('--rd-display <display>', 'Choose an existing local X11 display, such as :10')
216
- .description('Run the workstation daemon in the foreground (Docker ENTRYPOINT / debugging); subcommands manage the OS service')
97
+ rd.command('disable').action(() => runCommand(() => applyRd(clientDir(), { disableRd: true })));
98
+ workstation.option('--server <url>', 'Removed registration shortcut; use workstation add').option('--name <name>', 'Removed registration shortcut; use workstation add')
99
+ .option('--enable-rd', 'Install and enable Linux remote desktop').option('--disable-rd', 'Disable remote desktop').option('--rd-session <mode>', 'Desktop selection: auto, existing, or virtual').option('--rd-display <display>', 'Choose a local X11 display')
217
100
  .action((options) => runCommand(async () => {
218
- await applyRd(clientDir(), { ...options, server: options.server ?? serverOption() });
219
- await ensureRegistered(deps, options.server ?? serverOption(), options.name);
220
- dropAccessTokenFromEnvironment();
101
+ rejectSelectors(options);
102
+ await applyRd(clientDir(), options);
103
+ delete process.env.AXOM_ACCESS_TOKEN;
221
104
  await deps.runDaemon({ supervision: deps.detectSupervision() });
222
105
  }));
223
- program.command('__rd-desktop', { hidden: true }).requiredOption('--directory <path>')
224
- .action((options) => runCommand(async () => { dropAccessTokenFromEnvironment(); await runManagedDesktop(options.directory); }));
225
- program.command('__daemon', { hidden: true }).action(() => runCommand(async () => {
226
- // The literal exec target launchd/systemd start: the one call site allowed to opt into
227
- // OS-service treatment. See RunDaemonOptions in daemon.ts for why this must be an explicit
228
- // flag rather than "no `supervision` was passed".
229
- //
230
- // Same reasoning as the bare command's call: this process is about to become the daemon that
231
- // spawns agents and shell commands with its own environment. A service definition is not
232
- // supposed to carry AXOM_ACCESS_TOKEN at all, which makes this usually a no-op — and exactly
233
- // why it belongs here rather than being left to whoever writes the next plist.
234
- dropAccessTokenFromEnvironment();
235
- await deps.runDaemon({ supervisedByServiceManager: true });
236
- }));
106
+ program.command('__rd-desktop', { hidden: true }).requiredOption('--directory <path>').action((options) => runCommand(async () => { delete process.env.AXOM_ACCESS_TOKEN; await runManagedDesktop(options.directory); }));
107
+ program.command('__daemon', { hidden: true }).action(() => runCommand(async () => { delete process.env.AXOM_ACCESS_TOKEN; await deps.runDaemon({ supervisedByServiceManager: true }); }));
237
108
  }
@@ -0,0 +1,116 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { hostname } from 'node:os';
3
+ import { basename } from 'node:path';
4
+ import { loadConfig } from '../config.js';
5
+ import { peekAccessToken, revokeRefreshToken } from '../credentials.js';
6
+ import { resolveAuthServer } from './auth.js';
7
+ import { clientDir } from '../workstation/config.js';
8
+ import { executeDeviceCommand, inspectLocalDevice } from '../workstation/control/deviceCommands.js';
9
+ import { deviceEventSchema, deviceResultSchema, parseDeviceInspection } from '../workstation/control/deviceProtocol.js';
10
+ import { LocalControlError } from '../workstation/control/protocol.js';
11
+ import { normalizeWorkstationServer } from '../workstation/control/serverIdentity.js';
12
+ import { verifyRegistrationAuthority } from '../workstation/registrations/enrollment.js';
13
+ import { authorizeDevice } from '../workstation/oauth/deviceAuthorization.js';
14
+ import { resolveWorkstationName, sanitizeName } from '../workstation/names.js';
15
+ import { operationIdSchema } from '../workstation/registrations/enrollmentPending.js';
16
+ import { registrationIdSchema } from '../workstation/registrations/model.js';
17
+ const defaults = {
18
+ inspect: () => inspectLocalDevice(clientDir()), execute: request => executeDeviceCommand(request, clientDir()),
19
+ authority: verifyRegistrationAuthority, authorize: authorizeDevice, revoke: revokeRefreshToken, log: console.log,
20
+ };
21
+ export async function resolveWorkstationAddServer(explicit) {
22
+ return resolveAuthServer(await loadConfig(), explicit);
23
+ }
24
+ function printResult(result, options, log) {
25
+ result = deviceResultSchema.parse(result);
26
+ if (options.json)
27
+ log(JSON.stringify(deviceEventSchema.parse({ schemaVersion: 2, kind: 'result', operationId: result.operationId, result })));
28
+ else
29
+ log(`${result.status}: ${result.registrationId ?? 'device'} (operation ${result.operationId})${result.error ? ` — ${result.error.code}: ${result.error.message}` : ''}`);
30
+ if (result.status !== 'succeeded')
31
+ process.exitCode = 1;
32
+ }
33
+ export async function listWorkstationRegistrations(options = {}, overrides = {}) {
34
+ const deps = { ...defaults, ...overrides };
35
+ const inspection = parseDeviceInspection(await deps.inspect());
36
+ if (options.json) {
37
+ deps.log(JSON.stringify(inspection));
38
+ return;
39
+ }
40
+ deps.log(`Workstation device: ${inspection.runtime.state}`);
41
+ if (!inspection.registrations.length)
42
+ deps.log('No workstation registrations. Use `axom workstation add [address]`.');
43
+ for (const registration of inspection.registrations)
44
+ deps.log(`${registration.id}\t${registration.server}\taccount=${registration.ownerUserId}\tworkstation=${registration.workstationId}\t${registration.connection}\t${registration.identityState}\t${registration.removalState}`);
45
+ for (const pending of inspection.pending)
46
+ deps.log(`pending: ${pending.id}\t${pending.kind}\t${pending.registrationId ?? 'device'}\t${pending.phase}\t${pending.outcome}`);
47
+ }
48
+ export async function addWorkstationRegistration(options = {}, overrides = {}) {
49
+ const deps = { ...defaults, ...overrides };
50
+ const operation = options.operation ?? { operationId: operationIdSchema.parse(options.operationId ?? randomUUID()), registrationId: null };
51
+ const server = normalizeWorkstationServer(await resolveWorkstationAddServer(options.server));
52
+ const before = parseDeviceInspection(await deps.inspect());
53
+ if (before.runtime.state === 'unknown')
54
+ throw new LocalControlError('OUTCOME_UNKNOWN', 'Verify the workstation owner before adding a registration');
55
+ const operationId = operationIdSchema.parse(operation.operationId);
56
+ const hasExplicitAccessToken = Boolean(process.env.AXOM_ACCESS_TOKEN?.trim());
57
+ let token = await peekAccessToken(server);
58
+ let ownerUserId;
59
+ if (token) {
60
+ try {
61
+ ownerUserId = await deps.authority({ server, accessToken: token, expectedOwnerUserId: options.expectedOwnerUserId });
62
+ }
63
+ catch (error) {
64
+ if (!(error instanceof LocalControlError))
65
+ throw error;
66
+ if (error.code !== 'AUTHORIZATION_REQUIRED' && (error.code !== 'OWNER_MISMATCH' || hasExplicitAccessToken))
67
+ throw error;
68
+ token = undefined;
69
+ }
70
+ }
71
+ let issued;
72
+ try {
73
+ if (!token) {
74
+ if (!options.json)
75
+ deps.log('Workstation permission needs a separate approval; unavailable or expired ordinary access credentials are left unchanged.');
76
+ const host = hostname();
77
+ issued = await deps.authorize(server, `Axomwork workstation (${host})`, host, { print: message => { process.stderr.write(message + '\n'); } }, ['workstation'], Object.freeze({ deviceId: `workstation-${randomUUID()}` }));
78
+ token = issued.accessToken;
79
+ ownerUserId = await deps.authority({ server, accessToken: token, expectedOwnerUserId: options.expectedOwnerUserId });
80
+ }
81
+ const cwd = process.cwd();
82
+ const result = await deps.execute({ schemaVersion: 2, action: 'add-registration', operationId,
83
+ guard: { deviceRevision: before.deviceRevision, runtimeGeneration: before.runtime.generation },
84
+ input: { operationId, server, accessToken: token, expectedOwnerUserId: ownerUserId, defaults: { name: resolveWorkstationName(options.name, process.env, hostname()), shell: process.env.SHELL ?? '/bin/sh', workspace: { name: sanitizeName(basename(cwd), 'default'), cwd, shell: process.env.SHELL ?? '/bin/sh' } } },
85
+ });
86
+ printResult(result, options, deps.log);
87
+ }
88
+ finally {
89
+ if (issued)
90
+ await deps.revoke(server, issued.refreshToken);
91
+ }
92
+ }
93
+ export async function removeWorkstationRegistration(id, options = {}, overrides = {}) {
94
+ const deps = { ...defaults, ...overrides };
95
+ const operation = options.operation ?? { operationId: operationIdSchema.parse(options.operationId ?? randomUUID()), registrationId: null };
96
+ const target = registrationIdSchema.safeParse(id);
97
+ if (!target.success) {
98
+ operation.registrationId = null;
99
+ throw new LocalControlError('BINDING_CONFLICT', 'Invalid registration ID; use `axom workstation list`');
100
+ }
101
+ operation.registrationId = target.data;
102
+ const before = parseDeviceInspection(await deps.inspect());
103
+ const registration = before.registrations.find(candidate => candidate.id === id);
104
+ if (!registration)
105
+ throw new LocalControlError('BINDING_CONFLICT', 'No registration matches that exact ID; use `axom workstation list`');
106
+ const pending = before.pending.find(operation => operation.kind === 'remove' && operation.registrationId === id);
107
+ const operationId = operationIdSchema.parse(options.operationId ?? pending?.id ?? operation.operationId);
108
+ operation.operationId = operationId;
109
+ if (before.runtime.state !== 'running' || before.runtime.generation === null) {
110
+ printResult({ operationId, registrationId: id, status: 'pending', inspection: before, error: { code: 'OUTCOME_UNKNOWN', message: pending ? 'Original removal authority is unavailable; preserve its pending operation and data for verification' : 'Removal needs the authenticated running device owner; start the device, inspect, and retry' } }, options, deps.log);
111
+ return;
112
+ }
113
+ printResult(await deps.execute({ schemaVersion: 2, action: 'remove-registration', operationId, force: options.force === true,
114
+ guard: { deviceRevision: before.deviceRevision, runtimeGeneration: before.runtime.generation, registrationId: id, registrationRevision: registration.revision },
115
+ }), options, deps.log);
116
+ }
@@ -30,9 +30,17 @@ export async function requestCredentialBroker(request) {
30
30
  return await sendRequest(brokerRequest, controller.signal);
31
31
  }
32
32
  catch (error) {
33
- if (error instanceof BrokerOperationError)
33
+ if (error instanceof BrokerOperationError) {
34
+ if (request.action === 'peek' && error.message === 'Unsupported credential broker operation.')
35
+ return { ok: true, version: PROTOCOL_VERSION };
34
36
  throw error;
37
+ }
35
38
  throwIfAborted(controller.signal);
39
+ if (request.action === 'peek') {
40
+ if (['ENOENT', 'ECONNREFUSED'].includes(error.code ?? ''))
41
+ return { ok: true, version: PROTOCOL_VERSION };
42
+ throw error;
43
+ }
36
44
  await installAndStartBroker(controller.signal);
37
45
  let lastError;
38
46
  for (let attempt = 0; attempt < 20; attempt += 1) {
@@ -129,6 +137,9 @@ async function handleRequest(raw, connection, credentials) {
129
137
  const accessToken = await credentials.accessTokenFromNativeStore(request.server, request.forceRefresh);
130
138
  response = { ok: true, accessToken, version: PROTOCOL_VERSION };
131
139
  }
140
+ else if (request.action === 'peek') {
141
+ response = { ok: true, accessToken: credentials.peekNativeAccessToken(request.server), version: PROTOCOL_VERSION };
142
+ }
132
143
  else if (request.action === 'revoke') {
133
144
  const persisted = await credentials.loadNativeTokens(request.server);
134
145
  if (persisted?.refreshToken)
@@ -319,7 +330,7 @@ function parseRequest(raw) {
319
330
  const request = JSON.parse(raw);
320
331
  if (request.version !== PROTOCOL_VERSION)
321
332
  throw new Error('Unsupported credential broker protocol version.');
322
- if (request.action !== 'save' && request.action !== 'access' && request.action !== 'clear' && request.action !== 'revoke') {
333
+ if (request.action !== 'save' && request.action !== 'access' && request.action !== 'clear' && request.action !== 'revoke' && request.action !== 'peek') {
323
334
  throw new Error('Unsupported credential broker operation.');
324
335
  }
325
336
  if (typeof request.server !== 'string' || !request.server)
@@ -34,6 +34,22 @@ export async function accessTokenFor(server, forceRefresh = false) {
34
34
  }
35
35
  return accessTokenFromNativeStore(server, forceRefresh);
36
36
  }
37
+ export function peekNativeAccessToken(server) {
38
+ const cached = accessCache.get(server);
39
+ return cached?.accessToken && Date.parse(cached.expiresAt) - Date.now() >= 30_000 ? cached.accessToken : undefined;
40
+ }
41
+ export async function peekAccessToken(server) {
42
+ const fromEnv = process.env.AXOM_ACCESS_TOKEN?.trim();
43
+ if (fromEnv)
44
+ return fromEnv;
45
+ if (usesTestMemoryStore()) {
46
+ const cached = memoryStore.get(server);
47
+ return cached?.accessToken && Date.parse(cached.expiresAt) - Date.now() >= 30_000 ? cached.accessToken : undefined;
48
+ }
49
+ if (process.platform === 'darwin')
50
+ return (await requestCredentialBroker({ action: 'peek', server })).accessToken;
51
+ return peekNativeAccessToken(server);
52
+ }
37
53
  export async function clearTokens(server) {
38
54
  if (usesTestMemoryStore()) {
39
55
  memoryStore.delete(server);
@@ -41,8 +41,8 @@ $ARGUMENTS
41
41
  Workflow:
42
42
  1. Find the operation on its model (\`axom__app_struct__get_model\`) or the service by name.
43
43
  2. Call \`axom__execute__run_operation\` (applicationName/modelName/operationName/input) or \`axom__execute__run_service\` (serviceName/input).
44
- 3. Interpret the ExecuteResult payload: \`status\` (success/failure/running), \`message\`, \`text\`, \`returnValue\`, \`runId\`/\`runType\`, \`errors[]\` ({target,code,message}).
45
- 4. When \`status\` is \`running\`, follow \`runId\` (see \`runType\`) until a terminal state before reporting the outcome.
44
+ 3. Interpret the ExecuteResult payload: \`status\` (success/failure/running), \`message\`, \`text\`, \`returnValue\`, \`workId\`/\`workKind\`, \`errors[]\` ({target,code,message}).
45
+ 4. When \`status\` is \`running\`, follow \`workId\` (see \`workKind\`) until a terminal state before reporting the outcome.
46
46
 
47
47
  For sync-vs-job semantics see knowledge page \`concepts/execution/synchronous-and-task-execution.md\`; for run following see knowledge page \`agent-integration/operating-patterns/following-runs.md\`.
48
48
  `,