@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
@@ -97,23 +97,27 @@ export async function gcBundles(options) {
97
97
  const keep = new Set(options.keep ?? []);
98
98
  const removed = [];
99
99
  for (const kind of KEEPER_KINDS) {
100
- const root = path.join(path.resolve(options.configDir), 'keepers', kind);
101
- const referenced = new Set(keep);
102
- for (const gen of await fs.readdir(path.join(root, 'run')).catch(() => [])) {
103
- const manifest = await readPidManifest(path.join(root, 'run', gen, 'keeper.pid.json')).catch(() => null);
104
- if (manifest && await manifestProcessLive(manifest))
105
- referenced.add(manifest.buildVersion);
106
- }
107
- const bundles = path.join(root, 'bundles');
108
- for (const name of await fs.readdir(bundles).catch(() => [])) {
109
- if (referenced.has(name))
110
- continue;
111
- const dir = path.join(bundles, name);
112
- const stat = await fs.lstat(dir).catch(() => null);
113
- if (!stat || (minAge > 0 && now - Math.max(stat.mtimeMs, stat.ctimeMs) < minAge))
114
- continue;
115
- await fs.rm(dir, { recursive: true, force: true });
116
- removed.push(dir);
100
+ const kindRoot = path.join(path.resolve(options.configDir), 'keepers', kind);
101
+ const entries = await fs.readdir(kindRoot, { withFileTypes: true }).catch(() => []);
102
+ const roots = [kindRoot, ...entries.filter(entry => entry.isDirectory() && /^[0-9a-f]{32}$/.test(entry.name)).map(entry => path.join(kindRoot, entry.name))];
103
+ for (const root of roots) {
104
+ const referenced = new Set(keep);
105
+ for (const gen of await fs.readdir(path.join(root, 'run')).catch(() => [])) {
106
+ const manifest = await readPidManifest(path.join(root, 'run', gen, 'keeper.pid.json')).catch(() => null);
107
+ if (manifest && await manifestProcessLive(manifest))
108
+ referenced.add(manifest.buildVersion);
109
+ }
110
+ const bundles = path.join(root, 'bundles');
111
+ for (const name of await fs.readdir(bundles).catch(() => [])) {
112
+ if (referenced.has(name))
113
+ continue;
114
+ const dir = path.join(bundles, name);
115
+ const stat = await fs.lstat(dir).catch(() => null);
116
+ if (!stat || (minAge > 0 && now - Math.max(stat.mtimeMs, stat.ctimeMs) < minAge))
117
+ continue;
118
+ await fs.rm(dir, { recursive: true, force: true });
119
+ removed.push(dir);
120
+ }
117
121
  }
118
122
  }
119
123
  return removed;
@@ -24,7 +24,7 @@ export class KeeperRequestTimeoutError extends Error {
24
24
  }
25
25
  export class KeeperNotConnectedError extends Error {
26
26
  code = 'not_connected';
27
- constructor(message = 'Keeper is not connected') { super(message); this.name = 'KeeperNotConnectedError'; }
27
+ constructor(message = 'Keeper is not connected', options) { super(message, options); this.name = 'KeeperNotConnectedError'; }
28
28
  }
29
29
  export class KeeperClosedError extends Error {
30
30
  code = 'closed';
@@ -154,7 +154,7 @@ function connectLink(socketPath, limits, timeoutMs, onEvt, onClose) {
154
154
  const timer = setTimeout(() => fail(new KeeperNotConnectedError('Timed out connecting to the keeper')), timeoutMs);
155
155
  const fail = (error) => { if (settled)
156
156
  return; settled = true; clearTimeout(timer); socket.destroy(); reject(error); };
157
- socket.once('error', (error) => fail(new KeeperNotConnectedError(error.message)));
157
+ socket.once('error', (error) => fail(new KeeperNotConnectedError(error.message, { cause: error })));
158
158
  socket.once('connect', () => {
159
159
  if (settled)
160
160
  return;
@@ -1,6 +1,7 @@
1
1
  // Daemon-side: starts, probes and stops keepers. Exempt from the keeper-side import guard.
2
2
  import { spawn } from 'node:child_process';
3
3
  import { promises as fs } from 'node:fs';
4
+ import { constants, openSync, fstatSync, readFileSync, closeSync, lstatSync, mkdirSync, writeFileSync } from 'node:fs';
4
5
  import path from 'node:path';
5
6
  import { fileURLToPath } from 'node:url';
6
7
  import { keeperOneShot, readKeeperToken } from './client.js';
@@ -9,6 +10,7 @@ import { keeperGen } from './identity.js';
9
10
  import { encodeLaunchConfig, launchConfigOf } from './launchConfig.js';
10
11
  import { acquireStartupLock, liveManifest, manifestProcessLive, pidAlive, processCommand, readPidManifest, releaseStartupLock } from './lock.js';
11
12
  import { bundleDirFor, ensureKeeperDirectories, keeperPaths, touchSocket } from './paths.js';
13
+ import { assertOwnedDirectory, assertRegistrationOwner, legacyOwnershipAllowed, legacySourcesOf, readOwnerFile, RegistrationOwnershipError } from '../../runtime/ownership.js';
12
14
  const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
13
15
  /** `keeper.ping` on a fresh connection. Null unless the answer is from this exact keeper generation. */
14
16
  export async function pingKeeper(paths, timeoutMs = 250) {
@@ -46,20 +48,122 @@ export class KeeperLauncher {
46
48
  constructor(options) {
47
49
  this.options = options;
48
50
  }
51
+ get ownership() { return this.options.ownership; }
52
+ assertOwnership(kind, gen = 'ownership') { this.pathsFor(kind, gen); }
53
+ allowsLegacyOwnership(kind, gen) {
54
+ const sources = legacySourcesOf(this.ownership);
55
+ return !!sources && this.options.server === sources.keeperServer && path.resolve(this.options.configDir) === path.resolve(sources.adapterCacheDirectory) && legacyOwnershipAllowed(this.ownership, this.pathsFor(kind, gen).root, kind);
56
+ }
57
+ assertSessionOwnership(label, gen) {
58
+ assertRegistrationOwner(label, this.ownership, this.allowsLegacyOwnership('acp', gen) ? { directory: this.pathsFor('acp', gen).root, kind: 'acp' } : undefined);
59
+ }
60
+ assertResourceOwnership(kind, gen, resource, id, create = false) {
61
+ if (!this.ownership)
62
+ return;
63
+ const paths = this.pathsFor(kind, gen);
64
+ const directory = path.join(paths.dataDir, 'registration-owners', resource);
65
+ const file = path.join(directory, `${encodeURIComponent(id)}.json`);
66
+ try {
67
+ assertRegistrationOwner(readOwnerFile(file), this.ownership);
68
+ return;
69
+ }
70
+ catch (error) {
71
+ if (error.code !== 'ENOENT')
72
+ throw error;
73
+ }
74
+ if (!create && !this.allowsLegacyOwnership(kind, gen))
75
+ throw new RegistrationOwnershipError('Registration resource owner is unknown; state quarantined');
76
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
77
+ try {
78
+ writeFileSync(file, JSON.stringify({ schemaVersion: 1, registrationId: this.ownership.registrationId }), { mode: 0o600, flag: 'wx' });
79
+ }
80
+ catch (error) {
81
+ if (error.code !== 'EEXIST')
82
+ throw error;
83
+ }
84
+ assertRegistrationOwner(readOwnerFile(file), this.ownership);
85
+ }
86
+ async assertKeeperResources(kind, gen, timeoutMs = 1000) {
87
+ if (!this.ownership)
88
+ return;
89
+ const paths = this.pathsFor(kind, gen);
90
+ const token = await readKeeperToken(paths.tokenFile);
91
+ if (kind === 'acp') {
92
+ const listed = await keeperOneShot(paths.socket, 'acp.list', {}, timeoutMs, { kind, token });
93
+ if (!Array.isArray(listed.sessions))
94
+ throw new RegistrationOwnershipError('Registration session ownership needs verification');
95
+ for (const session of listed.sessions)
96
+ this.assertSessionOwnership(session.label, gen);
97
+ }
98
+ else {
99
+ const ptys = await keeperOneShot(paths.socket, 'pty.list', {}, timeoutMs, { kind, token });
100
+ const execs = await keeperOneShot(paths.socket, 'exec.list', {}, timeoutMs, { kind, token });
101
+ if (!Array.isArray(ptys.sessions) || !Array.isArray(execs.execs))
102
+ throw new RegistrationOwnershipError('Registration shell ownership needs verification');
103
+ for (const session of ptys.sessions)
104
+ this.assertResourceOwnership(kind, gen, 'pty', session.id);
105
+ for (const exec of execs.execs)
106
+ this.assertResourceOwnership(kind, gen, 'exec', exec.execId);
107
+ }
108
+ }
49
109
  get distDir() { return this.options.distDir ?? defaultDistDir(); }
50
110
  probe(paths, timeoutMs) { return this.options.probe ? this.options.probe(paths) : pingKeeper(paths, timeoutMs); }
51
111
  /** Paths of generation `gen` of `kind` (no build needed). */
52
- pathsFor(kind, gen) { return keeperPaths(this.options, kind, gen); }
112
+ pathsFor(kind, gen) {
113
+ const scoped = keeperPaths(this.options, kind, gen, { fallbackRoot: this.options.fallbackRoot });
114
+ assertOwnedDirectory(scoped.root, this.ownership, kind);
115
+ try {
116
+ lstatSync(scoped.pidFile);
117
+ return scoped;
118
+ }
119
+ catch (error) {
120
+ if (error.code !== 'ENOENT')
121
+ throw error;
122
+ }
123
+ const legacy = keeperPaths(this.options, kind, gen, { legacyLayout: true });
124
+ let descriptor;
125
+ try {
126
+ descriptor = openSync(legacy.pidFile, constants.O_RDONLY | constants.O_NOFOLLOW);
127
+ const stat = fstatSync(descriptor);
128
+ if (!stat.isFile() || stat.size > 4096 || process.getuid && stat.uid !== process.getuid())
129
+ throw new Error('Unsafe keeper PID manifest');
130
+ const manifest = JSON.parse(readFileSync(descriptor, 'utf8'));
131
+ if (manifest.identity === scoped.identity && manifest.kind === kind && manifest.gen === gen) {
132
+ assertOwnedDirectory(legacy.root, this.ownership, kind);
133
+ return legacy;
134
+ }
135
+ }
136
+ catch (error) {
137
+ if (error.code !== 'ENOENT') {
138
+ if (this.ownership && !(error instanceof RegistrationOwnershipError))
139
+ throw new RegistrationOwnershipError('Keeper registration ownership could not be verified; state quarantined', error);
140
+ throw error;
141
+ }
142
+ }
143
+ finally {
144
+ if (descriptor !== undefined)
145
+ closeSync(descriptor);
146
+ }
147
+ return scoped;
148
+ }
53
149
  /** The generation the current build of `kind` runs as (`dist/keepers/<kind>/manifest.json`); starts nothing. */
54
150
  async currentGen(kind) {
55
151
  return keeperGen((await readKeeperManifest(path.join(this.distDir, kind))).buildVersion);
56
152
  }
57
153
  /** Generations of `kind` whose pid manifest names a live keeper of that generation (`<root>/run/<gen>`); starts nothing. */
58
154
  async liveGenerations(kind) {
59
- const runRoot = path.join(this.pathsFor(kind, 'probe').root, 'run');
60
- const gens = await fs.readdir(runRoot).catch(() => []);
155
+ const scoped = keeperPaths(this.options, kind, 'probe');
156
+ const legacy = keeperPaths(this.options, kind, 'probe', { legacyLayout: true });
157
+ const roots = [scoped.root, legacy.root];
158
+ const gens = new Set();
159
+ for (const root of roots)
160
+ for (const gen of await fs.readdir(path.join(root, 'run')).catch(() => [])) {
161
+ const manifest = await readPidManifest(path.join(root, 'run', gen, 'keeper.pid.json')).catch(() => null);
162
+ if (manifest?.identity === scoped.identity && manifest.kind === kind && manifest.gen === gen)
163
+ gens.add(gen);
164
+ }
61
165
  const live = [];
62
- for (const gen of gens.sort())
166
+ for (const gen of [...gens].sort())
63
167
  if (await liveManifest(this.pathsFor(kind, gen)).catch(() => false))
64
168
  live.push(gen);
65
169
  return live;
@@ -76,7 +180,7 @@ export class KeeperLauncher {
76
180
  async start(kind) {
77
181
  const deadline = Date.now() + (this.options.startTimeoutMs ?? 7500);
78
182
  const manifest = await readKeeperManifest(path.join(this.distDir, kind));
79
- const paths = keeperPaths(this.options, kind, keeperGen(manifest.buildVersion));
183
+ const paths = this.pathsFor(kind, keeperGen(manifest.buildVersion));
80
184
  await ensureKeeperDirectories(paths);
81
185
  // A keeper that already answers needs no snapshot check: it runs from the snapshot it was started with.
82
186
  const alive = await this.probe(paths);
@@ -157,7 +261,10 @@ export class KeeperLauncher {
157
261
  // `detached` is what makes it survive; do not add launchd keys or drop it.
158
262
  const child = spawn(nodePath, [script, config], { detached: true, stdio: 'ignore' });
159
263
  child.on('error', () => { });
264
+ this.options.onNativeAcquired?.(child, paths);
160
265
  child.unref();
266
+ if (child.pid)
267
+ await this.options.onNativeSpawn?.(child.pid, paths, child);
161
268
  }
162
269
  /**
163
270
  * Spec §3.1: a socket that fell back to `/tmp` (its configDir path is too long for sockaddr_un) is touched for every generation
@@ -165,9 +272,10 @@ export class KeeperLauncher {
165
272
  * socket. Sockets under configDir are left alone. Returns how many were touched; never throws.
166
273
  */
167
274
  async touchSockets(kind) {
168
- const runRoot = path.join(this.pathsFor(kind, 'probe').root, 'run');
275
+ const root = keeperPaths(this.options, kind, 'probe').root;
276
+ const gens = new Set([...await fs.readdir(path.join(root, 'run')).catch(() => []), ...await this.liveGenerations(kind)]);
169
277
  let touched = 0;
170
- for (const gen of await fs.readdir(runRoot).catch(() => [])) {
278
+ for (const gen of gens) {
171
279
  const paths = this.pathsFor(kind, gen);
172
280
  if (paths.socketFallback && await touchSocket(paths).catch(() => false))
173
281
  touched += 1;
@@ -204,6 +312,14 @@ export class KeeperLauncher {
204
312
  }
205
313
  return true;
206
314
  };
315
+ if (this.ownership) {
316
+ try {
317
+ await this.assertKeeperResources(kind, gen);
318
+ }
319
+ catch {
320
+ return { stopped: false, pid, reason: 'Registration resource ownership needs verification' };
321
+ }
322
+ }
207
323
  // keeper.stop needs the connection token (the daemon owns the run directory it lives in). Without a usable token
208
324
  // the keeper cannot be asked, so after the same identity check the force path uses, SIGTERM it (its handler is the graceful stop).
209
325
  let reason;
@@ -16,7 +16,8 @@ export function keeperLockPort(identity, gen) {
16
16
  }
17
17
  export function keeperPaths(input, kind, gen, options = {}) {
18
18
  const identity = keeperIdentity(input, kind);
19
- const root = path.join(path.resolve(input.configDir), 'keepers', kind);
19
+ const kindRoot = path.join(path.resolve(input.configDir), 'keepers', kind);
20
+ const root = options.legacyLayout ? kindRoot : path.join(kindRoot, identity);
20
21
  const runDir = path.join(root, 'run', gen);
21
22
  let socket = path.join(runDir, 'keeper.sock');
22
23
  let socketDir = runDir;
@@ -9,8 +9,19 @@
9
9
  import { execFile, spawn } from 'node:child_process';
10
10
  import { promises as fs } from 'node:fs';
11
11
  import path from 'node:path';
12
- import { keeperPaths, keeperRpc } from '../tools/client.js';
12
+ import os from 'node:os';
13
+ import { createHash } from 'node:crypto';
14
+ import { keeperRpc } from '../tools/client.js';
13
15
  import { pidAlive } from './kit/lock.js';
16
+ import { legacySourcesOf } from '../runtime/ownership.js';
17
+ export function legacyKeeperPaths(options) {
18
+ const identity = createHash('sha256').update(JSON.stringify([path.resolve(options.configDir), options.workstationId, options.server])).digest('hex').slice(0, 32);
19
+ const namespace = `axom-tools-${process.getuid?.() ?? 'user'}`;
20
+ let dir = path.join(process.platform === 'darwin' ? '/tmp' : os.tmpdir(), namespace, identity);
21
+ if (Buffer.byteLength(path.join(dir, 'keeper.sock')) > 100)
22
+ dir = path.join('/tmp', namespace, identity);
23
+ return { dir, socket: path.join(dir, 'keeper.sock'), lock: path.join(dir, 'startup.lock'), pidFile: path.join(dir, 'keeper.pid.json'), lockPort: 20000 + Number.parseInt(identity.slice(0, 8), 16) % 40000, unit: `axom-tools-keeper-${identity}` };
24
+ }
14
25
  /** A legacy keeper with no open PTY is stopped once nothing was routed to it for this long (the user may still read an exited one). */
15
26
  export const LEGACY_IDLE_MS = 10 * 60_000;
16
27
  /**
@@ -57,9 +68,12 @@ export class LegacyToolsKeeper {
57
68
  rpc;
58
69
  constructor(options) {
59
70
  this.options = options;
60
- this.paths = options.paths ?? keeperPaths({ ...options.tools }, undefined, undefined, 'terminal');
71
+ this.paths = options.paths ?? legacyKeeperPaths(options.tools);
61
72
  this.now = options.now ?? Date.now;
62
73
  this.rpc = options.rpc ?? keeperRpc;
74
+ const sources = legacySourcesOf(options.ownership);
75
+ if (options.ownership && (!sources || path.resolve(options.tools.configDir) !== path.resolve(sources.adapterCacheDirectory) || options.tools.server !== sources.keeperServer))
76
+ this.current = 'absent';
63
77
  }
64
78
  get state() { return this.current; }
65
79
  /** Present or busy: it may hold sessions, so the router asks it. */
@@ -19,8 +19,6 @@
19
19
  // older one gets a persistent intent to move there when idle (`moveOffOlderKeeperGenerations`, Task 24); an older
20
20
  // generation that hosts no adapter any more is stopped (`retireIdleGenerations`). Never for `available` unless asked.
21
21
  // replace-now stops the older generations at once (their in-flight turns end; the sessions reopen through session/load).
22
- // legacy the legacy tools keeper (spec §9) retires by itself (keepers/legacy.ts); it is reported `required`/`when-idle`, and
23
- // replace-now stops it at once.
24
22
  import { promises as fs } from 'node:fs';
25
23
  import path from 'node:path';
26
24
  import { keeperOneShot, readKeeperToken } from './kit/client.js';
@@ -268,24 +266,8 @@ export class KeeperPolicy {
268
266
  this.log(`[keepers] keeper-terminal could not be described: ${error instanceof Error ? error.message : String(error)}`);
269
267
  }
270
268
  }
271
- const legacy = this.options.legacy;
272
- if (legacy?.live)
273
- out.push({ live: true, current: false, status: this.legacyStatus(legacy) });
274
269
  return out;
275
270
  }
276
- legacyStatus(legacy) {
277
- const { state, open } = legacy.status();
278
- const shell = nameLabel(this.options.legacyShell?.(), 'shell');
279
- const sessions = legacy.lastSessions();
280
- return {
281
- kind: 'terminal', gen: 'legacy', legacy: true, state: state === 'present' || state === 'stopping' ? 'running' : 'unavailable', // busy = not answering
282
- protocolVersion: 0, buildVersion: 'legacy', replacement: 'required', pending: 'when-idle',
283
- metrics: { ptyOpen: open ?? sessions.filter((session) => !session.exited).length, ptyActive: 0, execRunning: 0 },
284
- items: sessions.slice(0, KEEPER_STATUS_MAX_ITEMS).map((session) => ({
285
- type: 'pty', id: clip(session.id, 256), label: shell, state: session.exited ? 'exited' : 'idle', sinceEpochMs: 0,
286
- })),
287
- };
288
- }
289
271
  async collectAcp() {
290
272
  const acp = this.options.acp;
291
273
  if (!acp)
@@ -351,7 +333,7 @@ export class KeeperPolicy {
351
333
  /** required/mandatory, never decided before (a cancel or a restart never brings an automatic decision back): queue it. */
352
334
  async automatic(sampled) {
353
335
  for (const { status, live, current } of sampled) {
354
- if (status.legacy || current || !live || !replacesByItself(status.replacement))
336
+ if (current || !live || !replacesByItself(status.replacement))
355
337
  continue;
356
338
  if (this.decisions.has(decisionKey(status.kind, status.gen)))
357
339
  continue;
@@ -375,7 +357,7 @@ export class KeeperPolicy {
375
357
  async runQueued(sampled) {
376
358
  const acpPending = [];
377
359
  for (const { status, live, current } of sampled) {
378
- if (status.legacy || current || this.pendingOf(status.kind, status.gen) !== 'when-idle')
360
+ if (current || this.pendingOf(status.kind, status.gen) !== 'when-idle')
379
361
  continue;
380
362
  if (status.kind === 'acp') {
381
363
  if (live)
@@ -465,8 +447,7 @@ export class KeeperPolicy {
465
447
  }
466
448
  async apply(frame) {
467
449
  const sampled = await this.sampled(true);
468
- const older = sampled.filter((entry) => entry.status.kind === frame.kind && !entry.current && entry.live && !entry.status.legacy);
469
- const legacy = frame.kind === 'terminal' ? sampled.find((entry) => entry.status.legacy) : undefined;
450
+ const older = sampled.filter((entry) => entry.status.kind === frame.kind && !entry.current && entry.live);
470
451
  const affected = [];
471
452
  let ok = true;
472
453
  let incomplete = false;
@@ -502,15 +483,6 @@ export class KeeperPolicy {
502
483
  error = `generation ${status.gen} could not be stopped`;
503
484
  }
504
485
  }
505
- if (legacy && this.options.legacy) {
506
- const shell = nameLabel(this.options.legacyShell?.(), 'shell');
507
- for (const session of await this.options.legacy.replaceNow())
508
- affected.push({ type: 'pty', id: session.id, label: shell, gen: 'legacy' });
509
- if (this.options.legacy.live) {
510
- ok = false;
511
- error = 'the legacy tools keeper could not be stopped';
512
- }
513
- }
514
486
  break;
515
487
  }
516
488
  case 'cancel': {
@@ -525,9 +497,8 @@ export class KeeperPolicy {
525
497
  }
526
498
  }
527
499
  this.sample = undefined;
528
- // From the decisions themselves (a sample in flight may predate them); the legacy keeper is always queued while it lives.
529
- const queued = [...this.decisions].some(([key, decision]) => key.startsWith(`${frame.kind}:`) && decision.mode === 'when-idle')
530
- || (frame.kind === 'terminal' && this.options.legacy?.live === true);
500
+ // Read decisions directly: an in-flight sample may predate them.
501
+ const queued = [...this.decisions].some(([key, decision]) => key.startsWith(`${frame.kind}:`) && decision.mode === 'when-idle');
531
502
  if (affected.length > KEEPER_AFFECTED_MAX_ITEMS)
532
503
  incomplete = true;
533
504
  return {
@@ -3,11 +3,14 @@
3
3
  import { promises as fs } from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import { readKeeperManifest } from '../kit/bundle.js';
6
- import { KeeperClient } from '../kit/client.js';
6
+ import { KeeperClient, KeeperNotConnectedError, KeeperRequestError } from '../kit/client.js';
7
+ import { liveManifest } from '../kit/lock.js';
7
8
  import { keeperGen } from '../kit/identity.js';
8
9
  import { defaultDistDir } from '../kit/launcher.js';
9
10
  import { protocolWindow } from '../kit/protocol.js';
11
+ import { RegistrationOwnershipError } from '../../runtime/ownership.js';
10
12
  import { PTY_CAPABILITY, PTY_UNAVAILABLE_ERROR, TERMINAL_PROTOCOL_VERSION, execStream, } from './protocol.js';
13
+ import { TERMINAL_GRID_LIMITS } from './gridProtocol.js';
11
14
  /** keeper-terminal could not be reached (launch, connect or hello failed): the request was never sent. */
12
15
  export class KeeperLaunchError extends Error {
13
16
  cause;
@@ -27,7 +30,12 @@ export function terminalStatusOf(hello) {
27
30
  const NOT_RUNNING_TTL_MS = 5000;
28
31
  /** The default bound of {@link TerminalKeeperClient.probe}: a real connect and hello, not a quick peek. */
29
32
  export const KEEPER_PROBE_TIMEOUT_MS = 3000;
30
- const errnoOf = (error) => typeof error === 'object' && error !== null && typeof error.code === 'string' ? error.code : undefined;
33
+ const errnoOf = (error) => {
34
+ // The kit keeps its public not_connected code; classify only the underlying socket errno.
35
+ const source = error instanceof KeeperNotConnectedError ? error.cause : error;
36
+ return typeof source === 'object' && source !== null && typeof source.code === 'string'
37
+ ? source.code : undefined;
38
+ };
31
39
  /** Connect errors that prove nothing listens: the socket file is gone, or a stale one with no listener behind it. */
32
40
  const ABSENT_ERRNOS = new Set(['ENOENT', 'ECONNREFUSED']);
33
41
  const messageOf = (error) => (error instanceof Error ? error.message : String(error));
@@ -48,7 +56,7 @@ export class TerminalKeeperClient {
48
56
  }
49
57
  get daemonProtocol() { return this.options.daemonProtocol ?? protocolWindow(TERMINAL_PROTOCOL_VERSION); }
50
58
  clientFor(paths) {
51
- if (this.client && this.clientGen === paths.gen)
59
+ if (this.client && this.client.state !== 'closed' && this.clientGen === paths.gen)
52
60
  return this.client;
53
61
  this.client?.close();
54
62
  this.client = new KeeperClient({
@@ -70,6 +78,7 @@ export class TerminalKeeperClient {
70
78
  * most one keeper per kind at a time. A `gen`-bound client only connects to its generation.
71
79
  */
72
80
  async ensure() {
81
+ this.options.launcher.assertOwnership?.('terminal', this.clientGen ?? this.options.gen);
73
82
  const current = this.client;
74
83
  if (this.options.gen !== undefined) {
75
84
  if (current && current.state === 'ready')
@@ -123,24 +132,41 @@ export class TerminalKeeperClient {
123
132
  return probing;
124
133
  }
125
134
  async probeOnce(timeoutMs) {
135
+ try {
136
+ this.options.launcher.assertOwnership?.('terminal', this.clientGen ?? this.options.gen);
137
+ }
138
+ catch {
139
+ return { state: 'unknown', reason: 'Registration owner needs verification' };
140
+ }
126
141
  const absent = () => { this.notRunningUntil = Date.now() + NOT_RUNNING_TTL_MS; return { state: 'absent' }; };
127
142
  let gen;
128
143
  try {
129
- gen = this.options.gen
144
+ gen = this.options.gen ?? this.currentGen
130
145
  ?? keeperGen((await readKeeperManifest(path.join(this.options.distDir ?? defaultDistDir(), 'terminal'))).buildVersion);
131
146
  }
132
- catch {
133
- return absent();
134
- } // no build of the current generation: nothing of it can run
147
+ catch (error) {
148
+ return errnoOf(error) === 'ENOENT' ? absent() : { state: 'unknown', reason: messageOf(error) };
149
+ } // A missing build is absent; unreadable or invalid metadata is inconclusive.
135
150
  const paths = this.options.launcher.pathsFor('terminal', gen);
136
151
  if (this.options.gen === undefined)
137
152
  this.currentGen = paths.gen;
153
+ // A live owner may be initializing, busy, or have lost its socket. None proves its sessions are gone.
154
+ const unreachable = async () => {
155
+ try {
156
+ if (await liveManifest(paths))
157
+ return { state: 'unknown', reason: 'live keeper owner is unreachable' };
158
+ return absent();
159
+ }
160
+ catch (error) {
161
+ return { state: 'unknown', reason: messageOf(error) };
162
+ }
163
+ };
138
164
  try {
139
165
  await fs.access(paths.tokenFile);
140
166
  await fs.access(paths.socket);
141
167
  }
142
168
  catch (error) {
143
- return ABSENT_ERRNOS.has(errnoOf(error) ?? '') ? absent() : { state: 'unknown', reason: messageOf(error) };
169
+ return ABSENT_ERRNOS.has(errnoOf(error) ?? '') ? unreachable() : { state: 'unknown', reason: messageOf(error) };
144
170
  }
145
171
  const client = this.clientFor(paths);
146
172
  let timer;
@@ -159,41 +185,105 @@ export class TerminalKeeperClient {
159
185
  return { state: 'unknown', reason: `protocol ${result.hello.protocolVersion} is outside the daemon's window` };
160
186
  }
161
187
  catch (error) {
162
- // The token or the socket went away meanwhile, or nothing listens behind a stale socket file: the keeper is gone.
163
- return ABSENT_ERRNOS.has(errnoOf(error) ?? '') ? absent() : { state: 'unknown', reason: messageOf(error) };
188
+ // Missing endpoints prove absence only when there is no verified live owner.
189
+ return ABSENT_ERRNOS.has(errnoOf(error) ?? '') ? unreachable() : { state: 'unknown', reason: messageOf(error) };
164
190
  }
165
191
  finally {
166
192
  clearTimeout(timer);
167
193
  }
168
194
  }
169
- /** `tools.info` terminal fields from a running keeper's hello, else from `fallback` (a cached node-pty check). Starts nothing. */
195
+ /** `tools.info` terminal fields from a running keeper's hello, else from the caller's unavailable status. Starts nothing. */
170
196
  async terminalStatus(fallback) {
171
197
  const hello = await this.runningHello();
172
198
  return hello ? terminalStatusOf(hello) : fallback();
173
199
  }
174
200
  /** A failure to reach the keeper (launch, connect, hello) surfaces as {@link KeeperLaunchError}: nothing was sent. */
175
201
  async call(method, params, options = {}) {
202
+ this.options.launcher.assertOwnership?.('terminal', this.clientGen ?? this.options.gen);
176
203
  let client;
177
204
  try {
178
205
  client = await this.ensure();
179
206
  }
180
207
  catch (error) {
208
+ if (error instanceof RegistrationOwnershipError)
209
+ throw error;
181
210
  throw new KeeperLaunchError(error);
182
211
  }
183
- return client.request(method, params, options);
212
+ const input = params;
213
+ const resource = method.startsWith('pty.') ? 'pty' : 'exec';
214
+ const id = resource === 'pty' ? input.id : input.execId;
215
+ if (this.options.launcher.ownership && typeof id === 'string' && method !== 'exec.status') {
216
+ let create = false;
217
+ if (method === 'pty.create')
218
+ create = !(await client.request('pty.list', {})).sessions.some(session => session.id === id);
219
+ if (method === 'exec.start')
220
+ create = !(await client.request('exec.list', {})).execs.some(exec => exec.execId === id);
221
+ this.options.launcher.assertResourceOwnership?.('terminal', this.clientGen, resource, id, create);
222
+ }
223
+ const result = await client.request(method, input, options);
224
+ if (this.options.launcher.ownership && typeof id === 'string' && method === 'exec.status') {
225
+ this.options.launcher.assertResourceOwnership?.('terminal', this.clientGen, resource, id);
226
+ }
227
+ if (this.options.launcher.ownership && (method === 'pty.list' || method === 'exec.list')) {
228
+ const registrationId = this.options.launcher.ownership.registrationId;
229
+ if (method === 'pty.list')
230
+ result.sessions = result.sessions.filter(session => !session.registrationId || session.registrationId === registrationId);
231
+ else
232
+ result.execs = result.execs.filter(exec => !exec.registrationId || exec.registrationId === registrationId);
233
+ const ids = method === 'pty.list' ? result.sessions.map(session => session.id) : result.execs.map(exec => exec.execId);
234
+ for (const current of ids)
235
+ this.options.launcher.assertResourceOwnership?.('terminal', this.clientGen, resource, current);
236
+ }
237
+ return result;
238
+ }
239
+ /** Connect only to existing terminal work. Never launch or retry a possibly delivered event. */
240
+ async callNative(method, params, absentResult) {
241
+ this.options.launcher.assertOwnership?.('terminal', this.clientGen ?? this.options.gen);
242
+ // A cached absence may predate another client's create; cleanup needs a fresh absence check.
243
+ const presence = this.hello ? { state: 'running' } : await this.probeOnce(KEEPER_PROBE_TIMEOUT_MS);
244
+ if (presence.state === 'absent') {
245
+ if (absentResult !== undefined)
246
+ return absentResult;
247
+ throw new KeeperRequestError('pty_not_found', 'Terminal session not found; keeper restart loses live PTYs');
248
+ }
249
+ if (presence.state === 'unknown')
250
+ throw new KeeperRequestError('pty_unavailable', `Terminal keeper unavailable: ${presence.reason}`);
251
+ const client = this.client;
252
+ if (!client)
253
+ throw new KeeperRequestError('pty_unavailable', 'Terminal keeper disconnected');
254
+ const input = params;
255
+ if (typeof input.id === 'string')
256
+ this.options.launcher.assertResourceOwnership?.('terminal', this.clientGen, 'pty', input.id);
257
+ return client.request(method, input);
258
+ }
259
+ create(params) { return this.call('pty.create', { ...params, ...(this.options.launcher.ownership ? { registrationId: this.options.launcher.ownership.registrationId } : {}) }); }
260
+ attach(params) { return this.callNative('pty.attach', params); }
261
+ poll(params) { return this.callNative('pty.poll', params); }
262
+ input(params) { return this.callNative('pty.input', params); }
263
+ resize(params) { return this.callNative('pty.resize', params); }
264
+ scroll(params) { return this.callNative('pty.scroll', params); }
265
+ detach(params) { return this.callNative('pty.detach', params, { ok: true }); }
266
+ close(params) { return this.callNative('pty.close', params, { ok: true }); }
267
+ /** Listing is a probe even if the ready socket disappears: reconnect, never launch. */
268
+ async list() {
269
+ const presence = await this.probe();
270
+ if (presence.state === 'absent')
271
+ return { schema: 1, limits: { ...TERMINAL_GRID_LIMITS }, sessions: [] };
272
+ if (presence.state === 'unknown')
273
+ throw new KeeperLaunchError(new Error(presence.reason));
274
+ const result = await this.client.request('pty.list', {});
275
+ const registrationId = this.options.launcher.ownership?.registrationId;
276
+ if (registrationId)
277
+ result.sessions = result.sessions.filter(session => !session.registrationId || session.registrationId === registrationId);
278
+ for (const session of result.sessions)
279
+ this.options.launcher.assertResourceOwnership?.('terminal', this.clientGen, 'pty', session.id);
280
+ return result;
184
281
  }
185
- create(params) { return this.call('pty.create', params); }
186
- attach(params) { return this.call('pty.attach', params); }
187
- poll(params) { return this.call('pty.poll', params); }
188
- write(params) { return this.call('pty.write', params); }
189
- resize(params) { return this.call('pty.resize', params); }
190
- takeover(params) { return this.call('pty.takeover', params); }
191
- detach(params) { return this.call('pty.detach', params); }
192
- close(params) { return this.call('pty.close', params); }
193
- list() { return this.call('pty.list', {}); }
194
282
  activity() { return this.call('keeper.activity', {}); }
195
283
  // exec.* (spec §4.2). The daemon decides argv/env/deadline/quotas; the keeper only executes them.
196
- execStart(params, options = {}) { return this.call('exec.start', params, options); }
284
+ execStart(params, options = {}) {
285
+ return this.call('exec.start', { ...params, ...(this.options.launcher.ownership ? { registrationId: this.options.launcher.ownership.registrationId } : {}) }, options);
286
+ }
197
287
  execList() { return this.call('exec.list', {}); }
198
288
  /** Descriptor plus the latest checkpoint (blob as given to `execCheckpoint`). */
199
289
  execStatus(execId, options = {}) { return this.call('exec.status', { execId }, options); }
@@ -206,7 +296,9 @@ export class TerminalKeeperClient {
206
296
  * resumes after the last record delivered.
207
297
  */
208
298
  async execSubscribe(execId, handler, options) {
209
- return (await this.ensure()).subscribe(execStream(execId), handler, options);
299
+ const client = await this.ensure();
300
+ this.options.launcher.assertResourceOwnership?.('terminal', this.clientGen, 'exec', execId);
301
+ return client.subscribe(execStream(execId), handler, options);
210
302
  }
211
303
  /** Drops the connection (the keeper and its PTYs keep running). */
212
304
  disconnect() { this.client?.close(); this.client = undefined; this.clientGen = undefined; }