@platless/axom-cli 0.5.9 → 0.6.36

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 (163) hide show
  1. package/README.md +18 -5
  2. package/dist/axom-patch/cli/README.md +100 -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/codex-account-limits.json +11 -11
  7. package/dist/axom-patch/cli/codex-terminal-output.json +16 -0
  8. package/dist/axom-patch/cli/commandcode-account-limits.json +9 -9
  9. package/dist/axom-patch/cli/kimi-quota.mjs +12 -0
  10. package/dist/axom-patch/cli/kimi-subagents.mjs +173 -0
  11. package/dist/axom-patch/cli/nightly-task.md +55 -13
  12. package/dist/axom-patch/cli/probes/codex-terminal-output.mjs +33 -0
  13. package/dist/axom-patch/cli/probes/kimi-subagent-subwork.mjs +82 -0
  14. package/dist/axom-patch/cli/qoder/dist/axom-acp-compat.mjs +1591 -0
  15. package/dist/axom-patch/cli/qoder/releases/1.1.75.json +360 -0
  16. package/dist/axom-patch/cli/qoder-background-terminal.md +13 -6
  17. package/dist/axom-patch/cli/qoder-foreground-output.md +14 -8
  18. package/dist/axom-patch/cli/qoder-native-acp.json +227 -0
  19. package/dist/axom-patch/cli/qoder-steering.md +20 -25
  20. package/dist/axom-patch/cli/qoder-subagent-subwork.md +17 -68
  21. package/dist/axom-patch/cli/runtime.json +16 -18
  22. package/dist/axom-patch/cli/zcode-account-limits.json +10 -11
  23. package/dist/axom-patch/cli/zcode-models.json +1 -2
  24. package/dist/axom-patch/cli/zcode-subagent-subwork.json +9 -10
  25. package/dist/cli.js +4 -36
  26. package/dist/commands/knowledge.js +47 -0
  27. package/dist/commands/update.js +65 -3
  28. package/dist/commands/workstation.js +73 -7
  29. package/dist/install/claude/commands.js +13 -13
  30. package/dist/mcp.js +1 -1
  31. package/dist/native/darwin-arm64/axom-quic +0 -0
  32. package/dist/workstation/acp/acpAgents.js +102 -98
  33. package/dist/workstation/acp/acpClient.js +26 -0
  34. package/dist/workstation/acp/builds/install/npm.js +99 -10
  35. package/dist/workstation/acp/initializeResponse.js +6 -1
  36. package/dist/workstation/acp/managedPatches.js +189 -25
  37. package/dist/workstation/acp/managedRuntime.js +314 -0
  38. package/dist/workstation/acp/patchAssets.js +40 -0
  39. package/dist/workstation/acp/processor/checkpoint.js +25 -34
  40. package/dist/workstation/acp/processor/control.js +5 -78
  41. package/dist/workstation/acp/processor/runtime.js +5 -81
  42. package/dist/workstation/acp/processor/session.js +51 -80
  43. package/dist/workstation/acp/processor/state.js +6 -13
  44. package/dist/workstation/acp/processor/step.js +3 -20
  45. package/dist/workstation/acp/processor/test/fixtureLog.js +9 -16
  46. package/dist/workstation/acp/processor/updates.js +131 -105
  47. package/dist/workstation/acp/processorSession.js +8 -172
  48. package/dist/workstation/acp/registry/catalog.js +65 -39
  49. package/dist/workstation/acp/registry/launch.js +16 -1
  50. package/dist/workstation/acp/switching.js +0 -3
  51. package/dist/workstation/acp/test/fakeAgent.js +13 -0
  52. package/dist/workstation/acp/test/keeperWorld.js +28 -0
  53. package/dist/workstation/acp/test/raw/sources.js +16 -130
  54. package/dist/workstation/acp/test/realAdapter.js +1 -47
  55. package/dist/workstation/acp/test/scriptAgent.js +11 -3
  56. package/dist/workstation/agentFlow.js +1 -1
  57. package/dist/workstation/agentSessions.js +38 -143
  58. package/dist/workstation/channels/management.js +95 -11
  59. package/dist/workstation/channels/url.js +1 -1
  60. package/dist/workstation/config.js +16 -4
  61. package/dist/workstation/connection/encryptedChannel.js +4 -1
  62. package/dist/workstation/control/bindingStore.js +200 -0
  63. package/dist/workstation/control/commands.js +124 -0
  64. package/dist/workstation/control/daemonIdentity.js +53 -0
  65. package/dist/workstation/control/enrollment.js +106 -0
  66. package/dist/workstation/control/inspect.js +71 -0
  67. package/dist/workstation/control/locks.js +63 -0
  68. package/dist/workstation/control/protocol.js +40 -0
  69. package/dist/workstation/control/restartGuard.js +46 -0
  70. package/dist/workstation/control/runtimeClient.js +97 -0
  71. package/dist/workstation/control/runtimeHost.js +148 -0
  72. package/dist/workstation/control/serverIdentity.js +35 -0
  73. package/dist/workstation/daemon.js +106 -6
  74. package/dist/workstation/daemonctl.js +77 -14
  75. package/dist/workstation/desktopControl/audio.js +99 -0
  76. package/dist/workstation/desktopControl/capabilities.js +92 -0
  77. package/dist/workstation/desktopControl/captureModes.js +9 -0
  78. package/dist/workstation/desktopControl/captureTargetLabel.js +9 -0
  79. package/dist/workstation/desktopControl/deviceSockets.js +246 -0
  80. package/dist/workstation/desktopControl/helperProcess.js +353 -0
  81. package/dist/workstation/desktopControl/lobby.js +129 -0
  82. package/dist/workstation/desktopControl/macProvider.js +155 -0
  83. package/dist/workstation/desktopControl/media.js +331 -0
  84. package/dist/workstation/desktopControl/nativeProvider.js +1018 -0
  85. package/dist/workstation/desktopControl/networkEvidence.js +43 -0
  86. package/dist/workstation/desktopControl/overview.js +58 -0
  87. package/dist/workstation/desktopControl/protocol.js +302 -0
  88. package/dist/workstation/desktopControl/quicMedia.js +240 -0
  89. package/dist/workstation/desktopControl/rateController.js +226 -0
  90. package/dist/workstation/desktopControl/supervisor.js +1681 -0
  91. package/dist/workstation/desktopControl/transport.js +27 -0
  92. package/dist/workstation/desktopControl/wallpaper.js +78 -0
  93. package/dist/workstation/files/credentials.js +142 -0
  94. package/dist/workstation/files/text.js +42 -8
  95. package/dist/workstation/keepers/kit/bundle.js +21 -17
  96. package/dist/workstation/keepers/kit/launcher.js +47 -7
  97. package/dist/workstation/keepers/kit/paths.js +2 -1
  98. package/dist/workstation/managedInstall.js +57 -0
  99. package/dist/workstation/protocol/agent.js +2 -15
  100. package/dist/workstation/protocol/management.js +93 -4
  101. package/dist/workstation/protocol/runtimeDiagnostics.js +15 -0
  102. package/dist/workstation/rd/artifact.js +119 -0
  103. package/dist/workstation/rd/control.js +119 -0
  104. package/dist/workstation/rd/display.js +29 -0
  105. package/dist/workstation/rd/provider.js +222 -0
  106. package/dist/workstation/rd/sessions.js +391 -0
  107. package/dist/workstation/rd/state.js +45 -0
  108. package/dist/workstation/rd/update.js +171 -0
  109. package/dist/workstation/rd/upkeep.js +38 -0
  110. package/dist/workstation/register.js +27 -37
  111. package/dist/workstation/service/installer.js +104 -13
  112. package/dist/workstation/service/launchd.js +1 -0
  113. package/dist/workstation/storageMetrics.js +118 -0
  114. package/dist/workstation/systemMetrics.js +44 -92
  115. package/dist/workstation/toolchain.js +94 -0
  116. package/dist/workstation/tools/browser.js +28 -4
  117. package/dist/workstation/update/artifact.js +2 -1
  118. package/dist/workstation/update/globalInstall.js +116 -0
  119. package/dist/workstation/update/npmInstaller.js +49 -23
  120. package/dist/workstation/update/rdRollback.js +145 -0
  121. package/dist/workstation/update/runUpdate.js +3 -2
  122. package/dist/workstation/update/watchdog.js +16790 -334
  123. package/package.json +5 -5
  124. package/skills/axom/SKILL.md +11 -11
  125. package/skills/axom-definition/SKILL.md +3 -3
  126. package/skills/axom-platform/SKILL.md +3 -3
  127. package/dist/axom-patch/cli/probes/qoder-steering.mjs +0 -89
  128. package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +0 -61
  129. package/dist/axom-patch/cli/qoder-steering.json +0 -52
  130. package/dist/axom-patch/cli/qoder-subagent-origin.json +0 -73
  131. package/dist/commands/guide.js +0 -38
  132. package/dist/runtime-deps/qoder-sdk/package-lock.json +0 -1189
  133. package/dist/runtime-deps/qoder-sdk/package.json +0 -8
  134. package/dist/workstation/acp/delegationMcp.js +0 -24
  135. package/dist/workstation/acp/processor/sideChannel.js +0 -80
  136. package/dist/workstation/acp/qoderStartup.js +0 -45
  137. package/dist/workstation/acp/test/raw/qoderCaptures.js +0 -125
  138. package/dist/workstation/qoderAdapter/backgroundProcess.js +0 -118
  139. package/dist/workstation/qoderAdapter/backgroundTerminal.js +0 -193
  140. package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +0 -372
  141. package/dist/workstation/qoderAdapter/callbackContext.js +0 -76
  142. package/dist/workstation/qoderAdapter/completedContinuation.js +0 -139
  143. package/dist/workstation/qoderAdapter/contract.js +0 -78
  144. package/dist/workstation/qoderAdapter/foregroundOutput.js +0 -36
  145. package/dist/workstation/qoderAdapter/history.js +0 -85
  146. package/dist/workstation/qoderAdapter/identityIndex.js +0 -191
  147. package/dist/workstation/qoderAdapter/inputQueue.js +0 -42
  148. package/dist/workstation/qoderAdapter/main.js +0 -95
  149. package/dist/workstation/qoderAdapter/mapper.js +0 -117
  150. package/dist/workstation/qoderAdapter/observerPlugin.js +0 -245
  151. package/dist/workstation/qoderAdapter/observerReceiver.js +0 -187
  152. package/dist/workstation/qoderAdapter/permissions.js +0 -61
  153. package/dist/workstation/qoderAdapter/profile.js +0 -93
  154. package/dist/workstation/qoderAdapter/sdkDriver.js +0 -48
  155. package/dist/workstation/qoderAdapter/sdkRuntime.js +0 -93
  156. package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +0 -145
  157. package/dist/workstation/qoderAdapter/session.js +0 -319
  158. package/dist/workstation/qoderAdapter/subagentMeta.js +0 -28
  159. package/dist/workstation/qoderAdapter/subwork.js +0 -85
  160. package/dist/workstation/qoderAdapter/taskControl.js +0 -38
  161. package/dist/workstation/qoderAdapter/taskLedger.js +0 -163
  162. package/dist/workstation/qoderAdapter/usage.js +0 -37
  163. /package/dist/workstation/{qoderAdapter/types.js → desktopControl/provider.js} +0 -0
@@ -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,97 @@
1
+ import net from 'node:net';
2
+ import { runtimePaths, readControlFile, runtimeOwnerLive } from './runtimeHost.js';
3
+ import { runtimeSnapshotSchema, stoppedRuntime, LocalControlError } from './protocol.js';
4
+ import { pidAlive } from '../keepers/kit/lock.js';
5
+ export async function runtimeRequest(directory, action, expectedGeneration, extra = {}) {
6
+ const paths = runtimePaths(directory);
7
+ const owner = JSON.parse(await readControlFile(paths.ownerFile));
8
+ if (typeof owner.generation !== 'string' || !pidAlive(owner.pid))
9
+ throw new LocalControlError('OUTCOME_UNKNOWN', 'Runtime owner cannot be verified');
10
+ if (expectedGeneration && owner.generation !== expectedGeneration)
11
+ throw new LocalControlError('BINDING_CONFLICT', 'Runtime changed');
12
+ const token = await readControlFile(paths.tokenFile);
13
+ return new Promise((resolve, reject) => {
14
+ const socket = net.createConnection(paths.socket);
15
+ let data = '';
16
+ let done = false;
17
+ const fail = (error) => { if (!done) {
18
+ done = true;
19
+ socket.destroy();
20
+ reject(error);
21
+ } };
22
+ socket.setTimeout(3000, () => fail(new LocalControlError('OUTCOME_UNKNOWN', 'Runtime did not respond')));
23
+ socket.on('error', () => fail(new LocalControlError('OUTCOME_UNKNOWN', 'Cannot contact runtime')));
24
+ socket.on('end', () => { if (!done)
25
+ fail(new LocalControlError('OUTCOME_UNKNOWN', 'Incomplete runtime response')); });
26
+ socket.on('connect', () => socket.write(JSON.stringify({ ...extra, action, generation: owner.generation, token }) + '\n'));
27
+ socket.on('data', chunk => {
28
+ data += chunk.toString();
29
+ if (Buffer.byteLength(data) > 65_536) {
30
+ fail(Error('Oversized response'));
31
+ return;
32
+ }
33
+ if (!data.includes('\n') || done)
34
+ return;
35
+ try {
36
+ const result = JSON.parse(data.split('\n')[0]);
37
+ if (!result.ok)
38
+ throw new LocalControlError(result.code === 'BINDING_CONFLICT' ? result.code : 'SERVICE_FAILED', 'Runtime rejected the request');
39
+ done = true;
40
+ socket.destroy();
41
+ resolve(result.value);
42
+ }
43
+ catch (error) {
44
+ fail(error);
45
+ }
46
+ });
47
+ });
48
+ }
49
+ export async function inspectRuntime(directory) {
50
+ try {
51
+ return runtimeSnapshotSchema.parse(await runtimeRequest(directory, 'inspect'));
52
+ }
53
+ catch (error) {
54
+ try {
55
+ const owner = JSON.parse(await readControlFile(runtimePaths(directory).ownerFile));
56
+ if (Number.isSafeInteger(owner.pid) && owner.pid > 0 && typeof owner.command === 'string' && typeof owner.generation === 'string' && !await runtimeOwnerLive(owner))
57
+ return stoppedRuntime();
58
+ }
59
+ catch { /* Partial ownership evidence remains unknown. */ }
60
+ if (error.code === 'ENOENT') {
61
+ // No control files at all is stopped; partial state must remain uncertain.
62
+ const p = runtimePaths(directory);
63
+ const { promises: fs } = await import('node:fs');
64
+ try {
65
+ await fs.lstat(p.socket);
66
+ }
67
+ catch (e) {
68
+ if (e.code === 'ENOENT') {
69
+ try {
70
+ await fs.lstat(p.ownerFile);
71
+ }
72
+ catch (e2) {
73
+ if (e2.code === 'ENOENT')
74
+ return stoppedRuntime();
75
+ }
76
+ }
77
+ }
78
+ }
79
+ return { ...stoppedRuntime(), state: 'unknown', connection: 'unknown' };
80
+ }
81
+ }
82
+ export async function stopRuntime(directory, expectedGeneration) {
83
+ await runtimeRequest(directory, 'stop', expectedGeneration);
84
+ const deadline = Date.now() + 10_000;
85
+ while (Date.now() < deadline) {
86
+ const state = await inspectRuntime(directory);
87
+ if (state.state === 'stopped')
88
+ return;
89
+ if (state.generation && state.generation !== expectedGeneration)
90
+ throw new LocalControlError('BINDING_CONFLICT', 'A new daemon started');
91
+ await new Promise(resolve => setTimeout(resolve, 50));
92
+ }
93
+ throw new LocalControlError('OUTCOME_UNKNOWN', 'Workstation shutdown has not been confirmed');
94
+ }
95
+ export async function requestRuntimeUpdate(directory, release, operationId) {
96
+ return await runtimeRequest(directory, 'update', undefined, { release, operationId });
97
+ }
@@ -0,0 +1,148 @@
1
+ import { createHash, randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
2
+ import { promises as fs } from 'node:fs';
3
+ import net from 'node:net';
4
+ import path from 'node:path';
5
+ import { privateDirectory, socketLimit } from '../keepers/kit/paths.js';
6
+ import { pidAlive, processCommand } from '../keepers/kit/lock.js';
7
+ import { processStartTime } from '../keepers/kit/orphanSweep.js';
8
+ import { LocalControlError } from './protocol.js';
9
+ import { withPrivateLock } from './locks.js';
10
+ export function runtimePaths(directory) {
11
+ const root = path.join(path.resolve(directory), 'control');
12
+ const key = createHash('sha256').update(`${process.getuid?.() ?? 'user'}:${path.resolve(directory)}`).digest('hex').slice(0, 24);
13
+ const local = path.join(root, 'runtime.sock');
14
+ return { socket: Buffer.byteLength(local) < socketLimit() ? local : `/tmp/axom-control-${process.getuid?.() ?? 'user'}/${key}.sock`, tokenFile: path.join(root, 'runtime.token'), ownerFile: path.join(root, 'runtime.owner.json') };
15
+ }
16
+ export async function readControlFile(file) {
17
+ const stat = await fs.lstat(file);
18
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 65_536 || (process.getuid && stat.uid !== process.getuid()) || (stat.mode & 0o077))
19
+ throw Error('Unsafe control file');
20
+ return fs.readFile(file, 'utf8');
21
+ }
22
+ /** A persisted PID can belong to another process after a container restart or reboot. */
23
+ export async function runtimeOwnerLive(owner) {
24
+ if (!pidAlive(owner.pid))
25
+ return false;
26
+ const command = await processCommand(owner.pid);
27
+ if (command !== null && command !== owner.command)
28
+ return false;
29
+ if (typeof owner.startTime === 'string') {
30
+ const startTime = await processStartTime(owner.pid);
31
+ if (startTime !== null && startTime !== owner.startTime)
32
+ return false;
33
+ }
34
+ // Inability to read the process identity is not evidence that it stopped.
35
+ return true;
36
+ }
37
+ export async function openRuntimeHost(options) {
38
+ const paths = runtimePaths(options.directory);
39
+ await privateDirectory(path.dirname(paths.ownerFile));
40
+ await privateDirectory(path.dirname(paths.socket));
41
+ return withPrivateLock(path.join(options.directory, 'control/runtime-start'), async () => {
42
+ try {
43
+ const owner = JSON.parse(await readControlFile(paths.ownerFile));
44
+ if (!Number.isSafeInteger(owner.pid) || owner.pid <= 0 || typeof owner.command !== 'string')
45
+ throw Error('Unknown runtime owner');
46
+ if (await runtimeOwnerLive(owner))
47
+ throw new LocalControlError('INSTANCE_OCCUPIED', 'A workstation daemon already owns this computer');
48
+ }
49
+ catch (error) {
50
+ if (error.code !== 'ENOENT') {
51
+ if (error instanceof LocalControlError)
52
+ throw error;
53
+ throw new LocalControlError('INSTANCE_OCCUPIED', 'Cannot verify the existing workstation daemon');
54
+ }
55
+ // A socket with no owner cannot safely be reclaimed.
56
+ try {
57
+ await fs.lstat(paths.socket);
58
+ throw new LocalControlError('INSTANCE_OCCUPIED', 'Unverified workstation socket');
59
+ }
60
+ catch (e) {
61
+ if (e.code !== 'ENOENT')
62
+ throw e;
63
+ }
64
+ }
65
+ await fs.rm(paths.socket, { force: true });
66
+ const generation = randomUUID();
67
+ const token = randomBytes(32).toString('hex');
68
+ const binding = { ...options.binding };
69
+ let closed = false;
70
+ const clients = new Set();
71
+ const server = net.createServer(socket => {
72
+ clients.add(socket);
73
+ socket.on('close', () => clients.delete(socket));
74
+ socket.on('error', () => { });
75
+ socket.setTimeout(3000, () => socket.destroy());
76
+ let body = '';
77
+ let handled = false;
78
+ socket.on('data', chunk => {
79
+ body += chunk.toString();
80
+ if (Buffer.byteLength(body) > 65_536) {
81
+ socket.destroy();
82
+ return;
83
+ }
84
+ if (handled || !body.includes('\n'))
85
+ return;
86
+ handled = true;
87
+ void (async () => {
88
+ try {
89
+ const request = JSON.parse(body.split('\n')[0]);
90
+ if (typeof request.token !== 'string' || request.token.length !== token.length || !timingSafeEqual(Buffer.from(token), Buffer.from(request.token)))
91
+ throw Error('Unauthorized');
92
+ if (request.generation !== generation)
93
+ throw new LocalControlError('BINDING_CONFLICT', 'Runtime generation changed');
94
+ let value;
95
+ if (request.action === 'inspect')
96
+ value = { ...await options.snapshot(), state: 'running', binding, generation, mode: options.mode };
97
+ else if (request.action === 'stop')
98
+ value = { accepted: true };
99
+ else if (request.action === 'update') {
100
+ const r = request.release;
101
+ if (typeof request.operationId !== 'string' || request.operationId.length > 128 || !r || typeof r.version !== 'string' || typeof r.url !== 'string' || typeof r.sha512 !== 'string' || !Number.isSafeInteger(r.size))
102
+ throw Error('Invalid update request');
103
+ value = await options.update(r, request.operationId);
104
+ }
105
+ else
106
+ throw Error('Invalid action');
107
+ socket.end(JSON.stringify({ ok: true, value }) + '\n');
108
+ if (request.action === 'stop')
109
+ setImmediate(() => { void options.shutdown().catch(() => { }); });
110
+ }
111
+ catch (error) {
112
+ socket.end(JSON.stringify({ ok: false, code: error instanceof LocalControlError ? error.code : 'SERVICE_FAILED' }) + '\n');
113
+ }
114
+ })();
115
+ });
116
+ });
117
+ const close = async () => {
118
+ if (closed)
119
+ return;
120
+ closed = true;
121
+ for (const socket of clients)
122
+ socket.destroy();
123
+ await new Promise(resolve => server.close(() => resolve()));
124
+ // Only remove our own generation.
125
+ try {
126
+ if (JSON.parse(await readControlFile(paths.ownerFile)).generation === generation) {
127
+ await fs.rm(paths.ownerFile, { force: true });
128
+ await fs.rm(paths.tokenFile, { force: true });
129
+ await fs.rm(paths.socket, { force: true });
130
+ }
131
+ }
132
+ catch { /* never remove another owner */ }
133
+ };
134
+ try {
135
+ await new Promise((resolve, reject) => { server.once('error', reject); server.listen(paths.socket, () => { server.removeListener('error', reject); resolve(); }); });
136
+ await fs.chmod(paths.socket, 0o600);
137
+ await fs.writeFile(paths.tokenFile, token, { mode: 0o600 });
138
+ await fs.chmod(paths.tokenFile, 0o600);
139
+ await fs.writeFile(paths.ownerFile, JSON.stringify({ schemaVersion: 1, pid: process.pid, command: await processCommand(process.pid), startTime: await processStartTime(process.pid), generation }), { mode: 0o600 });
140
+ await fs.chmod(paths.ownerFile, 0o600);
141
+ return { generation, close };
142
+ }
143
+ catch (error) {
144
+ await close();
145
+ throw error;
146
+ }
147
+ });
148
+ }
@@ -0,0 +1,35 @@
1
+ const ENDPOINTS = new Set(['/workstation/register', '/download/client/manifest.json', '/rest/auth/me', '/rest/auth/scopes/check']);
2
+ export function normalizeWorkstationServer(value) {
3
+ if (/[\u0000-\u0020\\]/.test(value))
4
+ throw new Error('Invalid workstation server URL');
5
+ // Inspect the original path: URL construction silently removes encoded dot segments.
6
+ const rawPath = /^[A-Za-z][A-Za-z0-9+.-]*:\/\/[^/?#]*(\/[^?#]*)?/.exec(value)?.[1] ?? '';
7
+ for (const part of rawPath.split('/')) {
8
+ const decoded = decodeURIComponent(part);
9
+ if (decoded === '.' || decoded === '..' || /[\/\\\u0000-\u001f\u007f]/.test(decoded))
10
+ throw new Error('Invalid workstation server path');
11
+ }
12
+ const url = new URL(value);
13
+ if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.search || url.hash) {
14
+ throw new Error('Workstation server must be an HTTP(S) URL without credentials, query or fragment');
15
+ }
16
+ return url.origin + url.pathname.replace(/\/+$/, '');
17
+ }
18
+ export function workstationEndpoint(server, apiPath) {
19
+ if (!ENDPOINTS.has(apiPath))
20
+ throw new Error('Unrecognized workstation endpoint');
21
+ return new URL(normalizeWorkstationServer(server) + apiPath);
22
+ }
23
+ /** Catalog URLs are deployment-relative even when a reverse proxy adds a path prefix. */
24
+ export function deploymentDownloadURL(server, value) {
25
+ const base = normalizeWorkstationServer(server);
26
+ const configured = new URL(base);
27
+ const candidate = value.startsWith('/download/client/') ? base + value : new URL(value, base + '/').toString();
28
+ const normalized = normalizeWorkstationServer(candidate);
29
+ const url = new URL(normalized);
30
+ if (url.origin !== configured.origin)
31
+ throw new Error('Artifact URL must use the configured server origin');
32
+ if (!url.pathname.startsWith(configured.pathname.replace(/\/$/, '') + '/download/client/'))
33
+ throw new Error('Artifact URL must stay in the configured deployment');
34
+ return normalized;
35
+ }
@@ -1,9 +1,13 @@
1
+ import { claimDaemonIdentity } from './control/daemonIdentity.js';
2
+ import { openRuntimeHost } from './control/runtimeHost.js';
3
+ import { stoppedRuntime } from './control/protocol.js';
4
+ import { normalizeWorkstationServer } from './control/serverIdentity.js';
1
5
  import { promises as fs } from 'node:fs';
2
6
  import os from 'node:os';
3
7
  import path from 'node:path';
4
8
  import { fileURLToPath } from 'node:url';
5
9
  import { AGENT_WIRE_MIN_VERSION } from './protocol/agent.js';
6
- import { clientDir, loadClientConfig, readKeys, updateServiceContext } from './config.js';
10
+ import { clientDir, loadClientConfig, updateServiceContext } from './config.js';
7
11
  import { AgentSessionManager } from './agentSessions.js';
8
12
  import { AcpAgents } from './acp/acpAgents.js';
9
13
  import { ConnectionSupervisor } from './connection/supervisor.js';
@@ -11,6 +15,7 @@ import { ManagementChannel } from './channels/management.js';
11
15
  import { ComputerChannel } from './channels/computer.js';
12
16
  import { AgentChannel, dispatchAgentFrame as dispatchAgentChannelFrame } from './channels/agent.js';
13
17
  import { TaskRegistry } from './update/taskRegistry.js';
18
+ import { RdUpkeep } from './rd/upkeep.js';
14
19
  import { createUpdateCoordinator } from './update/runUpdate.js';
15
20
  import { resolveCliPath } from './daemonctl.js';
16
21
  import { SystemMetricsSampler } from './systemMetrics.js';
@@ -29,6 +34,7 @@ import { keeperGenerations } from './acp/processor/transport/generations.js';
29
34
  import { ExecPolicy } from './exec/policy.js';
30
35
  import { LegacyToolsKeeper } from './keepers/legacy.js';
31
36
  import { TerminalRouter, terminalEnv, terminalShell } from './tools/terminalRouter.js';
37
+ import { ToolchainProbe } from './toolchain.js';
32
38
  /**
33
39
  * The path `runDaemon` hands `createUpdateCoordinator` as `cliPath`, used to re-provision the OS
34
40
  * service after an update (see runUpdate.ts's `provisionService`).
@@ -281,6 +287,30 @@ export function startExclusivePoll(poll, intervalMs) {
281
287
  return () => clearInterval(timer);
282
288
  }
283
289
  export async function runDaemon(options = {}) {
290
+ const directory = clientDir();
291
+ const shutdown = new AbortController();
292
+ const state = { snapshot: async () => ({ ...stoppedRuntime(), connection: 'connecting' }) };
293
+ const { host, identity } = await claimDaemonIdentity(directory, async (identity) => {
294
+ const config = identity.config;
295
+ const host = await openRuntimeHost({ directory, binding: { server: normalizeWorkstationServer(config.server), workstationId: config.workstationId, ...(config.ownerUserId ? { ownerUserId: config.ownerUserId } : {}) },
296
+ mode: options.supervisedByServiceManager ? 'launchd' : 'foreground', snapshot: () => state.snapshot(), shutdown: async () => { shutdown.abort(); },
297
+ update: async (release, operationId) => {
298
+ if (!state.update)
299
+ return { phase: 'BUSY', commandId: operationId };
300
+ void state.update.handle({ commandId: operationId, release, force: false }).catch(error => console.error('[update] local request failed', error));
301
+ return { phase: 'REQUESTED', commandId: operationId, version: release.version };
302
+ },
303
+ });
304
+ return { host, identity };
305
+ });
306
+ try {
307
+ await runOwnedDaemon(options, shutdown, state, identity);
308
+ }
309
+ finally {
310
+ await host.close();
311
+ }
312
+ }
313
+ async function runOwnedDaemon(options, shutdown, localState, identity) {
284
314
  // Last line of defence for the whole process. Node's default for an unhandled rejection is to
285
315
  // *exit*, and this daemon is the only thing keeping every session on this workstation alive —
286
316
  // dying because one async callback rejected is strictly worse than logging and carrying on.
@@ -290,8 +320,25 @@ export async function runDaemon(options = {}) {
290
320
  console.error(`[daemon] unhandled rejection (ignored, daemon stays up): ${detail}`);
291
321
  });
292
322
  const dir = clientDir();
293
- const cfg = await loadClientConfig(dir);
294
- const { clientKey, serverPub } = await readKeys(dir);
323
+ const cfg = identity.config;
324
+ let activateRdAfterReadiness;
325
+ if (process.platform === 'linux') {
326
+ const { synchronizeRd } = await import('./rd/update.js');
327
+ activateRdAfterReadiness = await synchronizeRd(dir, cfg.server);
328
+ }
329
+ const { NativeProvider } = await import('./desktopControl/nativeProvider.js');
330
+ const { DeviceSockets } = await import('./desktopControl/deviceSockets.js');
331
+ const { DesktopSessionSupervisor } = await import('./desktopControl/supervisor.js');
332
+ const { MacProvider } = await import('./desktopControl/macProvider.js');
333
+ const { LinuxDesktopProvider } = await import('./rd/provider.js');
334
+ const legacyDesktop = cfg.desktopProvider ? new NativeProvider({ ...cfg.desktopProvider, validateSession: async () => {
335
+ const { readRdState } = await import('./rd/state.js');
336
+ if (await readRdState(dir))
337
+ throw new Error('Remote desktop configuration changed');
338
+ } }) : undefined;
339
+ const desktopProvider = options.desktopProvider ?? (process.platform === 'linux' ? new LinuxDesktopProvider(dir, cfg.server, legacyDesktop) :
340
+ legacyDesktop ?? (process.platform === 'darwin' && process.arch === 'arm64' ? new MacProvider(dir, cfg) : undefined));
341
+ const { clientKey, serverPub } = identity;
295
342
  // Repair the first fixed release even when it was installed by an older ignore-scripts updater,
296
343
  // before a newly started keeper-terminal snapshots native dependency availability.
297
344
  await preflightOptionalPty(updateServiceContext(dir));
@@ -317,6 +364,7 @@ export async function runDaemon(options = {}) {
317
364
  // invoked*, not on what the host merely supports.
318
365
  serviceLifecycle: resolveServiceLifecycle(options),
319
366
  });
367
+ localState.update = updates;
320
368
  // One ACP client for every external agent; the roster it reports is the ACP agent registry, keyed
321
369
  // by registry agent id (ruling R2) — the same key `agent_open.bridge` now carries.
322
370
  // Live sessions run in keeper-acp (spec §5.3) — the adapters outlive the daemon there — or, when it cannot be reached, in the
@@ -326,7 +374,7 @@ export async function runDaemon(options = {}) {
326
374
  // Sessions keeper-acp still runs from a previous daemon (a restart, an upgrade, a crash) are adopted where they are — across
327
375
  // keeper-acp generations (spec §5.5, §6.3) — before the agent channel's reconnect offer is answered.
328
376
  const acpGenerations = keeperGenerations({ launcher: acpLauncher, current: acpTransport });
329
- const acp = new AcpAgents({ cacheDir: dir, transports: { keeper: acpTransport }, keeperGenerations: acpGenerations });
377
+ const acp = new AcpAgents({ cacheDir: dir, serverOrigin: cfg.server, transports: { keeper: acpTransport }, keeperGenerations: acpGenerations });
330
378
  const agents = new AgentSessionManager(acp, null, path.join(dir, 'agents'), tasks);
331
379
  void agents.adoptSessions();
332
380
  let bridges = await acp.roster();
@@ -448,14 +496,54 @@ export async function runDaemon(options = {}) {
448
496
  });
449
497
  const metrics = new SystemMetricsSampler(cfg.workspaces ?? []);
450
498
  await metrics.start();
499
+ const toolchain = new ToolchainProbe(() => terminalShell(), () => terminalEnv());
500
+ toolchain.start();
501
+ const desktopSupervisor = desktopProvider && !options.onDesktopCommand ? new DesktopSessionSupervisor({ provider: desktopProvider,
502
+ sockets: new DeviceSockets(cfg.server, process.env.AXOM_DESKTOP_ALLOW_PRIVATE_WS === '1'),
503
+ report: (generation, frame) => management.sendDesktopState(generation, frame),
504
+ refreshCapabilities: generation => management.refreshDesktopCapabilities(generation), }) : undefined;
505
+ let rdGeneration;
506
+ const rdUpkeep = process.platform === 'linux' && desktopSupervisor ? new RdUpkeep({
507
+ synchronize: async () => {
508
+ if (!rdGeneration || shutdown.signal.aborted)
509
+ return;
510
+ const { synchronizeRd } = await import('./rd/update.js');
511
+ return synchronizeRd(dir, cfg.server, { deferActivation: true });
512
+ },
513
+ whenIdle: operation => desktopSupervisor.whenIdle(async () => {
514
+ if (!tasks.canUpdate() || !rdGeneration || shutdown.signal.aborted)
515
+ return;
516
+ await operation();
517
+ }),
518
+ refresh: () => { if (rdGeneration)
519
+ management.refreshDesktopCapabilities(rdGeneration); },
520
+ log: message => console.error(`${new Date().toISOString()} ${message}`),
521
+ }) : undefined;
522
+ const stopRdUpkeep = rdUpkeep ? startExclusivePoll(() => rdUpkeep.tick(), 60_000) : () => { };
451
523
  const management = new ManagementChannel({
452
524
  server: cfg.server, workstationId: cfg.workstationId, workstation: cfg.workstationId, clientKey, serverPub,
453
525
  bridges: () => bridges,
454
526
  workspaces: cfg.workspaces,
455
527
  playgroundCwd,
456
528
  metrics: () => metrics.snapshot(),
529
+ toolchain: () => toolchain.current(),
457
530
  allowInsecureWs,
458
- onReady: (ready) => updates.managementReady(ready.release, ready.generation),
531
+ onReady: async (ready) => {
532
+ await updates.managementReady(ready.release, ready.generation);
533
+ await activateRdAfterReadiness?.();
534
+ if (ready.selectedWire >= 9)
535
+ desktopSupervisor?.managementReady(ready.generation);
536
+ rdGeneration = ready.selectedWire >= 9 ? ready.generation : undefined;
537
+ void rdUpkeep?.tick();
538
+ },
539
+ desktopProvider,
540
+ onDesktopCommand: options.onDesktopCommand ?? ((frame, generation) => desktopSupervisor?.dispatch(frame, generation) ?? Promise.resolve()),
541
+ onClosed: generation => {
542
+ if (rdGeneration === generation)
543
+ rdGeneration = undefined;
544
+ desktopSupervisor?.managementClosed(generation);
545
+ options.onDesktopManagementClosed?.(generation);
546
+ },
459
547
  onUpdate: (command) => updates.handle(command),
460
548
  onExternalAgentsScan: async () => {
461
549
  bridges = await scanRoster();
@@ -465,6 +553,7 @@ export async function runDaemon(options = {}) {
465
553
  keepers: () => buildKeeperStatuses(keeperPolicy, () => tasks.countedKeeperExecs),
466
554
  onKeeperControl: (frame) => keeperPolicy.control(frame),
467
555
  });
556
+ options.onDesktopManagement?.(management);
468
557
  const agent = new AgentChannel({ server: cfg.server, agents, bridges: () => bridges, allowInsecureWs });
469
558
  // Spec §6.2: first take over what an older CLI left (`_npx` copies, pre-build binary dirs) so agents
470
559
  // that worked keep working; then §6.1: a version this CLI certifies (runtime.json/catalog pin) or a
@@ -501,7 +590,14 @@ export async function runDaemon(options = {}) {
501
590
  });
502
591
  const stopIdleSweep = startIdleSweep(agents);
503
592
  const stopSwitchUpkeep = startSwitchUpkeep(agents, acp);
504
- const shutdown = new AbortController();
593
+ localState.snapshot = async () => {
594
+ const channels = supervisor.channelSnapshot();
595
+ const active = await buildManagementSnapshot(supervisor, tasks, updates);
596
+ const keepers = await buildKeeperStatuses(keeperPolicy, () => tasks.countedKeeperExecs);
597
+ const sum = (key) => keepers?.reduce((n, k) => n + (k.metrics[key] ?? 0), 0) ?? 0;
598
+ return { ...stoppedRuntime(), connection: channels.management === 'CONNECTED' ? 'online' : 'reconnecting',
599
+ activity: keepers === null ? null : { commands: Math.max(0, active.activeShellTasks + sum('execRunning') - sum('execCounted')), terminals: sum('ptyOpen'), agents: active.activeAgentTasks + sum('sessions'), transfers: 0 } };
600
+ };
505
601
  const stop = () => shutdown.abort();
506
602
  process.once('SIGINT', stop);
507
603
  process.once('SIGTERM', stop);
@@ -509,11 +605,15 @@ export async function runDaemon(options = {}) {
509
605
  await supervisor.run(shutdown.signal);
510
606
  }
511
607
  finally {
608
+ stopRdUpkeep();
609
+ rdUpkeep?.stop();
610
+ await desktopSupervisor?.close();
512
611
  process.removeListener('SIGINT', stop);
513
612
  process.removeListener('SIGTERM', stop);
514
613
  stopIdleSweep();
515
614
  stopSwitchUpkeep();
516
615
  metrics.stop();
616
+ toolchain.stop();
517
617
  stopRootsPoll();
518
618
  stopLegacyPoll();
519
619
  stopKeeperPolicy();