handmux 0.23.0 → 0.25.0

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 (327) hide show
  1. package/README.md +31 -8
  2. package/README.zh-CN.md +31 -8
  3. package/bin/handmux.js +14 -688
  4. package/bin/node-version.js +20 -0
  5. package/dist/bin/handmux.js +1015 -0
  6. package/dist/connectors/bridgeClient.js +385 -0
  7. package/dist/connectors/claude/index.js +476 -0
  8. package/dist/connectors/pi/bridgeClient.js +8 -0
  9. package/dist/connectors/pi/index.js +851 -0
  10. package/dist/hooks/handmux-notify.sh +64 -0
  11. package/{hooks → dist/hooks}/handmux-statusline.cjs +28 -6
  12. package/dist/hooks/handmux-write.cjs +264 -0
  13. package/dist/package.json +127 -0
  14. package/dist/public/assets/index-CTrqTqtT.css +32 -0
  15. package/dist/public/assets/index-DmjeMSQA.js +346 -0
  16. package/dist/public/assets/pcm-worklet-CxcDy7PB.js +1 -0
  17. package/{public → dist/public}/index.html +5 -4
  18. package/dist/src/agent-runtime/adapter.js +204 -0
  19. package/dist/src/agent-runtime/bridge.js +1006 -0
  20. package/dist/src/agent-runtime/bridgeStore.js +25 -0
  21. package/dist/src/agent-runtime/bridgeTransport.js +619 -0
  22. package/dist/src/agent-runtime/bridgeTypes.js +13 -0
  23. package/dist/src/agent-runtime/builtinRuntime.js +193 -0
  24. package/dist/src/agent-runtime/conversation.js +2179 -0
  25. package/dist/src/agent-runtime/conversationActivation.js +75 -0
  26. package/dist/src/agent-runtime/conversationActivity.js +69 -0
  27. package/dist/src/agent-runtime/conversationControls.js +206 -0
  28. package/dist/src/agent-runtime/conversationLiveHub.js +276 -0
  29. package/dist/src/agent-runtime/conversationStore.js +352 -0
  30. package/dist/src/agent-runtime/conversationTypes.js +1 -0
  31. package/dist/src/agent-runtime/conversationValidation.js +339 -0
  32. package/dist/src/agent-runtime/inbox.js +973 -0
  33. package/dist/src/agent-runtime/inboxPushProjection.js +69 -0
  34. package/dist/src/agent-runtime/inboxStore.js +25 -0
  35. package/dist/src/agent-runtime/inboxTypes.js +1 -0
  36. package/dist/src/agent-runtime/interaction.js +829 -0
  37. package/dist/src/agent-runtime/interactionLiveHub.js +225 -0
  38. package/dist/src/agent-runtime/interactionStore.js +18 -0
  39. package/dist/src/agent-runtime/interactionTypes.js +1 -0
  40. package/dist/src/agent-runtime/legacyInboxProjection.js +70 -0
  41. package/dist/src/agent-runtime/migrateLegacyCodexOutbox.js +336 -0
  42. package/dist/src/agent-runtime/resourceTypes.js +1 -0
  43. package/dist/src/agent-runtime/resources.js +245 -0
  44. package/dist/src/agent-runtime/run.js +328 -0
  45. package/dist/src/agent-runtime/runtime.js +793 -0
  46. package/dist/src/agent-runtime/sessionControl.js +164 -0
  47. package/dist/src/agent-runtime/subscriptionUsage.js +190 -0
  48. package/dist/src/agent-runtime/subscriptionUsageLegacy.js +18 -0
  49. package/dist/src/agent-runtime/tmuxRuntime.js +156 -0
  50. package/dist/src/agents/claude.js +203 -0
  51. package/dist/src/agents/claudeConversation.js +362 -0
  52. package/dist/src/agents/claudeInteraction.js +134 -0
  53. package/dist/src/agents/claudeSubscriptionUsage.js +61 -0
  54. package/dist/src/agents/codex.js +262 -0
  55. package/dist/src/agents/codexConversation.js +1103 -0
  56. package/dist/src/agents/codexConversationActivation.js +121 -0
  57. package/dist/src/agents/codexConversationControls.js +256 -0
  58. package/dist/src/agents/codexInteraction.js +306 -0
  59. package/dist/src/agents/codexSessionControl.js +118 -0
  60. package/dist/src/agents/codexSubscriptionUsage.js +313 -0
  61. package/dist/src/agents/codexTerminalControl.js +23 -0
  62. package/dist/src/agents/conversationProjectionSafety.js +416 -0
  63. package/dist/src/agents/index.js +39 -0
  64. package/dist/src/agents/nativeInbox.js +264 -0
  65. package/dist/src/agents/nativeInboxSources.js +115 -0
  66. package/dist/src/agents/pi.js +28 -0
  67. package/dist/src/agents/piConversation.js +523 -0
  68. package/dist/src/agents/piConversationHistory.js +502 -0
  69. package/dist/src/agents/piInboxBridge.js +368 -0
  70. package/dist/src/agents/piSessionControl.js +32 -0
  71. package/dist/src/agents/processIdentity.js +94 -0
  72. package/dist/src/agents/scanUtils.js +262 -0
  73. package/dist/src/apiAccountProviders.js +194 -0
  74. package/dist/src/apiAccounts.js +592 -0
  75. package/dist/src/apiErrors.js +86 -0
  76. package/{src → dist/src}/appName.js +12 -13
  77. package/dist/src/asr/iflyConfig.js +7 -0
  78. package/dist/src/asr/iflySign.js +14 -0
  79. package/dist/src/auth.js +27 -0
  80. package/dist/src/browser/bootstrap.js +72 -0
  81. package/dist/src/browser/cookieProfiles.js +413 -0
  82. package/dist/src/browser/coordinator.js +51 -0
  83. package/dist/src/browser/hammerheadCompat.js +104 -0
  84. package/dist/src/browser/hammerheadRedirectCompat.js +37 -0
  85. package/dist/src/browser/manager.js +560 -0
  86. package/dist/src/browser/originLabel.js +15 -0
  87. package/dist/src/browser/profilePersistence.js +235 -0
  88. package/dist/src/browser/publicProxy.js +176 -0
  89. package/dist/src/browser/routes.js +159 -0
  90. package/dist/src/browser/sessionStore.js +72 -0
  91. package/dist/src/browser/siteVersion.js +121 -0
  92. package/dist/src/browser/targetPolicy.js +163 -0
  93. package/{src → dist/src}/browser/worker.js +14 -18
  94. package/dist/src/browser/workerClient.js +286 -0
  95. package/dist/src/browser/workerServer.js +91 -0
  96. package/dist/src/captureBackground.js +95 -0
  97. package/dist/src/claudeEvents.js +525 -0
  98. package/dist/src/cli/agentIntegration.js +131 -0
  99. package/dist/src/cli/agentLauncher.js +26 -0
  100. package/dist/src/cli/claudeHooks.js +290 -0
  101. package/{src → dist/src}/cli/cloudflareUrl.js +2 -3
  102. package/dist/src/cli/cloudflared.js +106 -0
  103. package/dist/src/cli/codexManaged.js +80 -0
  104. package/{src → dist/src}/cli/cpolarUrl.js +8 -9
  105. package/dist/src/cli/drivers.js +98 -0
  106. package/dist/src/cli/hookScaffold.js +39 -0
  107. package/dist/src/cli/i18n/en.js +392 -0
  108. package/{src → dist/src}/cli/i18n/index.js +27 -25
  109. package/dist/src/cli/i18n/zh.js +391 -0
  110. package/dist/src/cli/legacyCodexHooks.js +52 -0
  111. package/{src → dist/src}/cli/natappUrl.js +2 -3
  112. package/dist/src/cli/options.js +245 -0
  113. package/dist/src/cli/piExtension.js +155 -0
  114. package/dist/src/cli/probe.js +16 -0
  115. package/{src → dist/src}/cli/prompt.js +4 -6
  116. package/dist/src/cli/pushCmd.js +102 -0
  117. package/{src → dist/src}/cli/qr.js +19 -19
  118. package/dist/src/cli/service.js +154 -0
  119. package/dist/src/cli/setupModel.js +194 -0
  120. package/dist/src/cli/setupWizard.js +647 -0
  121. package/dist/src/cli/shortcutEditor.js +351 -0
  122. package/dist/src/cli/sshTunnel.js +18 -0
  123. package/dist/src/cli/state.js +117 -0
  124. package/dist/src/cli/statusLine.js +115 -0
  125. package/dist/src/cli/supervisor.js +278 -0
  126. package/dist/src/cli/supervisorHealth.js +21 -0
  127. package/dist/src/cli/supervisorLaunch.js +148 -0
  128. package/dist/src/cli/supervisorProcesses.js +62 -0
  129. package/dist/src/cli/supervisorState.js +62 -0
  130. package/dist/src/cli/tmuxVersion.js +53 -0
  131. package/{src → dist/src}/cli/tunlite.js +7 -10
  132. package/dist/src/cli/tunnelClients.js +72 -0
  133. package/{src → dist/src}/cli/updateCheck.js +98 -69
  134. package/{src → dist/src}/cli/urlHost.js +8 -4
  135. package/dist/src/cli/workspaceCmd.js +347 -0
  136. package/dist/src/codexAppServer.js +3436 -0
  137. package/dist/src/codexConversationProjection.js +199 -0
  138. package/dist/src/codexDiff.js +118 -0
  139. package/dist/src/codexMessageIdentity.js +18 -0
  140. package/dist/src/codexPlan.js +30 -0
  141. package/dist/src/codexQueueProtocol.js +137 -0
  142. package/dist/src/codexStreamProtocol.js +207 -0
  143. package/dist/src/codexToolProtocol.js +68 -0
  144. package/dist/src/codexTranscriptParse.js +835 -0
  145. package/dist/src/codexUsageSnapshot.js +228 -0
  146. package/dist/src/config.js +8 -0
  147. package/{src → dist/src}/docPath.js +22 -24
  148. package/dist/src/docs.js +337 -0
  149. package/dist/src/git.js +284 -0
  150. package/dist/src/healthProtocol.js +97 -0
  151. package/dist/src/httpApi.js +96 -0
  152. package/dist/src/jsonStore.js +15 -0
  153. package/{src → dist/src}/keyNames.js +3 -3
  154. package/dist/src/notifications.js +111 -0
  155. package/dist/src/orphans.js +167 -0
  156. package/dist/src/paneInput.js +42 -0
  157. package/dist/src/pendingPrompt.js +194 -0
  158. package/dist/src/previewServer.js +202 -0
  159. package/dist/src/previews.js +179 -0
  160. package/dist/src/privateStateStore.js +124 -0
  161. package/dist/src/projectTask/backup.js +224 -0
  162. package/dist/src/projectTask/lock.js +108 -0
  163. package/dist/src/projectTask/migrations.js +90 -0
  164. package/dist/src/projectTask/routes.js +155 -0
  165. package/dist/src/projectTask/runtime.js +182 -0
  166. package/dist/src/projectTask/schema.js +56 -0
  167. package/dist/src/projectTask/store.js +457 -0
  168. package/dist/src/push.js +224 -0
  169. package/dist/src/routes/agents.js +631 -0
  170. package/dist/src/routes/apiAccounts.js +82 -0
  171. package/dist/src/routes/files.js +230 -0
  172. package/dist/src/routes/git.js +89 -0
  173. package/dist/src/routes/health.js +16 -0
  174. package/dist/src/routes/notifications.js +16 -0
  175. package/dist/src/routes/previews.js +43 -0
  176. package/dist/src/routes/push.js +145 -0
  177. package/dist/src/routes/sessions.js +244 -0
  178. package/dist/src/routes/system.js +220 -0
  179. package/dist/src/routes/terminal.js +244 -0
  180. package/dist/src/routes/workspace.js +267 -0
  181. package/dist/src/server.js +315 -0
  182. package/dist/src/shortcutConfig.js +83 -0
  183. package/{src → dist/src}/staticCache.js +5 -3
  184. package/{src → dist/src}/staticCompression.js +5 -6
  185. package/dist/src/terminalStream.js +441 -0
  186. package/{src → dist/src}/tmux/commands.js +185 -172
  187. package/dist/src/tmux/format.js +47 -0
  188. package/dist/src/transcriptParse.js +261 -0
  189. package/dist/src/transcriptReader.js +209 -0
  190. package/{src → dist/src}/trimCapture.js +11 -11
  191. package/dist/src/uploadTypes.js +31 -0
  192. package/dist/src/urlPolicy.js +18 -0
  193. package/dist/src/usagePaths.js +15 -0
  194. package/dist/src/workspace/agentRunner.js +301 -0
  195. package/dist/src/workspace/atomicJson.js +31 -0
  196. package/dist/src/workspace/capture.js +139 -0
  197. package/dist/src/workspace/checkpointer.js +228 -0
  198. package/dist/src/workspace/environment.js +71 -0
  199. package/dist/src/workspace/lock.js +137 -0
  200. package/dist/src/workspace/mapping.js +128 -0
  201. package/dist/src/workspace/operations.js +271 -0
  202. package/dist/src/workspace/paths.js +15 -0
  203. package/dist/src/workspace/planner.js +292 -0
  204. package/dist/src/workspace/restore.js +363 -0
  205. package/dist/src/workspace/runtime.js +318 -0
  206. package/dist/src/workspace/schema.js +156 -0
  207. package/dist/src/workspace/store.js +464 -0
  208. package/dist/src/workspace/tmuxAdapter.js +704 -0
  209. package/dist/src/workspaceProtocol.js +158 -0
  210. package/package.json +30 -17
  211. package/hooks/handmux-notify.sh +0 -22
  212. package/hooks/handmux-write.cjs +0 -78
  213. package/public/assets/index-C3N7OkyP.css +0 -32
  214. package/public/assets/index-D1DWcC5u.js +0 -337
  215. package/src/.gitkeep +0 -0
  216. package/src/agents/claude.js +0 -159
  217. package/src/agents/codex.js +0 -168
  218. package/src/agents/index.js +0 -21
  219. package/src/agents/processIdentity.js +0 -72
  220. package/src/agents/scanUtils.js +0 -225
  221. package/src/asr/iflyConfig.js +0 -10
  222. package/src/asr/iflySign.js +0 -16
  223. package/src/auth.js +0 -30
  224. package/src/browser/bootstrap.js +0 -68
  225. package/src/browser/cookieProfiles.js +0 -403
  226. package/src/browser/coordinator.js +0 -47
  227. package/src/browser/hammerheadCompat.js +0 -86
  228. package/src/browser/hammerheadRedirectCompat.js +0 -38
  229. package/src/browser/manager.js +0 -556
  230. package/src/browser/originLabel.js +0 -17
  231. package/src/browser/profilePersistence.js +0 -241
  232. package/src/browser/publicProxy.js +0 -175
  233. package/src/browser/routes.js +0 -154
  234. package/src/browser/sessionStore.js +0 -78
  235. package/src/browser/siteVersion.js +0 -124
  236. package/src/browser/targetPolicy.js +0 -137
  237. package/src/browser/workerClient.js +0 -253
  238. package/src/browser/workerServer.js +0 -98
  239. package/src/captureBackground.js +0 -93
  240. package/src/claudeEvents.js +0 -399
  241. package/src/cli/claudeHooks.js +0 -206
  242. package/src/cli/cloudflared.js +0 -99
  243. package/src/cli/codexManaged.js +0 -77
  244. package/src/cli/drivers.js +0 -97
  245. package/src/cli/hookScaffold.js +0 -33
  246. package/src/cli/i18n/en.js +0 -385
  247. package/src/cli/i18n/zh.js +0 -384
  248. package/src/cli/legacyCodexHooks.js +0 -49
  249. package/src/cli/options.js +0 -211
  250. package/src/cli/probe.js +0 -16
  251. package/src/cli/pushCmd.js +0 -60
  252. package/src/cli/service.js +0 -126
  253. package/src/cli/setupModel.js +0 -154
  254. package/src/cli/setupWizard.js +0 -506
  255. package/src/cli/shortcutEditor.js +0 -288
  256. package/src/cli/sshTunnel.js +0 -12
  257. package/src/cli/state.js +0 -90
  258. package/src/cli/statusLine.js +0 -90
  259. package/src/cli/supervisor.js +0 -183
  260. package/src/cli/supervisorProcesses.js +0 -57
  261. package/src/cli/tmuxVersion.js +0 -49
  262. package/src/cli/tunnelClients.js +0 -62
  263. package/src/cli/workspaceCmd.js +0 -300
  264. package/src/codexAppServer.js +0 -1620
  265. package/src/codexDiff.js +0 -108
  266. package/src/codexTranscriptParse.js +0 -413
  267. package/src/codexUsageSnapshot.cjs +0 -129
  268. package/src/config.js +0 -9
  269. package/src/docs.js +0 -228
  270. package/src/git.js +0 -208
  271. package/src/httpApi.js +0 -72
  272. package/src/jsonStore.js +0 -24
  273. package/src/notifications.js +0 -68
  274. package/src/orphans.js +0 -152
  275. package/src/pendingPrompt.js +0 -163
  276. package/src/previewServer.js +0 -168
  277. package/src/previews.js +0 -116
  278. package/src/push.js +0 -183
  279. package/src/routes/codex.js +0 -473
  280. package/src/routes/files.js +0 -172
  281. package/src/routes/git.js +0 -57
  282. package/src/routes/notifications.js +0 -19
  283. package/src/routes/previews.js +0 -35
  284. package/src/routes/push.js +0 -119
  285. package/src/routes/sessions.js +0 -168
  286. package/src/routes/system.js +0 -132
  287. package/src/routes/terminal.js +0 -210
  288. package/src/routes/transcript.js +0 -171
  289. package/src/routes/workspace.js +0 -267
  290. package/src/server.js +0 -186
  291. package/src/shortcutConfig.js +0 -75
  292. package/src/terminalStream.js +0 -391
  293. package/src/tmux/format.js +0 -45
  294. package/src/transcriptParse.js +0 -180
  295. package/src/transcriptReader.js +0 -101
  296. package/src/uploadTypes.js +0 -30
  297. package/src/urlPolicy.js +0 -14
  298. package/src/usage.js +0 -260
  299. package/src/workspace/agentRunner.js +0 -188
  300. package/src/workspace/atomicJson.js +0 -32
  301. package/src/workspace/capture.js +0 -139
  302. package/src/workspace/checkpointer.js +0 -187
  303. package/src/workspace/environment.js +0 -75
  304. package/src/workspace/lock.js +0 -128
  305. package/src/workspace/mapping.js +0 -124
  306. package/src/workspace/operations.js +0 -252
  307. package/src/workspace/paths.js +0 -16
  308. package/src/workspace/planner.js +0 -247
  309. package/src/workspace/restore.js +0 -279
  310. package/src/workspace/runtime.js +0 -314
  311. package/src/workspace/schema.js +0 -136
  312. package/src/workspace/store.js +0 -382
  313. package/src/workspace/tmuxAdapter.js +0 -554
  314. /package/{public → dist/public}/fonts/JetBrainsMonoNerdFontMono-Regular.woff2 +0 -0
  315. /package/{public → dist/public}/fonts/TWUnifont.woff2 +0 -0
  316. /package/{public → dist/public}/icons/apple-touch-icon.png +0 -0
  317. /package/{public → dist/public}/icons/badge-96.png +0 -0
  318. /package/{public → dist/public}/icons/icon-192-maskable.png +0 -0
  319. /package/{public → dist/public}/icons/icon-192.png +0 -0
  320. /package/{public → dist/public}/icons/icon-512-maskable.png +0 -0
  321. /package/{public → dist/public}/icons/icon-512.png +0 -0
  322. /package/{public → dist/public}/icons/logo.svg +0 -0
  323. /package/{public → dist/public}/manifest.webmanifest +0 -0
  324. /package/{public → dist/public}/offline.html +0 -0
  325. /package/{public → dist/public}/sw.js +0 -0
  326. /package/{src → dist/src}/browser/protocol.js +0 -0
  327. /package/{src → dist/src}/cli/cfNamed.js +0 -0
@@ -0,0 +1,25 @@
1
+ import { PrivateStateStore } from '../privateStateStore.js';
2
+ function copyState(value) {
3
+ return structuredClone(value);
4
+ }
5
+ export class MemoryBridgeStateStore {
6
+ #state = null;
7
+ load() {
8
+ return this.#state ? copyState(this.#state) : null;
9
+ }
10
+ save(state) {
11
+ this.#state = copyState(state);
12
+ }
13
+ }
14
+ export class FileBridgeStateStore {
15
+ #store;
16
+ constructor(file) {
17
+ this.#store = new PrivateStateStore(file);
18
+ }
19
+ load() {
20
+ return this.#store.readStrict();
21
+ }
22
+ save(state) {
23
+ this.#store.write(state);
24
+ }
25
+ }
@@ -0,0 +1,619 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import net from 'node:net';
4
+ import path from 'node:path';
5
+ import { DEFAULT_BRIDGE_LIMITS } from './bridgeTypes.js';
6
+ const PROTOCOL_VERSION = 1;
7
+ const NAME_RE = /^[a-z][a-z0-9._-]{0,63}$/;
8
+ const MAX_HANDSHAKE_BYTES = 64 * 1024;
9
+ const DEFAULT_HANDSHAKE_TIMEOUT_MS = 5_000;
10
+ const WIRE_OVERHEAD_BYTES = 16 * 1024;
11
+ function record(value) {
12
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
13
+ ? value : null;
14
+ }
15
+ function bounded(value, max) {
16
+ return typeof value === 'string' && value.length > 0 && value.length <= max;
17
+ }
18
+ function validChannelName(value) {
19
+ return typeof value === 'string' && NAME_RE.test(value) && !value.startsWith('handmux.');
20
+ }
21
+ function errorMessage(error) {
22
+ return (error instanceof Error ? error.message : String(error)).slice(0, 4096);
23
+ }
24
+ function nonceProof(authToken, nonce) {
25
+ return crypto.createHmac('sha256', authToken).update(nonce).digest('base64url');
26
+ }
27
+ function equalProof(actual, expected) {
28
+ if (typeof actual !== 'string')
29
+ return false;
30
+ const first = Buffer.from(actual);
31
+ const second = Buffer.from(expected);
32
+ return first.length === second.length && crypto.timingSafeEqual(first, second);
33
+ }
34
+ function wirePeer(socket, maxBytes, onMessage) {
35
+ const peer = {
36
+ socket,
37
+ maxBytes,
38
+ buffer: Buffer.alloc(0),
39
+ closed: false,
40
+ send(value) {
41
+ if (peer.closed || socket.destroyed)
42
+ throw new Error('Bridge transport is closed');
43
+ const encoded = Buffer.from(`${JSON.stringify(value)}\n`);
44
+ if (encoded.length > peer.maxBytes)
45
+ throw new Error('Bridge transport frame exceeds limit');
46
+ socket.write(encoded);
47
+ },
48
+ close() {
49
+ if (peer.closed)
50
+ return;
51
+ peer.closed = true;
52
+ socket.destroy();
53
+ },
54
+ };
55
+ socket.on('data', (chunk) => {
56
+ if (peer.closed)
57
+ return;
58
+ peer.buffer = Buffer.concat([peer.buffer, chunk]);
59
+ if (peer.buffer.length > peer.maxBytes && !peer.buffer.includes(0x0a)) {
60
+ peer.close();
61
+ return;
62
+ }
63
+ while (true) {
64
+ const newline = peer.buffer.indexOf(0x0a);
65
+ if (newline < 0)
66
+ break;
67
+ const frame = peer.buffer.subarray(0, newline);
68
+ peer.buffer = peer.buffer.subarray(newline + 1);
69
+ if (!frame.length)
70
+ continue;
71
+ if (frame.length > peer.maxBytes) {
72
+ peer.close();
73
+ return;
74
+ }
75
+ try {
76
+ onMessage(JSON.parse(frame.toString('utf8')));
77
+ }
78
+ catch {
79
+ peer.close();
80
+ return;
81
+ }
82
+ }
83
+ });
84
+ return peer;
85
+ }
86
+ function validCandidate(value) {
87
+ const candidate = record(value);
88
+ const process = record(candidate?.process);
89
+ if (!candidate || !bounded(candidate.paneId, 256) || !bounded(candidate.attachmentId, 512)
90
+ || (candidate.sessionId !== undefined && !bounded(candidate.sessionId, 1024))
91
+ || (candidate.implementationVersion !== undefined
92
+ && (!Number.isSafeInteger(candidate.implementationVersion)
93
+ || Number(candidate.implementationVersion) <= 0))
94
+ || !process || !Number.isSafeInteger(process.pid) || Number(process.pid) <= 0)
95
+ return false;
96
+ return (process.startedAt === undefined
97
+ || (typeof process.startedAt === 'number' && Number.isFinite(process.startedAt)))
98
+ && (process.tty === undefined || bounded(process.tty, 1024));
99
+ }
100
+ function validRun(value, adapterId, paneId) {
101
+ const run = record(value);
102
+ return run !== null && run.agentId === adapterId && run.paneId === paneId
103
+ && bounded(run.runId, 256)
104
+ && (run.implementationVersion === undefined
105
+ || (Number.isSafeInteger(run.implementationVersion)
106
+ && Number(run.implementationVersion) > 0))
107
+ && (run.sessionId === undefined || bounded(run.sessionId, 1024));
108
+ }
109
+ function validLimits(value) {
110
+ const limits = record(value);
111
+ if (!limits)
112
+ return false;
113
+ return Object.entries(DEFAULT_BRIDGE_LIMITS).every(([name, maximum]) => {
114
+ const actual = limits[name];
115
+ return typeof actual === 'number' && Number.isFinite(actual) && actual > 0 && actual <= maximum;
116
+ }) && Number(limits.defaultRequestTimeoutMs) <= Number(limits.maxRequestTimeoutMs);
117
+ }
118
+ function result(peer, id, value) {
119
+ if (bounded(id, 256))
120
+ peer.send({ type: 'result', id, ok: true, value });
121
+ }
122
+ function failure(peer, id, error) {
123
+ if (bounded(id, 256))
124
+ peer.send({ type: 'result', id, ok: false, error: errorMessage(error) });
125
+ }
126
+ async function liveSocket(socketPath) {
127
+ if (!fs.existsSync(socketPath))
128
+ return false;
129
+ return new Promise((resolve, reject) => {
130
+ const socket = net.createConnection(socketPath);
131
+ let settled = false;
132
+ const finish = (value, error) => {
133
+ if (settled)
134
+ return;
135
+ settled = true;
136
+ socket.destroy();
137
+ if (error)
138
+ reject(error);
139
+ else
140
+ resolve(value);
141
+ };
142
+ socket.once('connect', () => finish(true));
143
+ socket.once('error', (error) => {
144
+ if (error.code === 'ECONNREFUSED' || error.code === 'ENOENT')
145
+ finish(false);
146
+ else
147
+ finish(false, error);
148
+ });
149
+ socket.setTimeout(500, () => finish(false, new Error('Timed out probing existing Bridge socket')));
150
+ });
151
+ }
152
+ function ensurePrivateDirectory(directory) {
153
+ try {
154
+ const stat = fs.lstatSync(directory);
155
+ if (!stat.isDirectory() || (stat.mode & 0o077) !== 0) {
156
+ throw new Error('Bridge transport directory must be a private directory');
157
+ }
158
+ if (typeof process.getuid === 'function' && stat.uid !== process.getuid()) {
159
+ throw new Error('Bridge transport directory must be owned by the current user');
160
+ }
161
+ }
162
+ catch (error) {
163
+ if (!(error instanceof Error) || !('code' in error) || error.code !== 'ENOENT')
164
+ throw error;
165
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
166
+ const stat = fs.lstatSync(directory);
167
+ if (!stat.isDirectory() || (stat.mode & 0o077) !== 0) {
168
+ throw new Error('Bridge transport directory could not be created privately');
169
+ }
170
+ }
171
+ }
172
+ export class LocalAgentBridgeTransportServer {
173
+ socketPath;
174
+ #bridge;
175
+ #authToken;
176
+ #authorize;
177
+ #connected;
178
+ #newNonce;
179
+ #handshakeTimeoutMs;
180
+ #sessions = new Set();
181
+ #server;
182
+ constructor({ socketPath, authToken, bridge, authorize, connected, newNonce = () => crypto.randomBytes(32).toString('base64url'), handshakeTimeoutMs = DEFAULT_HANDSHAKE_TIMEOUT_MS, }) {
183
+ if (!path.isAbsolute(socketPath) || !bounded(authToken, 1024) || authToken.length < 32
184
+ || !bridge || typeof authorize !== 'function' || !Number.isSafeInteger(handshakeTimeoutMs)
185
+ || handshakeTimeoutMs <= 0 || (connected !== undefined && typeof connected !== 'function')) {
186
+ throw new TypeError('Bridge transport requires an absolute socket path, Bridge, and authorizer');
187
+ }
188
+ this.socketPath = socketPath;
189
+ this.#authToken = authToken;
190
+ this.#bridge = bridge;
191
+ this.#authorize = authorize;
192
+ this.#connected = connected;
193
+ this.#newNonce = newNonce;
194
+ this.#handshakeTimeoutMs = handshakeTimeoutMs;
195
+ }
196
+ async start() {
197
+ if (this.#server)
198
+ return;
199
+ ensurePrivateDirectory(path.dirname(this.socketPath));
200
+ if (await liveSocket(this.socketPath)) {
201
+ throw new Error('Bridge transport socket is already owned by a live server');
202
+ }
203
+ try {
204
+ fs.unlinkSync(this.socketPath);
205
+ }
206
+ catch (error) {
207
+ if (!(error instanceof Error) || !('code' in error) || error.code !== 'ENOENT')
208
+ throw error;
209
+ }
210
+ const server = net.createServer((socket) => this.#accept(socket));
211
+ this.#server = server;
212
+ try {
213
+ await new Promise((resolve, reject) => {
214
+ server.once('error', reject);
215
+ server.listen(this.socketPath, () => {
216
+ server.removeListener('error', reject);
217
+ resolve();
218
+ });
219
+ });
220
+ }
221
+ catch (error) {
222
+ if (this.#server === server)
223
+ this.#server = undefined;
224
+ throw error;
225
+ }
226
+ try {
227
+ fs.chmodSync(this.socketPath, 0o600);
228
+ }
229
+ catch (error) {
230
+ await this.close();
231
+ throw error;
232
+ }
233
+ }
234
+ async close() {
235
+ const server = this.#server;
236
+ if (!server)
237
+ return;
238
+ this.#server = undefined;
239
+ for (const session of [...this.#sessions])
240
+ this.#closeSession(session);
241
+ await new Promise((resolve) => server.close(() => resolve()));
242
+ try {
243
+ fs.unlinkSync(this.socketPath);
244
+ }
245
+ catch { /* already removed */ }
246
+ }
247
+ #accept(socket) {
248
+ socket.setNoDelay(true);
249
+ const nonce = this.#newNonce();
250
+ const session = {};
251
+ const peer = wirePeer(socket, MAX_HANDSHAKE_BYTES, (value) => {
252
+ const operation = session.tail.then(() => this.#message(session, nonce, value));
253
+ session.tail = operation.catch((error) => {
254
+ try {
255
+ failure(peer, record(value)?.id, error);
256
+ }
257
+ catch { /* closing */ }
258
+ if (!session.connection)
259
+ this.#closeSession(session);
260
+ });
261
+ });
262
+ const handshakeTimer = setTimeout(() => this.#closeSession(session), this.#handshakeTimeoutMs);
263
+ handshakeTimer.unref?.();
264
+ Object.assign(session, {
265
+ peer, unregister: new Map(), pending: new Map(), handshakeTimer, tail: Promise.resolve(),
266
+ });
267
+ this.#sessions.add(session);
268
+ socket.once('close', () => this.#closeSession(session));
269
+ socket.once('error', () => this.#closeSession(session));
270
+ peer.send({ type: 'challenge', protocolVersion: PROTOCOL_VERSION, nonce });
271
+ }
272
+ async #message(session, nonce, raw) {
273
+ const value = record(raw);
274
+ if (!value || typeof value.type !== 'string')
275
+ throw new Error('Invalid Bridge transport frame');
276
+ if (!session.connection) {
277
+ if (value.type !== 'hello' || value.protocolVersion !== PROTOCOL_VERSION
278
+ || !equalProof(value.proof, nonceProof(this.#authToken, nonce))
279
+ || !bounded(value.adapterId, 64) || !NAME_RE.test(value.adapterId)
280
+ || !validCandidate(value.candidate))
281
+ throw new Error('Invalid Bridge transport handshake');
282
+ const replacing = value.newGeneration === true;
283
+ if ((value.newGeneration !== undefined && typeof value.newGeneration !== 'boolean')
284
+ || (replacing && !bounded(value.generationId, 256))
285
+ || (!replacing && value.generationId !== undefined)) {
286
+ throw new Error('Invalid Bridge transport generation');
287
+ }
288
+ const lease = await this.#authorize(value.adapterId, structuredClone(value.candidate), replacing ? { id: value.generationId, replace: true } : undefined);
289
+ if (lease.ref.agentId !== value.adapterId)
290
+ throw new Error('Bridge transport authorization mismatch');
291
+ if (!this.#sessions.has(session) || session.peer.closed) {
292
+ throw new Error('Bridge transport closed during authorization');
293
+ }
294
+ session.lease = lease;
295
+ session.connection = this.#bridge.connect(lease);
296
+ session.connection.signal.addEventListener('abort', () => this.#closeSession(session), { once: true });
297
+ await this.#connected?.(lease, session.connection);
298
+ if (!this.#sessions.has(session) || session.peer.closed || session.connection.signal.aborted) {
299
+ throw new Error('Bridge transport closed while activating connection');
300
+ }
301
+ if (session.handshakeTimer)
302
+ clearTimeout(session.handshakeTimer);
303
+ session.handshakeTimer = undefined;
304
+ session.peer.maxBytes = Math.max(session.connection.limits.maxFrameBytes, session.connection.limits.maxSnapshotBytes) + WIRE_OVERHEAD_BYTES;
305
+ session.peer.send({
306
+ type: 'ready', protocolVersion: PROTOCOL_VERSION,
307
+ connectionId: session.connection.connectionId,
308
+ run: lease.ref,
309
+ limits: session.connection.limits,
310
+ });
311
+ return;
312
+ }
313
+ const connection = session.connection;
314
+ const id = value.id;
315
+ if (value.type === 'response') {
316
+ if (!bounded(value.requestId, 256))
317
+ throw new Error('Invalid Bridge response id');
318
+ const pending = session.pending.get(value.requestId);
319
+ if (!pending)
320
+ return;
321
+ session.pending.delete(value.requestId);
322
+ pending.detachAbort();
323
+ if (value.ok === true)
324
+ pending.resolve(value.value);
325
+ else
326
+ pending.reject(new Error(typeof value.error === 'string' ? value.error : 'Bridge handler failed'));
327
+ return;
328
+ }
329
+ if (value.type === 'snapshot') {
330
+ if (!bounded(id, 256) || !validChannelName(value.channel))
331
+ throw new Error('Invalid snapshot frame');
332
+ result(session.peer, id, await connection.channel(value.channel).setSnapshot(value.value));
333
+ return;
334
+ }
335
+ if (value.type === 'publish') {
336
+ if (!bounded(id, 256) || !validChannelName(value.channel) || !record(value.event)
337
+ || (value.delivery !== undefined && value.delivery !== 'ephemeral' && value.delivery !== 'durable')) {
338
+ throw new Error('Invalid publish frame');
339
+ }
340
+ result(session.peer, id, await connection.channel(value.channel).publish(value.event, value.delivery === undefined ? undefined : { delivery: value.delivery }));
341
+ return;
342
+ }
343
+ if (value.type === 'handle') {
344
+ if (!bounded(id, 256) || !validChannelName(value.channel) || !validChannelName(value.method)) {
345
+ throw new Error('Invalid handler frame');
346
+ }
347
+ const key = `${value.channel}\0${value.method}`;
348
+ if (session.unregister.has(key))
349
+ throw new Error('Bridge handler is already registered');
350
+ const unregister = connection.channel(value.channel).handle(value.method, (payload, context) => (this.#requestClient(session, value.channel, value.method, payload, context)));
351
+ session.unregister.set(key, unregister);
352
+ result(session.peer, id, { registered: true });
353
+ return;
354
+ }
355
+ if (value.type === 'unhandle') {
356
+ if (!bounded(id, 256) || !validChannelName(value.channel) || !validChannelName(value.method)) {
357
+ throw new Error('Invalid unhandle frame');
358
+ }
359
+ const key = `${value.channel}\0${value.method}`;
360
+ session.unregister.get(key)?.();
361
+ session.unregister.delete(key);
362
+ result(session.peer, id, { registered: false });
363
+ return;
364
+ }
365
+ throw new Error('Unsupported Bridge transport frame');
366
+ }
367
+ #requestClient(session, channel, method, payload, context) {
368
+ return new Promise((resolve, reject) => {
369
+ const cancel = () => {
370
+ if (!session.pending.delete(context.requestId))
371
+ return;
372
+ try {
373
+ session.peer.send({ type: 'cancel', requestId: context.requestId });
374
+ }
375
+ catch { /* closed */ }
376
+ reject(context.signal.reason instanceof Error ? context.signal.reason : new Error('Bridge request cancelled'));
377
+ };
378
+ context.signal.addEventListener('abort', cancel, { once: true });
379
+ session.pending.set(context.requestId, {
380
+ resolve, reject,
381
+ detachAbort: () => context.signal.removeEventListener('abort', cancel),
382
+ });
383
+ try {
384
+ session.peer.send({
385
+ type: 'request', requestId: context.requestId, channel, method, payload,
386
+ deadlineAt: context.deadlineAt,
387
+ });
388
+ }
389
+ catch (error) {
390
+ session.pending.delete(context.requestId);
391
+ context.signal.removeEventListener('abort', cancel);
392
+ reject(error);
393
+ }
394
+ });
395
+ }
396
+ #closeSession(session) {
397
+ if (!this.#sessions.delete(session))
398
+ return;
399
+ if (session.handshakeTimer)
400
+ clearTimeout(session.handshakeTimer);
401
+ session.handshakeTimer = undefined;
402
+ for (const unregister of session.unregister.values())
403
+ unregister();
404
+ session.unregister.clear();
405
+ for (const pending of session.pending.values()) {
406
+ pending.detachAbort();
407
+ pending.reject(new Error('Bridge transport closed'));
408
+ }
409
+ session.pending.clear();
410
+ session.connection?.close();
411
+ session.peer.close();
412
+ }
413
+ }
414
+ export async function connectBridgeTransport({ socketPath, authToken, adapterId, candidate, generation, connect = net.createConnection, newRequestId = crypto.randomUUID, handshakeTimeoutMs = DEFAULT_HANDSHAKE_TIMEOUT_MS, }) {
415
+ if (!path.isAbsolute(socketPath) || !bounded(authToken, 1024) || authToken.length < 32
416
+ || !NAME_RE.test(adapterId) || !validCandidate(candidate)
417
+ || (generation !== undefined && (!bounded(generation.id, 256) || generation.replace !== true))
418
+ || !Number.isSafeInteger(handshakeTimeoutMs) || handshakeTimeoutMs <= 0) {
419
+ throw new TypeError('Invalid Bridge transport client options');
420
+ }
421
+ const socket = connect(socketPath);
422
+ const abort = new AbortController();
423
+ const pending = new Map();
424
+ const handlers = new Map();
425
+ const handlerAborts = new Map();
426
+ let peer;
427
+ const nextId = () => {
428
+ for (let attempt = 0; attempt < 8; attempt += 1) {
429
+ const id = newRequestId();
430
+ if (bounded(id, 256) && !pending.has(id))
431
+ return id;
432
+ }
433
+ throw new Error('Unable to allocate Bridge wire request id');
434
+ };
435
+ const request = (value) => new Promise((resolve, reject) => {
436
+ const id = nextId();
437
+ pending.set(id, { resolve, reject });
438
+ try {
439
+ peer.send({ ...value, id });
440
+ }
441
+ catch (error) {
442
+ pending.delete(id);
443
+ reject(error);
444
+ }
445
+ });
446
+ let handshakeTimer;
447
+ let onHandshakeError;
448
+ let onHandshakeClose;
449
+ const opened = new Promise((resolve, reject) => {
450
+ let settled = false;
451
+ const finish = (outcome) => {
452
+ if (settled)
453
+ return;
454
+ settled = true;
455
+ if (handshakeTimer)
456
+ clearTimeout(handshakeTimer);
457
+ handshakeTimer = undefined;
458
+ if ('error' in outcome)
459
+ reject(outcome.error);
460
+ else
461
+ resolve(outcome.value);
462
+ };
463
+ onHandshakeError = (error) => finish({ error });
464
+ onHandshakeClose = () => finish({ error: new Error('Bridge transport closed during handshake') });
465
+ socket.once('error', onHandshakeError);
466
+ socket.once('close', onHandshakeClose);
467
+ peer = wirePeer(socket, MAX_HANDSHAKE_BYTES, (raw) => {
468
+ const value = record(raw);
469
+ if (!value || typeof value.type !== 'string') {
470
+ peer.close();
471
+ return;
472
+ }
473
+ if (value.type === 'challenge' && value.protocolVersion === PROTOCOL_VERSION
474
+ && bounded(value.nonce, 256)) {
475
+ peer.send({
476
+ type: 'hello', protocolVersion: PROTOCOL_VERSION,
477
+ proof: nonceProof(authToken, value.nonce),
478
+ adapterId, candidate,
479
+ ...(generation === undefined ? {} : {
480
+ newGeneration: true,
481
+ generationId: generation.id,
482
+ }),
483
+ });
484
+ return;
485
+ }
486
+ if (value.type === 'ready') {
487
+ finish({ value });
488
+ return;
489
+ }
490
+ if (value.type === 'result' && bounded(value.id, 256)) {
491
+ const waiter = pending.get(value.id);
492
+ if (!waiter)
493
+ return;
494
+ pending.delete(value.id);
495
+ if (value.ok === true)
496
+ waiter.resolve(value.value);
497
+ else
498
+ waiter.reject(new Error(typeof value.error === 'string' ? value.error : 'Bridge operation failed'));
499
+ return;
500
+ }
501
+ if (value.type === 'cancel' && bounded(value.requestId, 256)) {
502
+ handlerAborts.get(value.requestId)?.abort('server_cancelled');
503
+ handlerAborts.delete(value.requestId);
504
+ return;
505
+ }
506
+ if (value.type === 'request' && bounded(value.requestId, 256)
507
+ && validChannelName(value.channel) && validChannelName(value.method)
508
+ && typeof value.deadlineAt === 'number') {
509
+ const handler = handlers.get(`${value.channel}\0${value.method}`);
510
+ if (!handler) {
511
+ peer.send({ type: 'response', requestId: value.requestId, ok: false, error: 'handler unavailable' });
512
+ return;
513
+ }
514
+ const controller = new AbortController();
515
+ handlerAborts.set(value.requestId, controller);
516
+ Promise.resolve(handler(value.payload, {
517
+ requestId: value.requestId,
518
+ deadlineAt: value.deadlineAt,
519
+ signal: controller.signal,
520
+ })).then((response) => {
521
+ try {
522
+ peer.send({ type: 'response', requestId: value.requestId, ok: true, value: response });
523
+ }
524
+ catch { /* request was cancelled or transport closed */ }
525
+ }, (error) => {
526
+ try {
527
+ peer.send({
528
+ type: 'response', requestId: value.requestId, ok: false, error: errorMessage(error),
529
+ });
530
+ }
531
+ catch { /* request was cancelled or transport closed */ }
532
+ }).finally(() => handlerAborts.delete(value.requestId)).catch(() => { });
533
+ }
534
+ });
535
+ handshakeTimer = setTimeout(() => {
536
+ finish({ error: new Error('Bridge transport handshake timed out') });
537
+ peer.close();
538
+ }, handshakeTimeoutMs);
539
+ handshakeTimer.unref?.();
540
+ });
541
+ const close = () => {
542
+ if (abort.signal.aborted)
543
+ return;
544
+ abort.abort('transport_closed');
545
+ peer.close();
546
+ for (const waiter of pending.values())
547
+ waiter.reject(new Error('Bridge transport closed'));
548
+ pending.clear();
549
+ for (const controller of handlerAborts.values())
550
+ controller.abort('transport_closed');
551
+ handlerAborts.clear();
552
+ };
553
+ socket.once('close', close);
554
+ let handshake;
555
+ try {
556
+ handshake = await opened;
557
+ }
558
+ catch (error) {
559
+ close();
560
+ throw error;
561
+ }
562
+ socket.removeListener('error', onHandshakeError);
563
+ socket.removeListener('close', onHandshakeClose);
564
+ socket.on('error', close);
565
+ if (handshake.protocolVersion !== PROTOCOL_VERSION
566
+ || !validRun(handshake.run, adapterId, candidate.paneId)
567
+ || !bounded(handshake.connectionId, 256) || !validLimits(handshake.limits)) {
568
+ close();
569
+ throw new Error('Invalid Bridge transport ready frame');
570
+ }
571
+ const limits = handshake.limits;
572
+ peer.maxBytes = Math.max(limits.maxFrameBytes, limits.maxSnapshotBytes) + WIRE_OVERHEAD_BYTES;
573
+ const channels = new Map();
574
+ return Object.freeze({
575
+ run: structuredClone(handshake.run),
576
+ connectionId: handshake.connectionId,
577
+ limits: structuredClone(limits),
578
+ signal: abort.signal,
579
+ channel(name) {
580
+ if (!validChannelName(name) || abort.signal.aborted)
581
+ throw new Error('Invalid or closed Bridge channel');
582
+ const existing = channels.get(name);
583
+ if (existing)
584
+ return existing;
585
+ const channel = Object.freeze({
586
+ setSnapshot: (value) => request({ type: 'snapshot', channel: name, value }),
587
+ publish: (event, options) => request({
588
+ type: 'publish', channel: name, event,
589
+ ...(options?.delivery === undefined ? {} : { delivery: options.delivery }),
590
+ }),
591
+ async handle(method, handler) {
592
+ if (!validChannelName(method) || typeof handler !== 'function') {
593
+ throw new Error('Invalid Bridge handler');
594
+ }
595
+ const key = `${name}\0${method}`;
596
+ if (handlers.has(key))
597
+ throw new Error('Bridge handler is already registered');
598
+ handlers.set(key, handler);
599
+ try {
600
+ await request({ type: 'handle', channel: name, method });
601
+ }
602
+ catch (error) {
603
+ handlers.delete(key);
604
+ throw error;
605
+ }
606
+ return async () => {
607
+ if (handlers.get(key) !== handler)
608
+ return;
609
+ handlers.delete(key);
610
+ await request({ type: 'unhandle', channel: name, method });
611
+ };
612
+ },
613
+ });
614
+ channels.set(name, channel);
615
+ return channel;
616
+ },
617
+ close,
618
+ });
619
+ }
@@ -0,0 +1,13 @@
1
+ export const DEFAULT_BRIDGE_LIMITS = Object.freeze({
2
+ maxFrameBytes: 256 * 1024,
3
+ maxSnapshotBytes: 1024 * 1024,
4
+ maxQueuedEventsPerChannel: 256,
5
+ maxQueuedBytesPerChannel: 4 * 1024 * 1024,
6
+ maxDurableSpoolBytesPerAdapter: 16 * 1024 * 1024,
7
+ maxRequestsPerRun: 32,
8
+ maxRequestsPerAdapter: 128,
9
+ defaultRequestTimeoutMs: 30_000,
10
+ maxRequestTimeoutMs: 5 * 60_000,
11
+ sustainedEventsPerSecond: 200,
12
+ burstEvents: 500,
13
+ });