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,245 @@
1
+ import { constants } from 'node:fs';
2
+ import { open, realpath } from 'node:fs/promises';
3
+ import { basename, isAbsolute, relative, sep } from 'node:path';
4
+ import { randomUUID } from 'node:crypto';
5
+ const DEFAULT_TTL_MS = 60 * 60 * 1000;
6
+ const DEFAULT_MAX_RESOURCE_BYTES = 16 * 1024 * 1024;
7
+ const DEFAULT_MAX_BYTE_STORAGE = 64 * 1024 * 1024;
8
+ const DEFAULT_MAX_RESOURCES = 1024;
9
+ const DEFAULT_MAX_SESSION_RESOURCES = 128;
10
+ const RESOURCE_ID_RE = /^[a-zA-Z0-9_-]{16,256}$/;
11
+ const MEDIA_TYPE_RE = /^[a-zA-Z0-9][a-zA-Z0-9!#$&^_.+-]{0,126}\/[a-zA-Z0-9][a-zA-Z0-9!#$&^_.+-]{0,126}$/;
12
+ export class AgentResourceError extends Error {
13
+ code;
14
+ constructor(code, message) {
15
+ super(message);
16
+ this.code = code;
17
+ this.name = 'AgentResourceError';
18
+ }
19
+ }
20
+ function validText(value, max) {
21
+ return typeof value === 'string' && value.length > 0 && Buffer.byteLength(value) <= max;
22
+ }
23
+ function validSession(value) {
24
+ return value !== null && typeof value === 'object'
25
+ && validText(value.agentId, 64)
26
+ && validText(value.sessionId, 1024);
27
+ }
28
+ function validName(value) {
29
+ return value === undefined || (validText(value, 1024) && !/[\\/\0-\x1f\x7f]/.test(value));
30
+ }
31
+ function validMediaType(value) {
32
+ return typeof value === 'string' && MEDIA_TYPE_RE.test(value);
33
+ }
34
+ function within(root, target) {
35
+ const child = relative(root, target);
36
+ return child === '' || (!child.startsWith(`..${sep}`) && child !== '..' && !isAbsolute(child));
37
+ }
38
+ function positiveLimit(value, fallback) {
39
+ return value === undefined ? fallback : Math.max(1, Math.floor(value));
40
+ }
41
+ export class AgentResourceService {
42
+ #allowedFileRoots;
43
+ #now;
44
+ #newResourceId;
45
+ #ttlMs;
46
+ #maxResourceBytes;
47
+ #maxByteStorage;
48
+ #maxResources;
49
+ #maxSessionResources;
50
+ #records = new Map();
51
+ #byteStorage = 0;
52
+ constructor({ allowedFileRoots = {}, now = Date.now, newResourceId = randomUUID, ttlMs = DEFAULT_TTL_MS, maxResourceBytes = DEFAULT_MAX_RESOURCE_BYTES, maxByteStorage = DEFAULT_MAX_BYTE_STORAGE, maxResources = DEFAULT_MAX_RESOURCES, maxSessionResources = DEFAULT_MAX_SESSION_RESOURCES, } = {}) {
53
+ this.#allowedFileRoots = allowedFileRoots;
54
+ this.#now = now;
55
+ this.#newResourceId = newResourceId;
56
+ this.#ttlMs = positiveLimit(ttlMs, DEFAULT_TTL_MS);
57
+ this.#maxResourceBytes = positiveLimit(maxResourceBytes, DEFAULT_MAX_RESOURCE_BYTES);
58
+ this.#maxByteStorage = positiveLimit(maxByteStorage, DEFAULT_MAX_BYTE_STORAGE);
59
+ this.#maxResources = positiveLimit(maxResources, DEFAULT_MAX_RESOURCES);
60
+ this.#maxSessionResources = positiveLimit(maxSessionResources, DEFAULT_MAX_SESSION_RESOURCES);
61
+ }
62
+ forAdapter(agentId) {
63
+ if (!validText(agentId, 64))
64
+ throw new TypeError('Agent resource registry requires an adapter id');
65
+ return Object.freeze({
66
+ register: (session, source) => (this.#register(agentId, session, source)),
67
+ revoke: (resourceId) => this.#revoke(agentId, resourceId),
68
+ });
69
+ }
70
+ async read(session, resourceId) {
71
+ if (!validSession(session) || !RESOURCE_ID_RE.test(resourceId))
72
+ return null;
73
+ this.clearExpired();
74
+ const record = this.#records.get(resourceId);
75
+ if (!record || record.agentId !== session.agentId || record.sessionId !== session.sessionId)
76
+ return null;
77
+ let data;
78
+ if (record.kind === 'bytes') {
79
+ data = new Uint8Array(record.data);
80
+ }
81
+ else {
82
+ try {
83
+ const resolved = await realpath(record.path);
84
+ if (resolved !== record.path || !record.roots.some((root) => within(root, resolved))) {
85
+ this.#delete(record);
86
+ throw new AgentResourceError('resource-changed', 'Agent resource path changed after registration');
87
+ }
88
+ const handle = await open(resolved, constants.O_RDONLY | constants.O_NOFOLLOW);
89
+ try {
90
+ const stat = await handle.stat();
91
+ if (!stat.isFile() || stat.size !== record.size || stat.size > this.#maxResourceBytes
92
+ || stat.dev !== record.dev || stat.ino !== record.ino || stat.mtimeMs !== record.mtimeMs) {
93
+ this.#delete(record);
94
+ throw new AgentResourceError('resource-changed', 'Agent resource changed after registration');
95
+ }
96
+ data = new Uint8Array(await handle.readFile());
97
+ }
98
+ finally {
99
+ await handle.close();
100
+ }
101
+ }
102
+ catch (error) {
103
+ if (error instanceof AgentResourceError)
104
+ throw error;
105
+ this.#delete(record);
106
+ throw new AgentResourceError('resource-changed', 'Agent resource is no longer readable');
107
+ }
108
+ }
109
+ return {
110
+ resourceId,
111
+ session: { agentId: record.agentId, sessionId: record.sessionId },
112
+ data,
113
+ size: record.size,
114
+ ...(record.name === undefined ? {} : { name: record.name }),
115
+ mediaType: record.mediaType,
116
+ expiresAt: record.expiresAt,
117
+ };
118
+ }
119
+ clearExpired() {
120
+ const now = this.#now();
121
+ let removed = 0;
122
+ for (const record of this.#records.values()) {
123
+ if (record.expiresAt > now)
124
+ continue;
125
+ this.#delete(record);
126
+ removed += 1;
127
+ }
128
+ return removed;
129
+ }
130
+ revokeAdapter(agentId) {
131
+ for (const record of this.#records.values()) {
132
+ if (record.agentId === agentId)
133
+ this.#delete(record);
134
+ }
135
+ }
136
+ async #register(agentId, session, source) {
137
+ if (!validSession(session) || session.agentId !== agentId || !source || typeof source !== 'object'
138
+ || !validName(source.name)) {
139
+ throw new AgentResourceError('invalid-source', 'Invalid Agent resource owner or metadata');
140
+ }
141
+ this.clearExpired();
142
+ const sessionCount = [...this.#records.values()].filter((record) => (record.agentId === agentId && record.sessionId === session.sessionId)).length;
143
+ if (this.#records.size >= this.#maxResources || sessionCount >= this.#maxSessionResources) {
144
+ throw new AgentResourceError('capacity-exceeded', 'Agent resource capacity is exhausted');
145
+ }
146
+ const common = {
147
+ resourceId: this.#allocateId(),
148
+ agentId,
149
+ sessionId: session.sessionId,
150
+ expiresAt: this.#now() + this.#ttlMs,
151
+ };
152
+ let record;
153
+ if (source.kind === 'bytes') {
154
+ if (!(source.data instanceof Uint8Array) || !validMediaType(source.mediaType)) {
155
+ throw new AgentResourceError('invalid-source', 'Invalid Agent byte resource');
156
+ }
157
+ const data = new Uint8Array(source.data);
158
+ if (data.byteLength > this.#maxResourceBytes) {
159
+ throw new AgentResourceError('resource-too-large', 'Agent resource exceeds the size limit');
160
+ }
161
+ if (this.#byteStorage + data.byteLength > this.#maxByteStorage) {
162
+ throw new AgentResourceError('capacity-exceeded', 'Agent byte resource storage is exhausted');
163
+ }
164
+ record = {
165
+ ...common,
166
+ kind: 'bytes',
167
+ data,
168
+ size: data.byteLength,
169
+ ...(source.name === undefined ? {} : { name: source.name }),
170
+ mediaType: source.mediaType,
171
+ };
172
+ }
173
+ else if (source.kind === 'file') {
174
+ if (!validText(source.path, 16 * 1024) || !isAbsolute(source.path)
175
+ || (source.mediaType !== undefined && !validMediaType(source.mediaType))) {
176
+ throw new AgentResourceError('invalid-source', 'Invalid Agent file resource');
177
+ }
178
+ const roots = await Promise.all((this.#allowedFileRoots[agentId] ?? []).map((root) => realpath(root)));
179
+ let resolved;
180
+ try {
181
+ resolved = await realpath(source.path);
182
+ }
183
+ catch {
184
+ throw new AgentResourceError('path-denied', 'Agent resource file is unavailable');
185
+ }
186
+ if (!roots.some((root) => within(root, resolved))) {
187
+ throw new AgentResourceError('path-denied', 'Agent resource file is outside allowed roots');
188
+ }
189
+ const handle = await open(resolved, constants.O_RDONLY | constants.O_NOFOLLOW);
190
+ let stat;
191
+ try {
192
+ stat = await handle.stat();
193
+ }
194
+ finally {
195
+ await handle.close();
196
+ }
197
+ if (!stat.isFile())
198
+ throw new AgentResourceError('invalid-source', 'Agent resource must be a regular file');
199
+ if (stat.size > this.#maxResourceBytes) {
200
+ throw new AgentResourceError('resource-too-large', 'Agent resource exceeds the size limit');
201
+ }
202
+ const basenameValue = basename(resolved).replace(/[\0-\x1f\x7f]/g, '_');
203
+ record = {
204
+ ...common,
205
+ kind: 'file',
206
+ path: resolved,
207
+ roots,
208
+ dev: stat.dev,
209
+ ino: stat.ino,
210
+ mtimeMs: stat.mtimeMs,
211
+ size: stat.size,
212
+ name: source.name ?? basenameValue,
213
+ mediaType: source.mediaType ?? 'application/octet-stream',
214
+ };
215
+ }
216
+ else {
217
+ throw new AgentResourceError('invalid-source', 'Unknown Agent resource source');
218
+ }
219
+ this.#records.set(record.resourceId, record);
220
+ if (record.kind === 'bytes')
221
+ this.#byteStorage += record.size;
222
+ return { resourceId: record.resourceId, expiresAt: record.expiresAt };
223
+ }
224
+ async #revoke(agentId, resourceId) {
225
+ if (!RESOURCE_ID_RE.test(resourceId))
226
+ return;
227
+ const record = this.#records.get(resourceId);
228
+ if (record?.agentId === agentId)
229
+ this.#delete(record);
230
+ }
231
+ #allocateId() {
232
+ for (let attempt = 0; attempt < 8; attempt += 1) {
233
+ const candidate = this.#newResourceId();
234
+ if (RESOURCE_ID_RE.test(candidate) && !this.#records.has(candidate))
235
+ return candidate;
236
+ }
237
+ throw new AgentResourceError('capacity-exceeded', 'Unable to allocate an Agent resource id');
238
+ }
239
+ #delete(record) {
240
+ if (!this.#records.delete(record.resourceId))
241
+ return;
242
+ if (record.kind === 'bytes')
243
+ this.#byteStorage -= record.size;
244
+ }
245
+ }
@@ -0,0 +1,328 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ export class AgentRunError extends Error {
3
+ code;
4
+ constructor(code, message) {
5
+ super(message);
6
+ this.code = code;
7
+ this.name = 'AgentRunError';
8
+ }
9
+ }
10
+ function attachmentKey(agentId, attachmentId) {
11
+ return `${agentId}\0${attachmentId}`;
12
+ }
13
+ function validText(value, max) {
14
+ return typeof value === 'string' && value.length > 0 && value.length <= max;
15
+ }
16
+ function validateCandidate(candidate) {
17
+ if (!candidate || !validText(candidate.paneId, 256) || !validText(candidate.attachmentId, 512)) {
18
+ throw new AgentRunError('invalid-candidate', 'Attachment requires a valid paneId and attachmentId');
19
+ }
20
+ if (candidate.sessionId !== undefined && !validText(candidate.sessionId, 1024)) {
21
+ throw new AgentRunError('invalid-candidate', 'Attachment sessionId must be a non-empty bounded string');
22
+ }
23
+ if (candidate.implementationVersion !== undefined
24
+ && (!Number.isSafeInteger(candidate.implementationVersion)
25
+ || candidate.implementationVersion <= 0)) {
26
+ throw new AgentRunError('invalid-candidate', 'Attachment implementationVersion must be a positive integer');
27
+ }
28
+ if (!candidate.process || !Number.isSafeInteger(candidate.process.pid) || candidate.process.pid <= 0) {
29
+ throw new AgentRunError('invalid-candidate', 'Attachment process requires a positive integer pid');
30
+ }
31
+ if (candidate.process.startedAt !== undefined
32
+ && (!Number.isFinite(candidate.process.startedAt) || candidate.process.startedAt < 0)) {
33
+ throw new AgentRunError('invalid-candidate', 'Attachment process startedAt must be a finite timestamp');
34
+ }
35
+ if (candidate.process.tty !== undefined && !validText(candidate.process.tty, 1024)) {
36
+ throw new AgentRunError('invalid-candidate', 'Attachment process tty must be a non-empty bounded string');
37
+ }
38
+ }
39
+ function sameProcess(first, second) {
40
+ return first.pid === second.pid
41
+ && first.startedAt === second.startedAt
42
+ && first.tty === second.tty;
43
+ }
44
+ function refFor(agentId, paneId, runId, sessionId, implementationVersion) {
45
+ return Object.freeze({
46
+ agentId,
47
+ paneId,
48
+ runId,
49
+ ...(sessionId === undefined ? {} : { sessionId }),
50
+ ...(implementationVersion === undefined ? {} : { implementationVersion }),
51
+ });
52
+ }
53
+ // Owns logical run generations only. Bridge connection nonces and transport reconnects never enter here.
54
+ export class AgentRunRuntime {
55
+ #byRunId = new Map();
56
+ #knownRunRefs = new Map();
57
+ #byPane = new Map();
58
+ #byAttachment = new Map();
59
+ #records = new WeakMap();
60
+ #paneQueues = new Map();
61
+ #disabledAdapters = new Set();
62
+ #newRunId;
63
+ #verifyTimeoutMs;
64
+ #shuttingDown = false;
65
+ constructor({ newRunId = randomUUID, verifyTimeoutMs = 1000, } = {}) {
66
+ this.#newRunId = newRunId;
67
+ this.#verifyTimeoutMs = Math.max(1, verifyTimeoutMs);
68
+ }
69
+ resolve(ref) {
70
+ if (!ref || typeof ref.runId !== 'string')
71
+ return null;
72
+ const record = this.#byRunId.get(ref.runId);
73
+ if (!record || record.abort.signal.aborted)
74
+ return null;
75
+ if (record.ref.agentId !== ref.agentId || record.ref.paneId !== ref.paneId)
76
+ return null;
77
+ if (ref.sessionId !== undefined && record.ref.sessionId !== ref.sessionId)
78
+ return null;
79
+ return record.lease;
80
+ }
81
+ status(ref) {
82
+ if (this.resolve(ref))
83
+ return 'current';
84
+ const known = ref && typeof ref.runId === 'string' ? this.#knownRunRefs.get(ref.runId) : undefined;
85
+ if (!known || known.agentId !== ref.agentId || known.paneId !== ref.paneId)
86
+ return 'unknown';
87
+ if (ref.sessionId !== undefined && known.sessionId !== ref.sessionId)
88
+ return 'unknown';
89
+ return 'revoked';
90
+ }
91
+ isLive(lease) {
92
+ const record = this.#records.get(lease);
93
+ return record !== undefined && this.#byRunId.get(record.ref.runId) === record
94
+ && !record.abort.signal.aborted;
95
+ }
96
+ requireLive(lease) {
97
+ if (!this.isLive(lease))
98
+ throw new AgentRunError('stale-lease', 'Agent run lease is no longer live');
99
+ return lease.ref;
100
+ }
101
+ currentForPane(paneId) {
102
+ const record = this.#byPane.get(paneId);
103
+ return record && !record.abort.signal.aborted ? record.lease : null;
104
+ }
105
+ controller(agentId, verify) {
106
+ if (!validText(agentId, 64) || typeof verify !== 'function') {
107
+ throw new TypeError('Scoped Agent run controller requires an agentId and attachment verifier');
108
+ }
109
+ return Object.freeze({
110
+ attach: (candidate) => this.#attach(agentId, verify, candidate),
111
+ associateSession: (lease, sessionId) => (this.#associateSession(agentId, lease, sessionId)),
112
+ replace: (current, candidate, reason) => this.#replace(agentId, verify, current, candidate, reason),
113
+ revoke: (lease, reason) => this.#revoke(agentId, lease, reason),
114
+ });
115
+ }
116
+ async revokeAdapter(agentId, reason = 'adapter_stopped') {
117
+ this.#disabledAdapters.add(agentId);
118
+ const records = [...this.#byRunId.values()].filter((record) => record.agentId === agentId);
119
+ await Promise.all(records.map((record) => this.#withPane(record.paneId, async () => {
120
+ if (this.#byRunId.get(record.ref.runId) === record)
121
+ this.#remove(record, reason);
122
+ })));
123
+ }
124
+ // Root Runtime uses this after process identity changes. Scoped adapter controllers deliberately do not
125
+ // receive it, so one adapter cannot evict another adapter's run by naming its pane.
126
+ async revokePane(paneId, reason) {
127
+ await this.#withPane(paneId, async () => {
128
+ const record = this.#byPane.get(paneId);
129
+ if (record)
130
+ this.#remove(record, reason);
131
+ });
132
+ }
133
+ async shutdown() {
134
+ if (this.#shuttingDown)
135
+ return;
136
+ this.#shuttingDown = true;
137
+ const records = [...this.#byRunId.values()];
138
+ await Promise.all(records.map((record) => this.#withPane(record.paneId, async () => {
139
+ if (this.#byRunId.get(record.ref.runId) === record)
140
+ this.#remove(record, 'runtime_shutdown');
141
+ })));
142
+ }
143
+ async #withPane(paneId, operation) {
144
+ const previous = this.#paneQueues.get(paneId) ?? Promise.resolve();
145
+ let release;
146
+ const current = new Promise((resolve) => { release = resolve; });
147
+ const queued = previous.catch(() => { }).then(() => current);
148
+ this.#paneQueues.set(paneId, queued);
149
+ await previous.catch(() => { });
150
+ try {
151
+ return await operation();
152
+ }
153
+ finally {
154
+ release();
155
+ if (this.#paneQueues.get(paneId) === queued)
156
+ this.#paneQueues.delete(paneId);
157
+ }
158
+ }
159
+ async #verify(verify, candidate) {
160
+ let accepted = false;
161
+ let timer;
162
+ try {
163
+ accepted = await Promise.race([
164
+ Promise.resolve(verify(candidate)).then(Boolean, () => false),
165
+ new Promise((resolve) => {
166
+ timer = setTimeout(() => resolve(false), this.#verifyTimeoutMs);
167
+ }),
168
+ ]);
169
+ }
170
+ catch { /* adapter probe failure is a rejection */ }
171
+ finally {
172
+ if (timer)
173
+ clearTimeout(timer);
174
+ }
175
+ if (!accepted) {
176
+ throw new AgentRunError('attachment-unverified', 'Agent attachment no longer matches the live process');
177
+ }
178
+ }
179
+ #recordFor(agentId, lease) {
180
+ const record = this.#records.get(lease);
181
+ if (!record || record.agentId !== agentId) {
182
+ throw new AgentRunError('foreign-lease', 'Agent run lease does not belong to this scoped controller');
183
+ }
184
+ return record;
185
+ }
186
+ #create(agentId, candidate) {
187
+ const abort = new AbortController();
188
+ const record = {};
189
+ record.agentId = agentId;
190
+ record.paneId = candidate.paneId;
191
+ record.attachmentId = candidate.attachmentId;
192
+ record.process = Object.freeze({ ...candidate.process });
193
+ let runId = '';
194
+ for (let attempt = 0; attempt < 8; attempt += 1) {
195
+ const proposed = this.#newRunId();
196
+ if (validText(proposed, 256) && !this.#byRunId.has(proposed)) {
197
+ runId = proposed;
198
+ break;
199
+ }
200
+ }
201
+ if (!runId)
202
+ throw new Error('Unable to allocate a unique Agent runId');
203
+ record.ref = refFor(agentId, candidate.paneId, runId, candidate.sessionId, candidate.implementationVersion);
204
+ this.#knownRunRefs.set(runId, record.ref);
205
+ record.abort = abort;
206
+ record.lease = Object.freeze({
207
+ get ref() { return record.ref; },
208
+ signal: abort.signal,
209
+ });
210
+ this.#records.set(record.lease, record);
211
+ return record;
212
+ }
213
+ #install(record) {
214
+ this.#byRunId.set(record.ref.runId, record);
215
+ this.#byPane.set(record.paneId, record);
216
+ this.#byAttachment.set(attachmentKey(record.agentId, record.attachmentId), record);
217
+ }
218
+ #remove(record, reason) {
219
+ if (this.#byRunId.get(record.ref.runId) !== record)
220
+ return;
221
+ this.#byRunId.delete(record.ref.runId);
222
+ if (this.#byPane.get(record.paneId) === record)
223
+ this.#byPane.delete(record.paneId);
224
+ const key = attachmentKey(record.agentId, record.attachmentId);
225
+ if (this.#byAttachment.get(key) === record)
226
+ this.#byAttachment.delete(key);
227
+ record.abort.abort(reason);
228
+ }
229
+ #replaceInstalled(current, next, reason) {
230
+ this.#byRunId.delete(current.ref.runId);
231
+ if (this.#byPane.get(current.paneId) === current)
232
+ this.#byPane.delete(current.paneId);
233
+ const currentKey = attachmentKey(current.agentId, current.attachmentId);
234
+ if (this.#byAttachment.get(currentKey) === current)
235
+ this.#byAttachment.delete(currentKey);
236
+ this.#install(next);
237
+ current.abort.abort(reason);
238
+ return next.lease;
239
+ }
240
+ async #attach(agentId, verify, candidate) {
241
+ validateCandidate(candidate);
242
+ return this.#withPane(candidate.paneId, async () => {
243
+ if (this.#shuttingDown || this.#disabledAdapters.has(agentId)) {
244
+ throw new AgentRunError('runtime-unavailable', 'Agent runtime or adapter is stopped');
245
+ }
246
+ await this.#verify(verify, candidate);
247
+ const paneOwner = this.#byPane.get(candidate.paneId);
248
+ if (paneOwner && paneOwner.agentId !== agentId) {
249
+ throw new AgentRunError('pane-owned-by-another-adapter', `Pane ${candidate.paneId} is owned by another Agent adapter`);
250
+ }
251
+ const attached = this.#byAttachment.get(attachmentKey(agentId, candidate.attachmentId));
252
+ if (attached && attached.paneId !== candidate.paneId) {
253
+ throw new AgentRunError('attachment-already-attached', 'Agent attachment is already live on another pane');
254
+ }
255
+ if (!paneOwner) {
256
+ const created = this.#create(agentId, candidate);
257
+ this.#install(created);
258
+ return created.lease;
259
+ }
260
+ if (paneOwner.attachmentId === candidate.attachmentId
261
+ && sameProcess(paneOwner.process, candidate.process)) {
262
+ const currentSession = paneOwner.ref.sessionId;
263
+ if (currentSession !== undefined && candidate.sessionId !== undefined
264
+ && currentSession !== candidate.sessionId) {
265
+ throw new AgentRunError('session-replacement-required', 'A different session requires replace(), even when process and pane are unchanged');
266
+ }
267
+ if (currentSession === undefined && candidate.sessionId !== undefined) {
268
+ paneOwner.ref = refFor(agentId, candidate.paneId, paneOwner.ref.runId, candidate.sessionId);
269
+ this.#knownRunRefs.set(paneOwner.ref.runId, paneOwner.ref);
270
+ }
271
+ return paneOwner.lease;
272
+ }
273
+ const created = this.#create(agentId, candidate);
274
+ return this.#replaceInstalled(paneOwner, created, 'process_exit');
275
+ });
276
+ }
277
+ async #associateSession(agentId, lease, sessionId) {
278
+ if (!validText(sessionId, 1024)) {
279
+ throw new AgentRunError('invalid-candidate', 'sessionId must be a non-empty bounded string');
280
+ }
281
+ const known = this.#recordFor(agentId, lease);
282
+ return this.#withPane(known.paneId, async () => {
283
+ const record = this.#recordFor(agentId, lease);
284
+ if (this.#byRunId.get(record.ref.runId) !== record || record.abort.signal.aborted) {
285
+ throw new AgentRunError('stale-lease', 'Cannot associate a session with a stale Agent run');
286
+ }
287
+ if (record.ref.sessionId === sessionId)
288
+ return record.ref;
289
+ if (record.ref.sessionId !== undefined) {
290
+ throw new AgentRunError('session-replacement-required', 'An associated session is immutable; use replace() for a new session');
291
+ }
292
+ record.ref = refFor(agentId, record.paneId, record.ref.runId, sessionId);
293
+ this.#knownRunRefs.set(record.ref.runId, record.ref);
294
+ return record.ref;
295
+ });
296
+ }
297
+ async #replace(agentId, verify, current, candidate, reason) {
298
+ validateCandidate(candidate);
299
+ const known = this.#recordFor(agentId, current);
300
+ if (candidate.paneId !== known.paneId) {
301
+ throw new AgentRunError('pane-change-requires-attach', 'replace() cannot move an Agent attachment to another pane');
302
+ }
303
+ return this.#withPane(known.paneId, async () => {
304
+ const record = this.#recordFor(agentId, current);
305
+ if (this.#byRunId.get(record.ref.runId) !== record || record.abort.signal.aborted) {
306
+ throw new AgentRunError('stale-lease', 'Cannot replace a stale Agent run');
307
+ }
308
+ if (this.#shuttingDown || this.#disabledAdapters.has(agentId)) {
309
+ throw new AgentRunError('runtime-unavailable', 'Agent runtime or adapter is stopped');
310
+ }
311
+ await this.#verify(verify, candidate);
312
+ const attached = this.#byAttachment.get(attachmentKey(agentId, candidate.attachmentId));
313
+ if (attached && attached !== record) {
314
+ throw new AgentRunError('attachment-already-attached', 'Replacement attachment is already live');
315
+ }
316
+ const created = this.#create(agentId, candidate);
317
+ return this.#replaceInstalled(record, created, reason);
318
+ });
319
+ }
320
+ async #revoke(agentId, lease, reason) {
321
+ const known = this.#recordFor(agentId, lease);
322
+ await this.#withPane(known.paneId, async () => {
323
+ const record = this.#recordFor(agentId, lease);
324
+ if (this.#byRunId.get(record.ref.runId) === record)
325
+ this.#remove(record, reason);
326
+ });
327
+ }
328
+ }