@platless/axom-cli 0.5.9 → 0.6.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (258) hide show
  1. package/README.md +25 -6
  2. package/dist/axom-patch/cli/README.md +129 -12
  3. package/dist/axom-patch/cli/account-limits/generate.py +15 -12
  4. package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +20 -3
  5. package/dist/axom-patch/cli/claude-account-limits.json +9 -9
  6. package/dist/axom-patch/cli/claude-replay-lifecycle.json +16 -0
  7. package/dist/axom-patch/cli/codex-account-limits.json +11 -11
  8. package/dist/axom-patch/cli/codex-terminal-output.json +16 -0
  9. package/dist/axom-patch/cli/commandcode-account-limits.json +9 -9
  10. package/dist/axom-patch/cli/kimi-quota.mjs +12 -0
  11. package/dist/axom-patch/cli/kimi-subagents.mjs +173 -0
  12. package/dist/axom-patch/cli/nightly-task.md +55 -13
  13. package/dist/axom-patch/cli/probes/claude-replay-lifecycle.mjs +31 -0
  14. package/dist/axom-patch/cli/probes/codex-terminal-output.mjs +33 -0
  15. package/dist/axom-patch/cli/probes/kimi-subagent-subwork.mjs +82 -0
  16. package/dist/axom-patch/cli/probes/zcode-env-account.mjs +81 -0
  17. package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +7 -0
  18. package/dist/axom-patch/cli/qoder/dist/axom-acp-compat.mjs +1687 -0
  19. package/dist/axom-patch/cli/qoder/releases/1.1.75.json +277 -0
  20. package/dist/axom-patch/cli/qoder-background-terminal.md +13 -6
  21. package/dist/axom-patch/cli/qoder-foreground-output.md +14 -8
  22. package/dist/axom-patch/cli/qoder-native-acp.json +227 -0
  23. package/dist/axom-patch/cli/qoder-steering.md +20 -25
  24. package/dist/axom-patch/cli/qoder-subagent-subwork.md +17 -68
  25. package/dist/axom-patch/cli/runtime.json +19 -19
  26. package/dist/axom-patch/cli/zcode/env-account.mjs +13 -0
  27. package/dist/axom-patch/cli/zcode/generate-env-account.py +52 -0
  28. package/dist/axom-patch/cli/zcode-account-limits.json +10 -11
  29. package/dist/axom-patch/cli/zcode-env-account.json +58 -0
  30. package/dist/axom-patch/cli/zcode-models.json +9 -10
  31. package/dist/axom-patch/cli/zcode-subagent-subwork.json +10 -11
  32. package/dist/cli.js +11 -45
  33. package/dist/commands/auth.js +4 -1
  34. package/dist/commands/knowledge.js +47 -0
  35. package/dist/commands/update.js +126 -3
  36. package/dist/commands/workstation.js +101 -164
  37. package/dist/commands/workstationRegistrations.js +116 -0
  38. package/dist/credentialBroker.js +13 -2
  39. package/dist/credentials.js +16 -0
  40. package/dist/install/claude/commands.js +15 -15
  41. package/dist/keepers/terminal/keeper.mjs +1089 -673
  42. package/dist/keepers/terminal/manifest.json +4 -4
  43. package/dist/mcp.js +1 -1
  44. package/dist/native/darwin-x64/axom-quic +0 -0
  45. package/dist/workstation/acp/acpAgents.js +113 -102
  46. package/dist/workstation/acp/acpClient.js +26 -0
  47. package/dist/workstation/acp/builds/install/npm.js +99 -10
  48. package/dist/workstation/acp/initializeResponse.js +6 -1
  49. package/dist/workstation/acp/managedPatches.js +189 -25
  50. package/dist/workstation/acp/managedRuntime.js +314 -0
  51. package/dist/workstation/acp/outbox.js +12 -5
  52. package/dist/workstation/acp/patchAssets.js +40 -0
  53. package/dist/workstation/acp/permission.js +2 -2
  54. package/dist/workstation/acp/processor/checkpoint.js +26 -34
  55. package/dist/workstation/acp/processor/control.js +7 -80
  56. package/dist/workstation/acp/processor/jsonrpc.js +8 -0
  57. package/dist/workstation/acp/processor/runtime.js +5 -81
  58. package/dist/workstation/acp/processor/session.js +54 -82
  59. package/dist/workstation/acp/processor/state.js +7 -13
  60. package/dist/workstation/acp/processor/step.js +3 -20
  61. package/dist/workstation/acp/processor/test/fixtureLog.js +9 -16
  62. package/dist/workstation/acp/processor/transport/local.js +19 -3
  63. package/dist/workstation/acp/processor/updates.js +156 -105
  64. package/dist/workstation/acp/processorSession.js +20 -173
  65. package/dist/workstation/acp/registry/catalog.js +65 -39
  66. package/dist/workstation/acp/registry/launch.js +16 -1
  67. package/dist/workstation/acp/switching.js +0 -3
  68. package/dist/workstation/acp/test/fakeAgent.js +13 -0
  69. package/dist/workstation/acp/test/keeperWorld.js +32 -3
  70. package/dist/workstation/acp/test/raw/sources.js +16 -130
  71. package/dist/workstation/acp/test/realAdapter.js +1 -47
  72. package/dist/workstation/acp/test/scriptAgent.js +11 -3
  73. package/dist/workstation/agentFlow.js +1 -1
  74. package/dist/workstation/agentSessions.js +114 -166
  75. package/dist/workstation/channels/agent.js +9 -1
  76. package/dist/workstation/channels/computer.js +27 -4
  77. package/dist/workstation/channels/management.js +130 -15
  78. package/dist/workstation/channels/url.js +1 -1
  79. package/dist/workstation/config.js +33 -9
  80. package/dist/workstation/connection/encryptedChannel.js +4 -1
  81. package/dist/workstation/control/bindingStore.js +204 -0
  82. package/dist/workstation/control/commands.js +125 -0
  83. package/dist/workstation/control/daemonIdentity.js +143 -0
  84. package/dist/workstation/control/deviceCommands.js +191 -0
  85. package/dist/workstation/control/deviceProtocol.js +68 -0
  86. package/dist/workstation/control/enrollment.js +106 -0
  87. package/dist/workstation/control/inspect.js +71 -0
  88. package/dist/workstation/control/locks.js +110 -0
  89. package/dist/workstation/control/protocol.js +40 -0
  90. package/dist/workstation/control/restartGuard.js +46 -0
  91. package/dist/workstation/control/rootFormat.js +16 -0
  92. package/dist/workstation/control/runtimeClient.js +152 -0
  93. package/dist/workstation/control/runtimeHost.js +235 -0
  94. package/dist/workstation/control/serverIdentity.js +35 -0
  95. package/dist/workstation/control/updateTransition.js +91 -0
  96. package/dist/workstation/daemon.js +266 -264
  97. package/dist/workstation/daemonctl.js +226 -25
  98. package/dist/workstation/desktopControl/audio.js +99 -0
  99. package/dist/workstation/desktopControl/automationDevice.js +219 -0
  100. package/dist/workstation/desktopControl/automationJson.js +74 -0
  101. package/dist/workstation/desktopControl/automationLocalState.js +23 -0
  102. package/dist/workstation/desktopControl/automationProtocol.js +253 -0
  103. package/dist/workstation/desktopControl/automationSchema.generated.js +1 -0
  104. package/dist/workstation/desktopControl/automationSession.js +81 -0
  105. package/dist/workstation/desktopControl/automationTransport.js +138 -0
  106. package/dist/workstation/desktopControl/capabilities.js +93 -0
  107. package/dist/workstation/desktopControl/captureModes.js +9 -0
  108. package/dist/workstation/desktopControl/captureTargetLabel.js +9 -0
  109. package/dist/workstation/desktopControl/deviceAdmission.js +229 -0
  110. package/dist/workstation/desktopControl/deviceOwnership.js +130 -0
  111. package/dist/workstation/desktopControl/deviceSockets.js +246 -0
  112. package/dist/workstation/desktopControl/helperProcess.js +602 -0
  113. package/dist/workstation/desktopControl/hostGrant.js +65 -0
  114. package/dist/workstation/desktopControl/lobby.js +131 -0
  115. package/dist/workstation/desktopControl/macProvider.js +156 -0
  116. package/dist/workstation/desktopControl/media.js +331 -0
  117. package/dist/workstation/desktopControl/nativeDeviceAdmission.js +207 -0
  118. package/dist/workstation/desktopControl/nativeProvider.js +1044 -0
  119. package/dist/workstation/desktopControl/networkEvidence.js +43 -0
  120. package/dist/workstation/desktopControl/overview.js +58 -0
  121. package/dist/workstation/desktopControl/ownershipProtocol.js +31 -0
  122. package/dist/workstation/desktopControl/protocol.js +311 -0
  123. package/dist/workstation/desktopControl/provider.js +1 -0
  124. package/dist/workstation/desktopControl/quicMedia.js +240 -0
  125. package/dist/workstation/desktopControl/rateController.js +226 -0
  126. package/dist/workstation/desktopControl/supervisor.js +1774 -0
  127. package/dist/workstation/desktopControl/transport.js +27 -0
  128. package/dist/workstation/desktopControl/wallpaper.js +98 -0
  129. package/dist/workstation/exec/policy.js +5 -0
  130. package/dist/workstation/files/client.js +10 -1
  131. package/dist/workstation/files/credentials.js +142 -0
  132. package/dist/workstation/files/host.js +9 -2
  133. package/dist/workstation/files/text.js +42 -8
  134. package/dist/workstation/journal.js +5 -2
  135. package/dist/workstation/keepers/acp/client.js +38 -2
  136. package/dist/workstation/keepers/kit/buildVersion.js +35 -11
  137. package/dist/workstation/keepers/kit/bundle.js +21 -17
  138. package/dist/workstation/keepers/kit/client.js +2 -2
  139. package/dist/workstation/keepers/kit/launcher.js +123 -7
  140. package/dist/workstation/keepers/kit/paths.js +2 -1
  141. package/dist/workstation/keepers/legacy.js +16 -2
  142. package/dist/workstation/keepers/policy.js +5 -34
  143. package/dist/workstation/keepers/terminal/client.js +115 -23
  144. package/dist/workstation/keepers/terminal/exec.js +6 -3
  145. package/dist/workstation/keepers/terminal/frames.js +73 -0
  146. package/dist/workstation/keepers/terminal/generations.js +5 -8
  147. package/dist/workstation/keepers/terminal/gridProtocol.js +16 -0
  148. package/dist/workstation/keepers/terminal/input.js +146 -0
  149. package/dist/workstation/keepers/terminal/keeper.js +22 -8
  150. package/dist/workstation/keepers/terminal/main.js +1 -1
  151. package/dist/workstation/keepers/terminal/metadata.js +138 -0
  152. package/dist/workstation/keepers/terminal/protocol.js +4 -28
  153. package/dist/workstation/keepers/terminal/pty.js +1 -330
  154. package/dist/workstation/keepers/terminal/screen.js +246 -0
  155. package/dist/workstation/keepers/terminal/sessions.js +526 -0
  156. package/dist/workstation/managedInstall.js +57 -0
  157. package/dist/workstation/oauth/deviceAuthorization.js +31 -17
  158. package/dist/workstation/protocol/agent.js +2 -15
  159. package/dist/workstation/protocol/management.js +113 -5
  160. package/dist/workstation/protocol/runtimeDiagnostics.js +15 -0
  161. package/dist/workstation/rd/artifact.js +120 -0
  162. package/dist/workstation/rd/control.js +117 -0
  163. package/dist/workstation/rd/display.js +29 -0
  164. package/dist/workstation/rd/host.js +213 -0
  165. package/dist/workstation/rd/migrationReadiness.js +46 -0
  166. package/dist/workstation/rd/provider.js +256 -0
  167. package/dist/workstation/rd/sessions.js +391 -0
  168. package/dist/workstation/rd/state.js +57 -0
  169. package/dist/workstation/rd/update.js +168 -0
  170. package/dist/workstation/rd/upkeep.js +80 -0
  171. package/dist/workstation/register.js +27 -37
  172. package/dist/workstation/registrations/enrollment.js +250 -0
  173. package/dist/workstation/registrations/enrollmentPending.js +361 -0
  174. package/dist/workstation/registrations/migration.js +356 -0
  175. package/dist/workstation/registrations/model.js +34 -0
  176. package/dist/workstation/registrations/removal.js +222 -0
  177. package/dist/workstation/registrations/store.js +737 -0
  178. package/dist/workstation/runtime/desktopBinding.js +20 -0
  179. package/dist/workstation/runtime/deviceDesktop.js +193 -0
  180. package/dist/workstation/runtime/deviceRuntime.js +568 -0
  181. package/dist/workstation/runtime/deviceServices.js +117 -0
  182. package/dist/workstation/runtime/events.js +20 -0
  183. package/dist/workstation/runtime/ownership.js +100 -0
  184. package/dist/workstation/runtime/registrationRuntime.js +575 -0
  185. package/dist/workstation/runtime/remoteDesktopAdapter.js +186 -0
  186. package/dist/workstation/runtime/removalActivity.js +184 -0
  187. package/dist/workstation/service/installer.js +197 -14
  188. package/dist/workstation/service/launchd.js +2 -0
  189. package/dist/workstation/service/systemd.js +1 -0
  190. package/dist/workstation/storageMetrics.js +118 -0
  191. package/dist/workstation/systemMetrics.js +47 -92
  192. package/dist/workstation/toolchain.js +94 -0
  193. package/dist/workstation/tools/browser.js +28 -4
  194. package/dist/workstation/tools/client.js +8 -27
  195. package/dist/workstation/tools/info.js +6 -13
  196. package/dist/workstation/tools/terminalRouter.js +17 -172
  197. package/dist/workstation/tools/validation.js +55 -1
  198. package/dist/workstation/update/archiveEvidence.js +206 -0
  199. package/dist/workstation/update/artifact.js +2 -1
  200. package/dist/workstation/update/coordinator.js +34 -13
  201. package/dist/workstation/update/deviceCompatibility.js +119 -0
  202. package/dist/workstation/update/deviceCoordinator.js +369 -0
  203. package/dist/workstation/update/deviceHealth.js +104 -0
  204. package/dist/workstation/update/deviceHealthProtocol.js +26 -0
  205. package/dist/workstation/update/deviceRecoveryLifecycle.js +46 -0
  206. package/dist/workstation/update/deviceRollback.js +78 -0
  207. package/dist/workstation/update/globalInstall.js +186 -0
  208. package/dist/workstation/update/legacyBridge.js +414 -0
  209. package/dist/workstation/update/legacyOwner.js +39 -0
  210. package/dist/workstation/update/npmInstaller.js +51 -25
  211. package/dist/workstation/update/privateDurability.js +61 -0
  212. package/dist/workstation/update/rdRollback.js +145 -0
  213. package/dist/workstation/update/recoveryPlan.js +124 -0
  214. package/dist/workstation/update/runUpdate.js +10 -3
  215. package/dist/workstation/update/taskRegistry.js +161 -3
  216. package/dist/workstation/update/watchdog.js +29806 -333
  217. package/dist/workstation/update/watchdogProcess.js +72 -0
  218. package/package.json +11 -7
  219. package/skills/axom/SKILL.md +11 -11
  220. package/skills/axom-definition/SKILL.md +3 -3
  221. package/skills/axom-platform/SKILL.md +3 -3
  222. package/dist/axom-patch/cli/probes/qoder-steering.mjs +0 -89
  223. package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +0 -61
  224. package/dist/axom-patch/cli/qoder-steering.json +0 -52
  225. package/dist/axom-patch/cli/qoder-subagent-origin.json +0 -73
  226. package/dist/commands/guide.js +0 -38
  227. package/dist/runtime-deps/qoder-sdk/package-lock.json +0 -1189
  228. package/dist/runtime-deps/qoder-sdk/package.json +0 -8
  229. package/dist/workstation/acp/delegationMcp.js +0 -24
  230. package/dist/workstation/acp/processor/sideChannel.js +0 -80
  231. package/dist/workstation/acp/qoderStartup.js +0 -45
  232. package/dist/workstation/acp/test/raw/qoderCaptures.js +0 -125
  233. package/dist/workstation/qoderAdapter/backgroundProcess.js +0 -118
  234. package/dist/workstation/qoderAdapter/backgroundTerminal.js +0 -193
  235. package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +0 -372
  236. package/dist/workstation/qoderAdapter/callbackContext.js +0 -76
  237. package/dist/workstation/qoderAdapter/completedContinuation.js +0 -139
  238. package/dist/workstation/qoderAdapter/contract.js +0 -78
  239. package/dist/workstation/qoderAdapter/foregroundOutput.js +0 -36
  240. package/dist/workstation/qoderAdapter/history.js +0 -85
  241. package/dist/workstation/qoderAdapter/identityIndex.js +0 -191
  242. package/dist/workstation/qoderAdapter/inputQueue.js +0 -42
  243. package/dist/workstation/qoderAdapter/main.js +0 -95
  244. package/dist/workstation/qoderAdapter/mapper.js +0 -117
  245. package/dist/workstation/qoderAdapter/observerPlugin.js +0 -245
  246. package/dist/workstation/qoderAdapter/observerReceiver.js +0 -187
  247. package/dist/workstation/qoderAdapter/permissions.js +0 -61
  248. package/dist/workstation/qoderAdapter/profile.js +0 -93
  249. package/dist/workstation/qoderAdapter/sdkDriver.js +0 -48
  250. package/dist/workstation/qoderAdapter/sdkRuntime.js +0 -93
  251. package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +0 -145
  252. package/dist/workstation/qoderAdapter/session.js +0 -319
  253. package/dist/workstation/qoderAdapter/subagentMeta.js +0 -28
  254. package/dist/workstation/qoderAdapter/subwork.js +0 -85
  255. package/dist/workstation/qoderAdapter/taskControl.js +0 -38
  256. package/dist/workstation/qoderAdapter/taskLedger.js +0 -163
  257. package/dist/workstation/qoderAdapter/usage.js +0 -37
  258. /package/dist/workstation/{qoderAdapter/types.js → desktopControl/automationPreparation.js} +0 -0
@@ -0,0 +1,27 @@
1
+ import { z } from 'zod';
2
+ import { DesktopProtocolError, parseDesktopJson } from './protocol.js';
3
+ const sessionId = z.string().regex(/^[0-9a-f]{32}$/);
4
+ const generation = z.number().int().min(1).max(Number.MAX_SAFE_INTEGER);
5
+ const secret = z.string().regex(/^[A-Za-z0-9_-]{43}$/).refine(s => Buffer.from(s, 'base64url').toString('base64url') === s);
6
+ const role = z.enum(['viewer_control', 'viewer_media', 'device_control', 'device_media']);
7
+ const quic = z.strictObject({ port: z.number().int().min(1).max(65535), certificate: z.string().min(64).max(2048).refine(s => Buffer.from(s, 'base64').toString('base64') === s) });
8
+ const schema = z.discriminatedUnion('type', [
9
+ z.strictObject({ type: z.literal('device_attach'), sessionId, grant: secret }),
10
+ z.strictObject({ type: z.literal('viewer_attach'), sessionId, nonce: secret }),
11
+ z.strictObject({ type: z.literal('attached'), sessionId, role, controlGeneration: generation, overviewToken: secret.optional(), audioToken: secret.optional() }),
12
+ z.strictObject({ type: z.literal('media_grant'), sessionId, controlGeneration: generation, grant: secret, quic: quic.optional() }),
13
+ z.strictObject({ type: z.literal('media_nonce'), sessionId, controlGeneration: generation, nonce: secret, quic: quic.optional() }),
14
+ ]);
15
+ export function decodeDesktopTransport(source) {
16
+ try {
17
+ return schema.parse(parseDesktopJson(source, 4096));
18
+ }
19
+ catch {
20
+ throw new DesktopProtocolError('Invalid desktop transport exchange');
21
+ }
22
+ }
23
+ export function encodeDesktopTransport(message) {
24
+ const text = JSON.stringify(message);
25
+ decodeDesktopTransport(text);
26
+ return text;
27
+ }
@@ -0,0 +1,98 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { wallpaperAssetSchema } from './lobby.js';
3
+ export const WALLPAPER_MAX_BYTES = 2 * 1024 * 1024;
4
+ export const WALLPAPER_MAX_JSON = 3 * 1024 * 1024;
5
+ export function decodeWallpaper(text) {
6
+ if (Buffer.byteLength(text) > WALLPAPER_MAX_JSON)
7
+ throw new Error('wallpaper_unavailable');
8
+ const result = JSON.parse(text);
9
+ const asset = wallpaperAssetSchema.parse(result.asset);
10
+ if (typeof result.image !== 'string' || result.image.length > Math.ceil(WALLPAPER_MAX_BYTES / 3) * 4)
11
+ throw new Error('wallpaper_unavailable');
12
+ const bytes = Buffer.from(result.image, 'base64');
13
+ if (bytes.length < 20 || bytes.length > WALLPAPER_MAX_BYTES || bytes.toString('base64') !== result.image ||
14
+ bytes.toString('ascii', 0, 4) !== 'RIFF' || bytes.toString('ascii', 8, 12) !== 'WEBP' || bytes.readUInt32LE(4) + 8 !== bytes.length || asset.mimeType !== 'image/webp')
15
+ throw new Error('wallpaper_unavailable');
16
+ let dimensions;
17
+ for (let offset = 12; offset + 8 <= bytes.length;) {
18
+ const kind = bytes.toString('ascii', offset, offset + 4), size = bytes.readUInt32LE(offset + 4), data = offset + 8;
19
+ if (size > bytes.length - data || kind === 'ANIM' || kind === 'ANMF')
20
+ throw new Error('wallpaper_unavailable');
21
+ if (kind === 'VP8X' && (size !== 10 || (bytes[data] & 2)))
22
+ throw new Error('wallpaper_unavailable');
23
+ if (kind === 'VP8 ' && size >= 10 && !(bytes[data] & 1) && bytes[data + 3] === 0x9d && bytes[data + 4] === 1 && bytes[data + 5] === 0x2a)
24
+ dimensions = [bytes.readUInt16LE(data + 6) & 0x3fff, bytes.readUInt16LE(data + 8) & 0x3fff];
25
+ if (kind === 'VP8L' && size >= 5 && bytes[data] === 0x2f) {
26
+ const bits = bytes.readUInt32LE(data + 1);
27
+ dimensions = [(bits & 0x3fff) + 1, ((bits >>> 14) & 0x3fff) + 1];
28
+ }
29
+ offset = data + size + (size & 1);
30
+ }
31
+ if (!dimensions || dimensions[0] !== asset.width || dimensions[1] !== asset.height)
32
+ throw new Error('wallpaper_unavailable');
33
+ const version = createHash('sha256').update(JSON.stringify(asset)).update(bytes).digest('hex');
34
+ return { asset: { ...asset, version }, bytes };
35
+ }
36
+ export class XfceWallpaperReader {
37
+ execute;
38
+ constructor(execute) {
39
+ this.execute = execute;
40
+ }
41
+ async discover() {
42
+ const workspaceValue = await this.execute('xprop', ['-root', '_NET_CURRENT_DESKTOP']);
43
+ let workspace = /_NET_CURRENT_DESKTOP\(CARDINAL\)\s*=\s*(\d+)/.exec(workspaceValue)?.[1];
44
+ if (workspace == null || Number(workspace) > 1024)
45
+ throw new Error('wallpaper_unavailable');
46
+ const listed = await this.execute('xfconf-query', ['-c', 'xfce4-desktop', '-l']);
47
+ const read = (property) => this.execute('xfconf-query', ['-c', 'xfce4-desktop', '-p', property]);
48
+ if (listed.split(/\r?\n/).some(line => line.trim() === '/backdrop/single-workspace-mode') &&
49
+ (await read('/backdrop/single-workspace-mode')).trim() === 'true') {
50
+ workspace = (await read('/backdrop/single-workspace-number')).trim();
51
+ if (!/^\d{1,4}$/.test(workspace) || Number(workspace) > 1024)
52
+ throw new Error('wallpaper_unavailable');
53
+ }
54
+ const available = listed.split(/\r?\n/).map(line => line.trim());
55
+ const properties = available
56
+ .filter(line => new RegExp(`^/backdrop/screen0/monitor[^/]+/workspace${workspace}/last-image$`).test(line));
57
+ let selected = properties[0];
58
+ let defaultImage = false;
59
+ if (properties.length === 0) {
60
+ const version = await this.execute('xfdesktop', ['--version']);
61
+ const monitors = await this.execute('xrandr', ['--listactivemonitors']);
62
+ if (!/xfdesktop version 4\.20(?:\.\d+)?\b/.test(version) || !/^Monitors:\s*1\s*$/m.test(monitors))
63
+ throw new Error('wallpaper_unavailable');
64
+ const output = /^\s*0:\s+\S+\s+\S+\s+(\S+)\s*$/m.exec(monitors)?.[1];
65
+ if (!output || !/^[A-Za-z0-9_.:-]+$/.test(output))
66
+ throw new Error('wallpaper_unavailable');
67
+ selected = `/backdrop/screen0/monitor${output}/workspace${workspace}/last-image`;
68
+ defaultImage = true;
69
+ }
70
+ if (properties.length > 1)
71
+ throw new Error('wallpaper_unavailable');
72
+ const file = defaultImage ? '/usr/share/backgrounds/xfce/xfce-x.svg' : (await read(selected)).trim();
73
+ if (!file.startsWith('/') || file.length > 4096 || /[\r\n\0]/.test(file))
74
+ throw new Error('wallpaper_unavailable');
75
+ const prefix = selected.slice(0, -'last-image'.length);
76
+ const style = available.includes(`${prefix}image-style`) ? (await read(`${prefix}image-style`)).trim() : '5';
77
+ const placement = { '1': 'center', '2': 'tile', '3': 'stretch', '4': 'fit', '5': 'cover' }[style];
78
+ if (!placement)
79
+ throw new Error('wallpaper_unavailable');
80
+ let background = '#000000';
81
+ let fidelity = 'exact';
82
+ if (placement !== 'cover' && placement !== 'stretch') {
83
+ try {
84
+ if ((await read(`${prefix}color-style`)).trim() !== '0')
85
+ throw new Error('unsupported_gradient');
86
+ const values = (await read(`${prefix}rgba1`)).split(/\r?\n/).map(line => line.trim())
87
+ .filter(line => /^\d+(?:\.\d+)?$/.test(line)).map(Number);
88
+ if (values.length !== 4 || values.some(value => value < 0 || value > 1) || values[3] !== 1)
89
+ throw new Error('unknown_fill');
90
+ background = '#' + values.slice(0, 3).map(value => Math.round(value * 255).toString(16).padStart(2, '0')).join('');
91
+ }
92
+ catch {
93
+ fidelity = 'approximate';
94
+ }
95
+ }
96
+ return { file, placement, background, fidelity };
97
+ }
98
+ }
@@ -12,6 +12,7 @@
12
12
  import { createHash, randomUUID } from 'node:crypto';
13
13
  import { KeeperFrameTooLargeError, KeeperNotConnectedError, KeeperReplacementRequiredError, KeeperRequestError } from '../keepers/kit/client.js';
14
14
  import { KeeperLaunchError } from '../keepers/terminal/client.js';
15
+ import { RegistrationOwnershipError } from '../runtime/ownership.js';
15
16
  import { EXEC_CAPABILITY, EXEC_SIGNALS, EXEC_TERMINAL_TTL_MS, ExecErrorCode } from '../keepers/terminal/protocol.js';
16
17
  import { execArgv, execCwd, execDeadline, execEnvironment } from './command.js';
17
18
  import { DurableExec, EXEC_KILL_GRACE_MS } from './durable.js';
@@ -289,6 +290,8 @@ export class ExecPolicy {
289
290
  hello = (await keeper.ensure()).hello;
290
291
  }
291
292
  catch (error) {
293
+ if (error instanceof RegistrationOwnershipError)
294
+ throw error;
292
295
  // A repeat may be running in a keeper-terminal this daemon cannot reach right now: running it here too could run it twice.
293
296
  if (how.repeat) {
294
297
  this.log(`exec ${req.id}: repeated request while keeper-terminal is unreachable: ${messageOf(error)}`);
@@ -323,6 +326,8 @@ export class ExecPolicy {
323
326
  return { kind: 'durable', exec: DurableExec.follow(keeper, descriptor, { outlivesChannel, ...(wire ? { wire } : {}) }, this.durableOptions()) };
324
327
  }
325
328
  catch (error) {
329
+ if (error instanceof RegistrationOwnershipError)
330
+ throw error;
326
331
  // Another keeper-terminal generation owns an exec of this id (spec §3.1: two generations may coexist): it exists, so it is
327
332
  // never started again here and never run in this process.
328
333
  if (errorCode(error) === ExecErrorCode.foreign) {
@@ -78,6 +78,14 @@ export class FilesHostClient {
78
78
  return 0;
79
79
  return this.lastStatus.uploads + this.lastStatus.encodes;
80
80
  }
81
+ /** Removal must distinguish an idle host from one that has not supplied a current activity sample. */
82
+ activitySnapshot() {
83
+ return {
84
+ known: this.stopped ? !this.child && !this.spawning : !!this.child && this.hostUp && !this.retiring
85
+ && !!this.lastStatus && Date.now() - this.lastStatusAt <= this.timings.hungPinMs,
86
+ transfers: this.activity() + this.sent.size + this.waiting,
87
+ };
88
+ }
81
89
  /** Process id of the current host (diagnostics and tests). */
82
90
  get pid() { return this.child?.pid; }
83
91
  get isReady() { return this.ready; }
@@ -392,7 +400,8 @@ export class FilesHostClient {
392
400
  // Past the grace, stop feeding it: new requests wait for the restarted host while the ones in flight finish.
393
401
  if (overdue)
394
402
  this.ready = false;
395
- if (this.sent.size === 0 && (overdue || (this.lastStatus?.uploads ?? 0) === 0)) {
403
+ if (this.sent.size === 0 && (overdue || !!this.lastStatus
404
+ && this.lastStatus.uploads === 0 && this.lastStatus.encodes === 0 && this.lastStatus.inFlight === 0)) {
396
405
  this.restartRequestedAt = null;
397
406
  this.log('[files-host] workspaces changed; restarting');
398
407
  void this.shutdownChild(true);
@@ -0,0 +1,142 @@
1
+ import { realpathSync } from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { FileError } from './errors.js';
5
+ /*
6
+ * Credential storage the platform agent file tools (`file_*_request`, the request-root scope) never read, list or
7
+ * change. This is a security boundary, not a policy an approval can lift: an agent that lacks a capability must not go
8
+ * looking for another client's identity, a token or a key to work around it.
9
+ *
10
+ * The check runs on canonical paths (the request root after realpath, components below it already refused when they
11
+ * are links), and again inside the `pinned()` section on what the kernel resolves: the pinned directory's realpath and
12
+ * each existing leaf's realpath, which return the names stored on disk. A relative path, a symlinked root, a directory
13
+ * swapped for a link after validation, or a leaf spelled so that a case-insensitive file system resolves it to a store
14
+ * (`.git-credentialſ`, `.ßh`) cannot bypass it. Names are compared folded: NFC, then full case folding (`ſ`→`s`,
15
+ * `ß`→`ss`, `K`→`k`), so a missing leaf created under such a spelling is refused too.
16
+ *
17
+ * The stores are named below the process's home, below any other home-like directory (`/Users/<name>`, `/home/<name>`,
18
+ * `/root`, `/var/root`: a daemon whose HOME is not the workspace owner's), and below the configured client
19
+ * directories. The lists equal `contracts/credential-stores/credential-stores.json` (a test checks it), which the
20
+ * server's early refusal (axom-core `ProtectedCredentialPaths`) matches too.
21
+ *
22
+ * Only the user-facing Files surfaces (configured roots, read-only previews, the owner's file manager) are not fenced
23
+ * here: they act for the user at the keyboard, not for an agent.
24
+ */
25
+ export const PROTECTED_CREDENTIAL_MESSAGE = 'This path is protected credential storage. Agents may not read, list or change it by any means. Tell the user what is needed instead of looking for another way.';
26
+ /** Below a home directory; each entry covers itself and everything below it; a trailing `*` matches a name prefix. */
27
+ export const HOME_ENTRIES = [
28
+ // Axomwork's own identities: the desktop/app clients, the CLI's auth stores and config.
29
+ '.axom/clients', '.axom/auth', '.axom/config.json', '.axom/device-id',
30
+ // SSH, GPG, password managers and desktop keyrings.
31
+ '.ssh', '.gnupg', '.password-store', '.local/share/keyrings', '.config/op',
32
+ // Agent CLIs' credentials and session data (their instructions and settings stay readable).
33
+ '.claude.json', '.claude/.credentials.json', '.claude/projects', '.claude/sessions', '.claude/shell-snapshots',
34
+ '.claude/history.jsonl', '.claude/backups', '.claude/session-env', '.claude/file-history', '.claude/ide',
35
+ '.codex/auth.json', '.codex/sessions', '.codex/history.jsonl',
36
+ '.acpx',
37
+ '.gemini/oauth_creds.json', '.gemini/jetski-standalone-oauth-token', '.gemini/antigravity*',
38
+ '.commandcode/auth.json', '.commandcode/projects', '.commandcode/history.jsonl', '.commandcode/file-history',
39
+ '.qoder/.auth', '.kimi', '.zcode',
40
+ // Developer, database and cloud credentials.
41
+ '.config/gh/hosts.yml', '.git-credentials', '.netrc', '.npmrc', '.pypirc', '.docker/config.json',
42
+ '.pgpass', '.my.cnf', '.vault-token', '.cargo/credentials', '.cargo/credentials.toml', '.gem/credentials',
43
+ '.m2/settings-security.xml', '.terraform.d/credentials.tfrc.json', '.gradle/gradle.properties',
44
+ '.aws', '.azure', '.kube', '.config/gcloud',
45
+ // macOS keychains and the Axomwork credential broker's state.
46
+ 'Library/Keychains', 'Library/Caches/com.platless.axom-credentials',
47
+ ];
48
+ /**
49
+ * The workstation client directory holds the binding to the server (keys, identity, provider tokens, keeper sockets,
50
+ * the agent browser's profiles). Its logs, caches and update state are operational and stay readable.
51
+ */
52
+ export const WORKSTATION_ENTRIES = [
53
+ 'client.key', 'server.pub', 'config.yml', 'binding-history.json', 'operation.json', 'desktop-provider.json',
54
+ 'desktop-preferences.json', 'control', 'keepers', 'agents', 'files-host', 'browser-profiles',
55
+ ];
56
+ export const ABSOLUTE_ENTRIES = ['/Library/Keychains'];
57
+ /** Home-like directories besides the process's own; `*` is one path component. */
58
+ export const HOME_ROOTS = ['/Users/*', '/home/*', '/root', '/var/root', '/private/var/root'];
59
+ /** `/tmp/com.platless.axom-credentials.<uid>.sock` (credentialBroker.ts) and anything else the broker names so. */
60
+ export const BROKER_SOCKET_PREFIX = 'com.platless.axom-credentials.';
61
+ export const TEMP_DIRECTORIES = ['/tmp', '/private/tmp'];
62
+ /** NFC, then full case folding the way a case-insensitive file system compares (`ſ`→`s`, `ß`→`ss`, `K`→`k`). */
63
+ export function foldName(value) { return value.normalize('NFC').toUpperCase().toLowerCase(); }
64
+ function segments(absolute) { return foldName(path.resolve(absolute)).split(path.sep).filter(Boolean); }
65
+ function canonical(target) {
66
+ try {
67
+ return realpathSync(target);
68
+ }
69
+ catch {
70
+ return path.resolve(target);
71
+ }
72
+ }
73
+ const patterns = (entries) => entries.map(entry => foldName(entry).split('/').filter(Boolean));
74
+ const HOME_PATTERNS = patterns(HOME_ENTRIES);
75
+ const WORKSTATION_PATTERNS = patterns(WORKSTATION_ENTRIES);
76
+ const ABSOLUTE_PATTERNS = patterns(ABSOLUTE_ENTRIES);
77
+ const HOME_ROOT_PATTERNS = patterns(HOME_ROOTS);
78
+ const WORKSTATION_BELOW_HOME = ['.axom', 'workstation'];
79
+ /** [target] starts with [pattern]; `*` alone is any one component, a trailing `*` a name prefix. */
80
+ function startsWith(target, pattern) {
81
+ if (target.length < pattern.length)
82
+ return false;
83
+ return pattern.every((part, index) => part === '*' || (part.endsWith('*') ? target[index].startsWith(part.slice(0, -1)) : target[index] === part));
84
+ }
85
+ function coveredBelow(target, base, entries) {
86
+ if (!startsWith(target, base))
87
+ return false;
88
+ const rest = target.slice(base.length);
89
+ return entries.some(entry => startsWith(rest, entry));
90
+ }
91
+ let cached;
92
+ /** Both spellings of [value]: as given (a relative one resolved as the client resolves it) and canonical. */
93
+ function spellings(value) {
94
+ if (!value)
95
+ return [];
96
+ const unique = new Map();
97
+ for (const spelling of [path.resolve(value), canonical(value)])
98
+ unique.set(spelling, segments(spelling));
99
+ return [...unique.values()];
100
+ }
101
+ function bases(home) {
102
+ const key = JSON.stringify([home, process.env.AXOM_WORKSTATION_CONFIG_DIR ?? null, process.env.AXOM_CONFIG_DIR ?? null]);
103
+ if (cached?.key !== key) {
104
+ cached = {
105
+ key,
106
+ bases: {
107
+ homes: spellings(home),
108
+ workstations: spellings(process.env.AXOM_WORKSTATION_CONFIG_DIR),
109
+ // AXOM_CONFIG_DIR stands in for `~/.axom`: its config.json and device-id are the CLI's.
110
+ cliConfigs: spellings(process.env.AXOM_CONFIG_DIR),
111
+ },
112
+ };
113
+ }
114
+ return cached.bases;
115
+ }
116
+ const CLI_CONFIG_PATTERNS = patterns(['config.json', 'device-id']);
117
+ const TEMP_PATTERNS = patterns(TEMP_DIRECTORIES);
118
+ const BROKER_PREFIX = foldName(BROKER_SOCKET_PREFIX);
119
+ /** True when [target] (absolute) is, or lies below, protected credential storage. */
120
+ export function isCredentialPath(target, home = os.homedir()) {
121
+ const folded = segments(target);
122
+ if (ABSOLUTE_PATTERNS.some(entry => startsWith(folded, entry)))
123
+ return true;
124
+ if (TEMP_PATTERNS.some(dir => folded.length > dir.length && startsWith(folded, dir) && folded[dir.length].startsWith(BROKER_PREFIX)))
125
+ return true;
126
+ const { homes, workstations, cliConfigs } = bases(home);
127
+ // Any home-like directory the target is below, besides the process's own (a daemon running as another user).
128
+ const homeLike = HOME_ROOT_PATTERNS.filter(root => startsWith(folded, root)).map(root => folded.slice(0, root.length));
129
+ for (const base of [...homes, ...homeLike]) {
130
+ if (coveredBelow(folded, base, HOME_PATTERNS))
131
+ return true;
132
+ if (coveredBelow(folded, [...base, ...WORKSTATION_BELOW_HOME], WORKSTATION_PATTERNS))
133
+ return true;
134
+ }
135
+ if (workstations.some(base => coveredBelow(folded, base, WORKSTATION_PATTERNS)))
136
+ return true;
137
+ return cliConfigs.some(base => coveredBelow(folded, base, CLI_CONFIG_PATTERNS));
138
+ }
139
+ export function assertNotCredentialPath(target) {
140
+ if (isCredentialPath(target))
141
+ throw new FileError('permission_denied', PROTECTED_CREDENTIAL_MESSAGE);
142
+ }
@@ -19,6 +19,7 @@ let tools;
19
19
  let initializing = false;
20
20
  let closing;
21
21
  const inFlight = new Map();
22
+ const operations = new Set();
22
23
  function send(message) {
23
24
  if (!process.connected)
24
25
  return;
@@ -64,7 +65,7 @@ function run(request) {
64
65
  inFlight.set(request.id, controller);
65
66
  const deadline = request.deadlineAt === null ? undefined : setTimeout(() => controller.abort(), Math.max(0, request.deadlineAt - Date.now()));
66
67
  const current = tools;
67
- void (async () => {
68
+ const operation = (async () => {
68
69
  let reply;
69
70
  try {
70
71
  // runFileFrame never throws: errors (and cancellation) come back as the result frame's `error`.
@@ -89,6 +90,8 @@ function run(request) {
89
90
  status();
90
91
  send(reply);
91
92
  })();
93
+ operations.add(operation);
94
+ void operation.finally(() => operations.delete(operation));
92
95
  }
93
96
  /** Refuses new work, lets running calls finish within a budget (a hung IO cannot hold the exit forever), then exits. */
94
97
  function shutdown(code) {
@@ -96,7 +99,11 @@ function shutdown(code) {
96
99
  for (const controller of inFlight.values())
97
100
  controller.abort();
98
101
  const budget = new Promise(resolve => { setTimeout(resolve, DISPOSE_BUDGET_MS).unref(); });
99
- await Promise.race([tools?.dispose().catch(() => { }), budget]);
102
+ const dispose = async () => {
103
+ await Promise.allSettled([...operations]);
104
+ await tools?.dispose();
105
+ };
106
+ await Promise.race([dispose().catch(() => { }), budget]);
100
107
  process.exit(code);
101
108
  })();
102
109
  return closing;
@@ -1,8 +1,9 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
2
  import { constants } from 'node:fs';
3
- import { link, lstat, mkdir, open, readdir, rename, stat, unlink } from 'node:fs/promises';
3
+ import { link, lstat, mkdir, open, readdir, realpath, rename, stat, unlink } from 'node:fs/promises';
4
4
  import { basename, dirname, join } from 'node:path';
5
5
  import { canonicalRoot, FileError, pinned, resolvePinnedDir, resolvePinnedDirCreate, resolvePinnedLeaf, resolveRelativeDir, resolveRelativeDirCreate, resolveRelativeFile, throwIfAborted, toFileError, withinRoot, } from './fence.js';
6
+ import { assertNotCredentialPath } from './credentials.js';
6
7
  import { assertScopeAllows } from './scope.js';
7
8
  /** Text file semantics of the platform agent tools (`file_*_request`): request-root scope only. */
8
9
  export const MAX_READ_BYTES = 8 * 1024 * 1024;
@@ -306,14 +307,37 @@ function requireRequestRoot(scope, operation) {
306
307
  * keeps the file worker's contract: a request cancelled once it is under way reports `outcome_unknown` for mutations
307
308
  * and `workstation_offline` for reads.
308
309
  */
309
- async function anchored(operation, root, anchor, signal, hooks, run) {
310
+ async function anchored(operation, root, anchor, leaves, signal, hooks, run) {
311
+ const insideRoot = withinRoot(root);
310
312
  try {
311
313
  await hooks?.afterValidation?.();
312
314
  return await pinned(anchor, async () => {
313
315
  await hooks?.afterPinned?.();
314
316
  throwIfAborted(signal);
317
+ // What each existing leaf resolves to, by the name stored on disk: a spelling a case-insensitive file system
318
+ // maps onto a store (`.git-credentialſ`) is judged by the store it opens, not by how it was written.
319
+ for (const leaf of leaves) {
320
+ let stored;
321
+ try {
322
+ stored = await realpath(leaf);
323
+ }
324
+ catch {
325
+ continue;
326
+ }
327
+ assertNotCredentialPath(stored);
328
+ }
315
329
  return run();
316
- }, { verify: withinRoot(root), signal });
330
+ }, {
331
+ // The directory the kernel now holds, and what the operation touches below it, are checked again: a directory
332
+ // swapped for a link after validation must not reach credential storage (credentials.ts).
333
+ verify: (pinnedReal, parent) => {
334
+ insideRoot(pinnedReal, parent);
335
+ assertNotCredentialPath(pinnedReal);
336
+ for (const leaf of leaves)
337
+ assertNotCredentialPath(join(pinnedReal, leaf));
338
+ },
339
+ signal,
340
+ });
317
341
  }
318
342
  catch (error) {
319
343
  if (signal?.aborted)
@@ -325,32 +349,42 @@ export async function readText(scope, request, signal, hooks) {
325
349
  requireRequestRoot(scope, 'read');
326
350
  const root = await canonicalRoot(scope.root);
327
351
  const target = await resolveRelativeFile(root, request.path, false);
352
+ assertNotCredentialPath(target);
328
353
  throwIfAborted(signal);
329
- // As the file worker did, the result names the leaf it read.
330
- return anchored('read', root, dirname(target), signal, hooks, () => readTextFile({ ...request, root: '.', path: basename(target) }, signal, resolvePinnedLeaf));
354
+ // The pinned read sees only the leaf; the result names the workspace-relative path it was asked for.
355
+ const data = await anchored('read', root, dirname(target), [basename(target)], signal, hooks, () => readTextFile({ ...request, root: '.', path: basename(target) }, signal, resolvePinnedLeaf));
356
+ return { ...data, path: request.path };
331
357
  }
332
358
  export async function writeText(scope, request, signal, hooks) {
333
359
  requireRequestRoot(scope, 'write');
334
360
  const root = await canonicalRoot(scope.root);
335
361
  const target = await resolveRelativeFile(root, request.path, request.mode === 'create');
336
- return serialTarget(target, () => throwIfAbortedThen(signal, () => anchored('write', root, dirname(target), signal, hooks, () => writeTextFile({ ...request, root: '.', path: basename(target) }, signal, resolvePinnedLeaf))));
362
+ assertNotCredentialPath(target);
363
+ const data = await serialTarget(target, () => throwIfAbortedThen(signal, () => anchored('write', root, dirname(target), [basename(target)], signal, hooks, () => writeTextFile({ ...request, root: '.', path: basename(target) }, signal, resolvePinnedLeaf))));
364
+ return { ...data, path: request.path };
337
365
  }
338
366
  export async function listDir(scope, request, signal, hooks) {
339
367
  requireRequestRoot(scope, 'list');
340
368
  const root = await canonicalRoot(scope.root);
341
369
  const target = await resolveRelativeDir(root, request.path);
370
+ assertNotCredentialPath(target);
342
371
  throwIfAborted(signal);
343
372
  // Listing the root pins the root itself; '' is the zero-component leaf.
344
373
  const anchor = target === root ? root : dirname(target);
345
374
  const leaf = target === root ? '' : basename(target);
346
- const data = await anchored('list', root, anchor, signal, hooks, () => listTextDir({ ...request, root: '.', path: leaf }, signal, resolvePinnedDir));
375
+ const data = await anchored('list', root, anchor, leaf === '' ? [] : [leaf], signal, hooks, () => listTextDir({ ...request, root: '.', path: leaf }, signal, resolvePinnedDir));
347
376
  return { ...data, path: request.path };
348
377
  }
349
378
  export async function makeDir(scope, request, signal, hooks) {
350
379
  requireRequestRoot(scope, 'mkdir');
351
380
  const root = await canonicalRoot(scope.root);
352
381
  const plan = await resolveRelativeDirCreate(root, request.path, request.parents);
382
+ // Every directory this would create, not only the last one.
383
+ const created = plan.missing.map((_, index) => plan.missing.slice(0, index + 1).join('/'));
384
+ assertNotCredentialPath(plan.anchor);
385
+ for (const leaf of created)
386
+ assertNotCredentialPath(join(plan.anchor, leaf));
353
387
  throwIfAborted(signal);
354
- const data = await anchored('mkdir', root, plan.anchor, signal, hooks, () => createTextDir({ ...request, root: '.', path: plan.missing.join('/') }, signal, resolvePinnedDirCreate));
388
+ const data = await anchored('mkdir', root, plan.anchor, created, signal, hooks, () => createTextDir({ ...request, root: '.', path: plan.missing.join('/') }, signal, resolvePinnedDirCreate));
355
389
  return { ...data, path: request.path };
356
390
  }
@@ -15,6 +15,7 @@
15
15
  */
16
16
  import fs from 'node:fs/promises';
17
17
  import path from 'node:path';
18
+ import { assertRegistrationOwner } from './runtime/ownership.js';
18
19
  const SUFFIX = '.jsonl';
19
20
  function parseEntry(line) {
20
21
  try {
@@ -58,7 +59,7 @@ export async function listLegacyJournalRefs(dir) {
58
59
  return refs.sort();
59
60
  }
60
61
  /** Every readable entry of `sessionRef`'s journal, in file order (unreadable lines skipped). A missing file is empty. */
61
- export async function readLegacyJournal(dir, sessionRef) {
62
+ export async function readLegacyJournal(dir, sessionRef, ownership) {
62
63
  let text;
63
64
  try {
64
65
  text = await fs.readFile(fileOf(dir, sessionRef), 'utf8');
@@ -73,8 +74,10 @@ export async function readLegacyJournal(dir, sessionRef) {
73
74
  if (line.length === 0)
74
75
  continue;
75
76
  const entry = parseEntry(line);
76
- if (entry)
77
+ if (entry) {
78
+ assertRegistrationOwner(JSON.parse(line), ownership, { directory: dir, kind: 'journal' });
77
79
  entries.push(entry);
80
+ }
78
81
  }
79
82
  return entries;
80
83
  }
@@ -1,6 +1,7 @@
1
1
  // Daemon-side: a typed `acp.*` client for keeper-acp (launch through KeeperLauncher, one long-lived KeeperClient per generation).
2
2
  // Exempt from the keeper-side import guard (it runs in the daemon), though it imports nothing but the kit and this keeper's protocol.
3
3
  import { KeeperClient } from '../kit/client.js';
4
+ import { assertRegistrationOwner, RegistrationOwnershipError } from '../../runtime/ownership.js';
4
5
  import { protocolWindow } from '../kit/protocol.js';
5
6
  import { ACP_PROTOCOL_VERSION, acpStream, } from './protocol.js';
6
7
  /** keeper-acp could not be reached (launch, connect or hello failed): the request was never sent. */
@@ -23,7 +24,7 @@ export class AcpKeeperClient {
23
24
  }
24
25
  get daemonProtocol() { return this.options.daemonProtocol ?? protocolWindow(ACP_PROTOCOL_VERSION); }
25
26
  clientFor(paths) {
26
- if (this.client && this.clientGen === paths.gen)
27
+ if (this.client && this.client.state !== 'closed' && this.clientGen === paths.gen)
27
28
  return this.client;
28
29
  this.client?.close();
29
30
  this.client = new KeeperClient({
@@ -48,6 +49,7 @@ export class AcpKeeperClient {
48
49
  get gen() { return this.options.gen; }
49
50
  /** The connected client of the current build, launching keeper-acp only when needed (a `gen`-bound client only connects). */
50
51
  async ensure() {
52
+ this.options.launcher.assertOwnership?.('acp', this.clientGen ?? this.options.gen);
51
53
  const current = this.client;
52
54
  if (current && current.state === 'ready')
53
55
  return current;
@@ -64,14 +66,43 @@ export class AcpKeeperClient {
64
66
  return client;
65
67
  }
66
68
  async call(method, params, options = {}) {
69
+ this.options.launcher.assertOwnership?.('acp', this.clientGen ?? this.options.gen);
67
70
  let client;
68
71
  try {
69
72
  client = await this.ensure();
70
73
  }
71
74
  catch (error) {
75
+ if (error instanceof RegistrationOwnershipError)
76
+ throw error;
72
77
  throw new AcpKeeperUnavailableError(error);
73
78
  }
74
- return client.request(method, params, options);
79
+ const ownership = this.options.launcher.ownership;
80
+ const input = params;
81
+ if (ownership && typeof input.sessionKey === 'string') {
82
+ const listed = await client.request('acp.list', {});
83
+ const existing = listed.sessions.find(session => session.sessionKey === input.sessionKey);
84
+ if (existing) {
85
+ if (this.options.launcher.assertSessionOwnership)
86
+ this.options.launcher.assertSessionOwnership(existing.label, this.clientGen);
87
+ else
88
+ assertRegistrationOwner(existing.label, ownership);
89
+ }
90
+ else if (method !== 'acp.spawn')
91
+ throw new RegistrationOwnershipError('Registration session owner is unknown');
92
+ }
93
+ if (ownership && method === 'acp.spawn')
94
+ params = { ...input, label: { ...input.label, registrationId: ownership.registrationId } };
95
+ const result = await client.request(method, params, options);
96
+ if (ownership && method === 'acp.list') {
97
+ const listed = result;
98
+ for (const session of listed.sessions) {
99
+ if (this.options.launcher.assertSessionOwnership)
100
+ this.options.launcher.assertSessionOwnership(session.label, this.clientGen);
101
+ else
102
+ assertRegistrationOwner(session.label, ownership);
103
+ }
104
+ }
105
+ return result;
75
106
  }
76
107
  spawn(params, options = {}) { return this.call('acp.spawn', params, options); }
77
108
  list() { return this.call('acp.list', {}); }
@@ -87,13 +118,18 @@ export class AcpKeeperClient {
87
118
  release(sessionKey) { return this.call('acp.release', { sessionKey }); }
88
119
  activity() { return this.call('keeper.activity', {}); }
89
120
  async subscribe(sessionKey, stream, handler, options) {
121
+ this.options.launcher.assertOwnership?.('acp', this.clientGen ?? this.options.gen);
90
122
  let client;
91
123
  try {
92
124
  client = await this.ensure();
93
125
  }
94
126
  catch (error) {
127
+ if (error instanceof RegistrationOwnershipError)
128
+ throw error;
95
129
  throw new AcpKeeperUnavailableError(error);
96
130
  }
131
+ if (this.options.launcher.ownership)
132
+ await this.attach(sessionKey);
97
133
  return client.subscribe(acpStream(stream, sessionKey), handler, options);
98
134
  }
99
135
  /** Drops the connection (keeper-acp and its adapters keep running). */
@@ -1,5 +1,5 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { readdirSync, readFileSync, statSync } from 'node:fs';
2
+ import { opendirSync, readFileSync, lstatSync, statSync } from 'node:fs';
3
3
  import { join, relative, sep } from 'node:path';
4
4
  /**
5
5
  * The keeper buildVersion: sha256 over the bundle bytes plus the sorted list of
@@ -14,22 +14,46 @@ import { join, relative, sep } from 'node:path';
14
14
  * unchanged keeper code does not change it.
15
15
  */
16
16
  const sha256 = (data) => createHash('sha256').update(data).digest('hex');
17
- function walkFiles(dir) {
17
+ function walkFiles(dir, limits) {
18
18
  const out = [];
19
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
20
- const full = join(dir, entry.name);
21
- if (entry.isDirectory())
22
- out.push(...walkFiles(full));
23
- else
24
- out.push(full);
25
- }
19
+ let entries = 0;
20
+ let bytes = 0;
21
+ const walk = (directory, depth) => {
22
+ if (limits && depth > limits.maxDepth)
23
+ throw Error('Native bundle depth limit');
24
+ const handle = opendirSync(directory, { bufferSize: 32 });
25
+ try {
26
+ for (;;) {
27
+ const entry = handle.readSync();
28
+ if (!entry)
29
+ break;
30
+ if (limits && ++entries > limits.maxEntries)
31
+ throw Error('Native bundle entry limit');
32
+ const full = join(directory, entry.name);
33
+ if (entry.isDirectory())
34
+ walk(full, depth + 1);
35
+ else {
36
+ if (limits?.maxBytes !== undefined) {
37
+ bytes += lstatSync(full).size;
38
+ if (bytes > limits.maxBytes)
39
+ throw Error('Native bundle byte limit');
40
+ }
41
+ out.push(full);
42
+ }
43
+ }
44
+ }
45
+ finally {
46
+ handle.closeSync();
47
+ }
48
+ };
49
+ walk(dir, 0);
26
50
  return out;
27
51
  }
28
52
  /** Relative (posix) paths of the native files carried next to a bundle, sorted. */
29
- export function nativeFilesOf(kindDir) {
53
+ export function nativeFilesOf(kindDir, limits) {
30
54
  let files;
31
55
  try {
32
- files = walkFiles(join(kindDir, 'node_modules'));
56
+ files = walkFiles(join(kindDir, 'node_modules'), limits);
33
57
  }
34
58
  catch (error) {
35
59
  if (error.code === 'ENOENT')