agentbox-app 0.1.13 → 0.1.16

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 (278) hide show
  1. package/README.md +24 -9
  2. package/bin/agentbox.mjs +23 -1
  3. package/briefs/message-rules.md +36 -8
  4. package/briefs/worker.md +46 -23
  5. package/main/account-discovery.mjs +122 -30
  6. package/main/account-tooling.mjs +29 -1
  7. package/main/account-usage.mjs +46 -0
  8. package/main/agent-sessions.mjs +282 -268
  9. package/main/agents.mjs +179 -466
  10. package/main/analytics.mjs +12 -2
  11. package/main/answer-modes.mjs +103 -37
  12. package/main/approval-prompt-core.mjs +197 -0
  13. package/main/approval-prompt-server.mjs +4 -138
  14. package/main/approvals.mjs +101 -297
  15. package/main/artifact-path.mjs +47 -18
  16. package/main/claude-input.mjs +48 -4
  17. package/main/claude-plan.mjs +48 -0
  18. package/main/claude-session-open.mjs +28 -0
  19. package/main/claude-usage-process.mjs +40 -0
  20. package/main/claude-usage.mjs +18 -40
  21. package/main/cloud/account.mjs +179 -0
  22. package/main/cloud/aws.mjs +127 -0
  23. package/main/cloud/browser-return.mjs +58 -0
  24. package/main/cloud/clients.mjs +15 -0
  25. package/main/cloud/codex-cloud.mjs +53 -0
  26. package/main/cloud/config.mjs +107 -0
  27. package/main/cloud/docs.mjs +219 -0
  28. package/main/cloud/exec.mjs +27 -0
  29. package/main/cloud/host.mjs +165 -0
  30. package/main/cloud/kit.mjs +80 -0
  31. package/main/cloud/logins.mjs +543 -0
  32. package/main/cloud/managed.mjs +115 -0
  33. package/main/cloud/marker.mjs +104 -0
  34. package/main/cloud/org.mjs +53 -0
  35. package/main/cloud/reuse.mjs +208 -0
  36. package/main/cloud/run-cli.mjs +177 -0
  37. package/main/cloud/run.mjs +1934 -0
  38. package/main/cloud/usage.mjs +76 -0
  39. package/main/cloud-permission.mjs +51 -0
  40. package/main/codex-account.mjs +78 -0
  41. package/main/codex-app-server.mjs +276 -409
  42. package/main/codex-approvals.mjs +148 -433
  43. package/main/codex-models.mjs +20 -2
  44. package/main/codex-session.mjs +212 -507
  45. package/main/codex-threads-worker.mjs +12 -0
  46. package/main/codex-threads.mjs +71 -3
  47. package/main/codex-usage-file.mjs +5 -3
  48. package/main/codex-usage.mjs +56 -7
  49. package/main/codex-watch.mjs +17 -3
  50. package/main/codex.mjs +95 -363
  51. package/main/config.mjs +11 -1
  52. package/main/corner-tag.mjs +340 -19
  53. package/main/dev-window.mjs +1 -1
  54. package/main/engine-bin.mjs +232 -282
  55. package/main/engine-setup.mjs +21 -9
  56. package/main/feedback.mjs +3 -25
  57. package/main/folder-pictures.mjs +2 -2
  58. package/main/freeze-recorder-thread.mjs +86 -0
  59. package/main/freeze-recorder.mjs +146 -0
  60. package/main/fresh-user.mjs +20 -9
  61. package/main/git-change-offthread.mjs +17 -3
  62. package/main/git-change-thread.mjs +5 -2
  63. package/main/harnesses/claude.mjs +32 -11
  64. package/main/harnesses/codex.mjs +9 -2
  65. package/main/importable-threads.mjs +64 -6
  66. package/main/ipc.mjs +362 -111
  67. package/main/leftovers.mjs +117 -25
  68. package/main/liquid-glass.mjs +26 -0
  69. package/main/live-replies.mjs +10 -4
  70. package/main/main-went-red.mjs +330 -0
  71. package/main/main.mjs +199 -84
  72. package/main/memory-gate-history.mjs +36 -10
  73. package/main/memory-gate-server.mjs +62 -0
  74. package/main/memory-gate.mjs +14 -1
  75. package/main/menu.mjs +4 -2
  76. package/main/message-priority.mjs +2 -2
  77. package/main/mic.mjs +38 -0
  78. package/main/native/liquid-glass/build/liquid-glass.node +0 -0
  79. package/main/new-chats-watch.mjs +110 -0
  80. package/main/new-user-window.mjs +25 -0
  81. package/main/notify.mjs +24 -8
  82. package/main/phone-alerts.mjs +117 -0
  83. package/main/phone-inbox.mjs +625 -0
  84. package/main/phone-link.mjs +29 -9
  85. package/main/phone-uploads.mjs +476 -0
  86. package/main/picture-shape.mjs +114 -0
  87. package/main/quit-sequence.mjs +143 -0
  88. package/main/refresh-agent-models.mjs +15 -2
  89. package/main/relay-client.mjs +69 -1
  90. package/main/repeats.mjs +10 -4
  91. package/main/row-label.mjs +12 -41
  92. package/main/run-change.mjs +13 -0
  93. package/main/run-ledger.mjs +135 -0
  94. package/main/save-attachments.mjs +41 -0
  95. package/main/serve.mjs +45 -3
  96. package/main/settings.mjs +220 -44
  97. package/main/shell-path.mjs +185 -37
  98. package/main/ship-conflict-brief.mjs +119 -0
  99. package/main/ship-queue.mjs +115 -19
  100. package/main/source-updater.mjs +18 -10
  101. package/main/stage-attachment.mjs +68 -0
  102. package/main/stall-watch.mjs +50 -0
  103. package/main/store/home.mjs +87 -232
  104. package/main/store/paths.mjs +11 -0
  105. package/main/store/teammates-files.mjs +99 -0
  106. package/main/store/work-items.mjs +231 -13
  107. package/main/store.mjs +226 -52
  108. package/main/supervisor.mjs +3103 -341
  109. package/main/task-folders.mjs +430 -30
  110. package/main/task-terminals.mjs +110 -34
  111. package/main/team/index.mjs +114 -10
  112. package/main/team/memory-cloud.mjs +183 -1
  113. package/main/team/room-hours.mjs +48 -0
  114. package/main/team/room-setup.mjs +31 -0
  115. package/main/team/rooms-memory.mjs +454 -0
  116. package/main/team/rooms-shape.mjs +42 -0
  117. package/main/team/rooms-supabase.mjs +324 -0
  118. package/main/team/rooms.mjs +575 -0
  119. package/main/team/session.mjs +96 -26
  120. package/main/team/supabase-backend.mjs +180 -0
  121. package/main/team/sync.mjs +109 -1
  122. package/main/teammates.mjs +580 -0
  123. package/main/terminal-host.mjs +143 -0
  124. package/main/terminal-hosts.mjs +261 -0
  125. package/main/transcript-search.mjs +80 -0
  126. package/main/webview-guard.mjs +45 -0
  127. package/main/worktree-include.mjs +90 -219
  128. package/mcp/core/documents.mjs +3 -1
  129. package/mcp/core/repeats.mjs +88 -0
  130. package/mcp/core/teammates.mjs +222 -0
  131. package/mcp/core/work.mjs +76 -4
  132. package/mcp/stdio.mjs +24 -11
  133. package/mcp/tools.mjs +116 -8
  134. package/package.json +21 -5
  135. package/renderer/dist/assets/{abnfDiagram-YUKMZFIV-DPPuMWt-.js → abnfDiagram-YUKMZFIV-CoWB6YUK.js} +1 -1
  136. package/renderer/dist/assets/{arc-HmFfdseH.js → arc-Ch0NrCul.js} +1 -1
  137. package/renderer/dist/assets/{architectureDiagram-47P4ROYG-DxJ3zHrH.js → architectureDiagram-47P4ROYG-CXz9M5tQ.js} +1 -1
  138. package/renderer/dist/assets/{blockDiagram-E7TT5TSB-CbTXzOqQ.js → blockDiagram-E7TT5TSB-DchGUuHH.js} +1 -1
  139. package/renderer/dist/assets/{c4Diagram-CRA5TL53-X5ItFyLn.js → c4Diagram-CRA5TL53-Cip-zNyr.js} +1 -1
  140. package/renderer/dist/assets/channel-BQuQSL--.js +1 -0
  141. package/renderer/dist/assets/{chunk-2Q5K7J3B-DC12Ho_L.js → chunk-2Q5K7J3B-BoXid_2r.js} +1 -1
  142. package/renderer/dist/assets/{chunk-5VM5RSS4-ClVtfPd0.js → chunk-5VM5RSS4-Cpez1rRz.js} +1 -1
  143. package/renderer/dist/assets/{chunk-7TKQ45FW-DgcOm0HT.js → chunk-7TKQ45FW-DKA84YkE.js} +1 -1
  144. package/renderer/dist/assets/{chunk-AW2ZBBNX-C01s4yoF.js → chunk-AW2ZBBNX-Dg-SumPG.js} +1 -1
  145. package/renderer/dist/assets/{chunk-FVRAUYC3-Bj0zxhR4.js → chunk-FVRAUYC3-06TCvPm1.js} +2 -2
  146. package/renderer/dist/assets/{chunk-HTAEGDNF-DWDeEhLU.js → chunk-HTAEGDNF-CMEBy1Td.js} +1 -1
  147. package/renderer/dist/assets/{chunk-JWPE2WC7-Dsqwv3eW.js → chunk-JWPE2WC7-BJ2ObmV-.js} +1 -1
  148. package/renderer/dist/assets/{chunk-KQW6MTUR-CE86ANL2.js → chunk-KQW6MTUR-CGWOYXUE.js} +1 -1
  149. package/renderer/dist/assets/{chunk-XXDRQBXY-CoNTASfs.js → chunk-XXDRQBXY-CiZp07XC.js} +1 -1
  150. package/renderer/dist/assets/{classDiagram-v2-K4WV4PDN-2wprLqtw.js → classDiagram-v2-K4WV4PDN-DJNIX719.js} +1 -1
  151. package/renderer/dist/assets/cornerTag-CPgvu-q3.js +1 -0
  152. package/renderer/dist/assets/cornerTag-oMfEjzCO.css +1 -0
  153. package/renderer/dist/assets/{cose-bilkent-JH36ORCC-enqMiYbn.js → cose-bilkent-JH36ORCC-CbkyZOUH.js} +1 -1
  154. package/renderer/dist/assets/{cynefin-EF2NZ3EQ-DHngLxvQ.js → cynefin-EF2NZ3EQ-78iOroZO.js} +1 -1
  155. package/renderer/dist/assets/{cynefinDiagram-3GCD6N5R-C0rZZIl3.js → cynefinDiagram-3GCD6N5R-GcCopMs7.js} +1 -1
  156. package/renderer/dist/assets/{dagre-W4DXFKR2-DQnaEWO7.js → dagre-W4DXFKR2-DHLlXLkz.js} +1 -1
  157. package/renderer/dist/assets/{diagram-2UJZ2QOL-CS8AlGPx.js → diagram-2UJZ2QOL-DuZSUHcH.js} +1 -1
  158. package/renderer/dist/assets/{diagram-OVF4WLC6-yiDMHIkY.js → diagram-OVF4WLC6-Ckxu3cqH.js} +1 -1
  159. package/renderer/dist/assets/{diagram-PFPMY2P6-XbQKzbBA.js → diagram-PFPMY2P6-CsTw9Ycn.js} +1 -1
  160. package/renderer/dist/assets/{diagram-UMYRVEAY-dhVvlfl_.js → diagram-UMYRVEAY-CTXd8qVg.js} +1 -1
  161. package/renderer/dist/assets/{diagram-YEKJPTXX-DZHe8SCf.js → diagram-YEKJPTXX-CJs1T_9m.js} +1 -1
  162. package/renderer/dist/assets/{diagram-ZIFT7M5P-B5h6aXVR.js → diagram-ZIFT7M5P-CvjI23mf.js} +1 -1
  163. package/renderer/dist/assets/{ebnfDiagram-VR2GEFS7-PR9ScDco.js → ebnfDiagram-VR2GEFS7-d_IeAzOJ.js} +1 -1
  164. package/renderer/dist/assets/{elk-IJKZMXRS-Cg-yQXPd.js → elk-IJKZMXRS-zo9KFP3N.js} +1 -1
  165. package/renderer/dist/assets/{erDiagram-O2IAPWRE-omOrJMxu.js → erDiagram-O2IAPWRE-DqfYf5Dt.js} +1 -1
  166. package/renderer/dist/assets/{flowDiagram-OXPTDLAJ-aOX2hzvT.js → flowDiagram-OXPTDLAJ-DAWsMLmo.js} +1 -1
  167. package/renderer/dist/assets/{ganttDiagram-R7TSEDQI-B1H3pWyI.js → ganttDiagram-R7TSEDQI-4ml_bhwR.js} +1 -1
  168. package/renderer/dist/assets/{gitGraphDiagram-XJZIOB7I-Cvw304HC.js → gitGraphDiagram-XJZIOB7I-B_WcqSId.js} +1 -1
  169. package/renderer/dist/assets/{infoDiagram-5W2HQ5XZ-_ZXcNViH.js → infoDiagram-5W2HQ5XZ-CI-jYqzc.js} +1 -1
  170. package/renderer/dist/assets/{ishikawaDiagram-K3B6WC7H-C7Mx_qQk.js → ishikawaDiagram-K3B6WC7H-B56VWRlV.js} +1 -1
  171. package/renderer/dist/assets/{journeyDiagram-COXZFDF6-fr62QoAt.js → journeyDiagram-COXZFDF6-D1-p09Lr.js} +1 -1
  172. package/renderer/dist/assets/{kanban-definition-P3RFRI5V-J8zsabCe.js → kanban-definition-P3RFRI5V-Cv3w7JoN.js} +1 -1
  173. package/renderer/dist/assets/{linear-BmNdEtIi.js → linear-C35V2Spi.js} +1 -1
  174. package/renderer/dist/assets/main-BcoBiGwv.css +32 -0
  175. package/renderer/dist/assets/main-CLnIOulD.js +1191 -0
  176. package/renderer/dist/assets/{mermaid.core-CnYzvFkz.js → mermaid.core-BzGT5Vo9.js} +4 -4
  177. package/renderer/dist/assets/{mindmap-definition-LZFPQGKD-Bfcxc7PM.js → mindmap-definition-LZFPQGKD-BZUfuymC.js} +1 -1
  178. package/renderer/dist/assets/{pegDiagram-BGZESJAR-CwUJOq6X.js → pegDiagram-BGZESJAR-DFh9XNGe.js} +1 -1
  179. package/renderer/dist/assets/{pieDiagram-CAPJLFHJ-BrwMmqfC.js → pieDiagram-CAPJLFHJ-C65tPVsy.js} +1 -1
  180. package/renderer/dist/assets/{product-name-CTKLYNWY.js → product-name-CSNW6d7s.js} +1 -1
  181. package/renderer/dist/assets/{quadrantDiagram-GDMTTRAM-BN4Stoj-.js → quadrantDiagram-GDMTTRAM-Cushj_nk.js} +1 -1
  182. package/renderer/dist/assets/{railroadDiagram-SM67HX2B-CPZ5bbeR.js → railroadDiagram-SM67HX2B-DZGebGx5.js} +1 -1
  183. package/renderer/dist/assets/{requirementDiagram-X7JNWC4A-CgRnimVe.js → requirementDiagram-X7JNWC4A-DpAB0MNo.js} +1 -1
  184. package/renderer/dist/assets/{sankeyDiagram-UM26HJRW-Bt2rej__.js → sankeyDiagram-UM26HJRW-D-29rHra.js} +1 -1
  185. package/renderer/dist/assets/{sequenceDiagram-ZO4K6R2Y-Dd5nKe9G.js → sequenceDiagram-ZO4K6R2Y-FfOQAZol.js} +1 -1
  186. package/renderer/dist/assets/{sizeCapture-INFHLROL-usyNyXyh.js → sizeCapture-INFHLROL-C_n6fDBY.js} +1 -1
  187. package/renderer/dist/assets/{stateDiagram-v2-TBUQTH76-CRzUBb0u.js → stateDiagram-v2-TBUQTH76-BZjenzJG.js} +1 -1
  188. package/renderer/dist/assets/{swimlanes-N4OXWK64-C1GJvMS0.js → swimlanes-N4OXWK64-DxaMU9vX.js} +1 -1
  189. package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cb2kM6rr.js +8 -0
  190. package/renderer/dist/assets/{timeline-definition-YOQAKGHF-caeyPlcc.js → timeline-definition-YOQAKGHF-DTg5HJKz.js} +1 -1
  191. package/renderer/dist/assets/{usecaseDiagram-VIAY4XPW-BN_SV6PC.js → usecaseDiagram-VIAY4XPW-Bc3MZDsl.js} +1 -1
  192. package/renderer/dist/assets/{vennDiagram-BLWOH2XV-BLGuaUX-.js → vennDiagram-BLWOH2XV-GIVGk9Id.js} +1 -1
  193. package/renderer/dist/assets/{wardleyDiagram-YMQ3BMBF-Da0cJeNc.js → wardleyDiagram-YMQ3BMBF-NmJnbQOg.js} +1 -1
  194. package/renderer/dist/assets/{xychartDiagram-TAQBALBS-cGfUffMk.js → xychartDiagram-TAQBALBS-p-kfGfnW.js} +1 -1
  195. package/renderer/dist/corner-tag.html +3 -3
  196. package/renderer/dist/index.html +3 -3
  197. package/scripts/after-install.mjs +30 -8
  198. package/scripts/cloud-shim.pl +57 -0
  199. package/shared/account-names.d.mts +6 -0
  200. package/shared/account-names.mjs +15 -0
  201. package/shared/agent-hours.mjs +136 -0
  202. package/shared/agent-import.mjs +60 -34
  203. package/shared/agents.mjs +126 -522
  204. package/shared/analytics-events.mjs +12 -0
  205. package/shared/approval-card.mjs +36 -0
  206. package/shared/bridge-map.mjs +27 -1
  207. package/shared/claude-code-shape.mjs +6 -0
  208. package/shared/claude-usage.d.mts +1 -0
  209. package/shared/claude-usage.mjs +42 -1
  210. package/shared/cloud-hand-back.d.mts +7 -0
  211. package/shared/cloud-hand-back.mjs +20 -0
  212. package/shared/cloud-kit.d.mts +219 -0
  213. package/shared/cloud-kit.mjs +341 -0
  214. package/shared/cloud-login-name.mjs +4 -0
  215. package/shared/cloud-start.mjs +17 -0
  216. package/shared/cloud-versions.mjs +23 -0
  217. package/shared/codex-import.mjs +2 -2
  218. package/shared/codex-modes.d.mts +22 -9
  219. package/shared/codex-modes.mjs +67 -11
  220. package/shared/codex-usage.mjs +108 -2
  221. package/shared/corner-tag.d.mts +23 -1
  222. package/shared/corner-tag.mjs +284 -29
  223. package/shared/dead-run-trace.mjs +19 -2
  224. package/shared/engines.mjs +70 -309
  225. package/shared/feedback.d.mts +7 -0
  226. package/shared/feedback.mjs +25 -0
  227. package/shared/first-run-practice.mjs +17 -24
  228. package/shared/harness-definitions.mjs +8 -0
  229. package/shared/instruction-defaults.json +2 -2
  230. package/shared/key-caps.d.mts +14 -0
  231. package/shared/key-caps.mjs +68 -0
  232. package/shared/message-edits.d.mts +7 -0
  233. package/shared/message-edits.mjs +35 -0
  234. package/shared/move-thread.d.mts +24 -0
  235. package/shared/move-thread.mjs +112 -0
  236. package/shared/named-threads.mjs +72 -0
  237. package/shared/not-shipped.d.mts +22 -0
  238. package/shared/not-shipped.mjs +89 -0
  239. package/shared/notify-rules.d.mts +6 -0
  240. package/shared/notify-rules.mjs +36 -8
  241. package/shared/phone-files.mjs +28 -0
  242. package/shared/phone-settings.mjs +118 -0
  243. package/shared/phone-snapshot.d.mts +16 -0
  244. package/shared/phone-snapshot.mjs +348 -0
  245. package/shared/picture-formats.d.mts +1 -0
  246. package/shared/picture-formats.mjs +13 -0
  247. package/shared/rank.d.mts +2 -1
  248. package/shared/rank.mjs +32 -19
  249. package/shared/repeats.d.mts +8 -0
  250. package/shared/repeats.mjs +33 -2
  251. package/shared/room-cards.mjs +167 -0
  252. package/shared/room-links.d.mts +4 -0
  253. package/shared/room-links.mjs +21 -0
  254. package/shared/runs-on.d.mts +3 -3
  255. package/shared/runs-on.mjs +8 -5
  256. package/shared/shipped-note.d.mts +4 -0
  257. package/shared/shipped-note.mjs +6 -0
  258. package/shared/shortcuts.mjs +14 -6
  259. package/shared/spawn-trouble.d.mts +4 -0
  260. package/shared/spawn-trouble.mjs +443 -40
  261. package/shared/team-rules.mjs +9 -2
  262. package/shared/teammates.d.mts +19 -0
  263. package/shared/teammates.mjs +756 -0
  264. package/shared/terminal-focus.d.mts +1 -0
  265. package/shared/terminal-focus.mjs +13 -0
  266. package/shared/thread-cards.mjs +4 -1
  267. package/shared/usage.d.mts +9 -0
  268. package/shared/usage.mjs +49 -3
  269. package/shared/websites.d.mts +15 -0
  270. package/shared/websites.mjs +61 -0
  271. package/shared/work-items.mjs +127 -30
  272. package/shared/work-lines.mjs +92 -244
  273. package/renderer/dist/assets/channel-BlT2g9Sz.js +0 -1
  274. package/renderer/dist/assets/cornerTag-BLrxiDE5.css +0 -1
  275. package/renderer/dist/assets/cornerTag-BOT6u3Tn.js +0 -1
  276. package/renderer/dist/assets/main-ChNdnUvt.js +0 -1132
  277. package/renderer/dist/assets/main-nj0QR5kg.css +0 -32
  278. package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cy4Av2_p.js +0 -8
@@ -0,0 +1,543 @@
1
+ // THE CLOUD'S ACCOUNTS, AS THE MAC KNOWS THEM: BY NAME, NEVER BY CONTENT.
2
+ //
3
+ // A cloud account is one a person ADDED for the cloud, signed in through the
4
+ // vendor's own page, and it need not be any account this Mac uses (2026-10-14:
5
+ // "we shouldn't predefine which accounts to sign up with ... maybe I want to
6
+ // sign in with my work account"). Each is filed under a key of its own
7
+ // (`acct-…`), and a cloud run of an engine takes whichever of that engine's
8
+ // accounts was used least recently (`pickCloudLogin`).
9
+ //
10
+ // docs/cloud/PLAN.md, "Sign-ins live in the cloud". A person signs in to
11
+ // Claude or Codex inside a cloud sandbox, through the vendor's own sign-in,
12
+ // and the login is kept in the cloud bucket as `logins/<engine>/<id>.json`.
13
+ // The Mac keeps only a note per account in ~/.agentbox/cloud-logins.json:
14
+ // which object is current, when it was saved, when a Codex one runs out, and
15
+ // whether it stopped working. Nothing here ever reads a login object; it only
16
+ // asks for links to one, which the sandbox uses.
17
+ //
18
+ // Three things the Mac does with that note:
19
+ // - decides whether an account's threads may go to the cloud
20
+ // (`cloudLoginState`);
21
+ // - runs the sign-in from the cloud menu (`startCloudSignIn`), which is a
22
+ // sandbox session the Mac only relays a link and a code to;
23
+ // - renews a Codex login before it runs out (`renewCloudCodexLogin`), one
24
+ // at a time per account, fenced across an app restart.
25
+ import fs from 'node:fs';
26
+ import os from 'node:os';
27
+ import path from 'node:path';
28
+ import crypto from 'node:crypto';
29
+ import { wayBack, listenForWayBack } from './browser-return.mjs';
30
+
31
+ // A Codex access token lasts 240 hours. One with under 3 days left is
32
+ // renewed; one with under 9 hours (an 8-hour run and its homecoming) does not
33
+ // start a run until it has been.
34
+ export const CODEX_RENEW_BEFORE_MS = 3 * 24 * 3600_000;
35
+ export const CODEX_RUN_NEEDS_MS = 9 * 3600_000;
36
+ // A renewal that started this long ago and never said how it ended is over.
37
+ const RENEWAL_FENCE_MS = 10 * 60_000;
38
+ // An object a newer sign-in replaced is deleted this long after, so a run
39
+ // that was handed its link a moment before still finds it.
40
+ const OLD_LOGIN_GRACE_MS = 60 * 60_000;
41
+ const LINK_SECONDS = 30 * 60;
42
+
43
+ export const loginObjectKey = (engine, id) => `logins/${engine}/${id}.json`;
44
+ export const cloudLoginsPath = (home = os.homedir()) => path.join(home, '.agentbox', 'cloud-logins.json');
45
+ // The key is the account's own (`acct-…`), or, in older notes and tests, a
46
+ // folder; either way it is used exactly as given.
47
+ const accountOf = (engine, key) => `${engine}:${key}`;
48
+ export const newCloudAccountKey = () => `acct-${crypto.randomUUID().slice(0, 8)}`;
49
+
50
+ export function readCloudLogins({ home = os.homedir() } = {}) {
51
+ try {
52
+ const all = JSON.parse(fs.readFileSync(cloudLoginsPath(home), 'utf8'));
53
+ return all && typeof all === 'object' ? all : {};
54
+ } catch {
55
+ return {};
56
+ }
57
+ }
58
+
59
+ function writeCloudLogins(all, { home = os.homedir() } = {}) {
60
+ const file = cloudLoginsPath(home);
61
+ fs.mkdirSync(path.dirname(file), { recursive: true });
62
+ const tmp = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`;
63
+ fs.writeFileSync(tmp, JSON.stringify(all, null, 2), { mode: 0o600 });
64
+ fs.renameSync(tmp, file);
65
+ }
66
+
67
+ // ONE WRITER AT A TIME. The supervisor and every cloud run's own process
68
+ // write this note; a check of the current login and the write after it must
69
+ // be one step, or an old run's report lands over a new sign-in (Codex's
70
+ // review, finding 2). A lock file, taken exclusively; one left by a process
71
+ // that died is stale after ten seconds.
72
+ function withLock(home, fn) {
73
+ const lock = `${cloudLoginsPath(home)}.lock`;
74
+ fs.mkdirSync(path.dirname(lock), { recursive: true });
75
+ const until = Date.now() + 5000;
76
+ const pause = new Int32Array(new SharedArrayBuffer(4));
77
+ for (;;) {
78
+ try {
79
+ fs.closeSync(fs.openSync(lock, 'wx'));
80
+ break;
81
+ } catch (err) {
82
+ if (err.code !== 'EEXIST') throw err;
83
+ try { if (Date.now() - fs.statSync(lock).mtimeMs > 10_000) { fs.rmSync(lock, { force: true }); continue; } } catch { continue; }
84
+ if (Date.now() > until) throw new Error('the cloud sign-in note is busy');
85
+ Atomics.wait(pause, 0, 0, 15);
86
+ }
87
+ }
88
+ try { return fn(); } finally { fs.rmSync(lock, { force: true }); }
89
+ }
90
+
91
+ export function cloudLoginFor(engine, dir, { home = os.homedir() } = {}) {
92
+ return readCloudLogins({ home })[accountOf(engine, dir)] ?? null;
93
+ }
94
+
95
+ // A new sign-in becomes the account's current one. What it replaced is kept
96
+ // on a list to delete once no run can still be about to fetch it.
97
+ export function recordCloudLogin(engine, dir, { id, savedAt = Date.now(), expiresAt = null, email = null, org = null, limits = null }, { home = os.homedir(), now = Date.now() } = {}) {
98
+ return withLock(home, () => {
99
+ const all = readCloudLogins({ home });
100
+ const key = accountOf(engine, dir);
101
+ const was = all[key];
102
+ const retired = [...(was?.retired || [])];
103
+ if (was?.id && was.id !== id) retired.push({ id: was.id, at: now });
104
+ // What the cloud read off a reply at sign-in: whose organization, and the usage.
105
+ const read = org || limits?.length ? { org: org ?? null, limits: limits ?? [], readAt: now } : {};
106
+ all[key] = { engine, key: dir, id, savedAt, expiresAt, email: email ?? was?.email ?? null, broken: null, renewing: null, lastUsedAt: was?.lastUsedAt ?? null, retired, ...read };
107
+ writeCloudLogins(all, { home });
108
+ return all[key];
109
+ });
110
+ }
111
+
112
+ // A change to the account's note that applies only while `id` is still its
113
+ // current login: an old run that reports trouble cannot undo a new sign-in.
114
+ export function markCloudLogin(engine, dir, id, patch, { home = os.homedir() } = {}) {
115
+ return withLock(home, () => {
116
+ const all = readCloudLogins({ home });
117
+ const key = accountOf(engine, dir);
118
+ if (!all[key] || (id != null && all[key].id !== id)) return false;
119
+ all[key] = { ...all[key], ...patch };
120
+ writeCloudLogins(all, { home });
121
+ return true;
122
+ });
123
+ }
124
+
125
+ // THE SAME ACCOUNT ADDED TWICE: records of one engine whose organization
126
+ // (`org`, main/cloud/org.mjs) matches an earlier record's, by when they were
127
+ // saved. Key -> the earlier record's key. A record with no org yet is no one's
128
+ // duplicate.
129
+ function duplicatesIn(records) {
130
+ const first = new Map();
131
+ const dup = new Map();
132
+ for (const r of [...records].sort((a, b) => (a.savedAt || 0) - (b.savedAt || 0) || String(a.key).localeCompare(String(b.key)))) {
133
+ if (!r?.org) continue;
134
+ const id = `${r.engine}:${r.org}`;
135
+ if (first.has(id)) dup.set(r.key ?? r.dir, first.get(id));
136
+ else first.set(id, r.key ?? r.dir);
137
+ }
138
+ return dup;
139
+ }
140
+
141
+ // 'none' (never signed in), 'broken' (sign in again), 'duplicate' (the same
142
+ // account as an earlier one), 'limited' (at its usage limit until
143
+ // `limitedUntil`), 'renew' (Codex: must be renewed before a run), 'stale'
144
+ // (Codex: may run, renewal due), 'ready'.
145
+ export function cloudLoginState(rec, { now = Date.now() } = {}) {
146
+ if (!rec?.id) return 'none';
147
+ if (rec.broken) return 'broken';
148
+ if (rec.duplicateOf) return 'duplicate';
149
+ if (rec.limitedUntil && now < rec.limitedUntil) return 'limited';
150
+ if (rec.engine !== 'codex' || !rec.expiresAt) return 'ready';
151
+ const left = rec.expiresAt - now;
152
+ if (left < CODEX_RUN_NEEDS_MS) return 'renew';
153
+ if (left < CODEX_RENEW_BEFORE_MS) return 'stale';
154
+ return 'ready';
155
+ }
156
+
157
+ export const mayRunOn = (state) => state === 'ready' || state === 'stale';
158
+
159
+ // Every cloud account of an engine, oldest first.
160
+ export function listCloudLogins(engine, { home = os.homedir() } = {}) {
161
+ const mine = Object.values(readCloudLogins({ home })).filter((r) => r && r.engine === engine && r.id);
162
+ const dup = duplicatesIn(mine);
163
+ return mine
164
+ .map((r) => ({ ...r, key: r.key ?? r.dir, ...(dup.has(r.key ?? r.dir) ? { duplicateOf: dup.get(r.key ?? r.dir) } : {}) }))
165
+ .sort((a, b) => (a.savedAt || 0) - (b.savedAt || 0));
166
+ }
167
+
168
+ export function cloudAccountsFrom(env = process.env) {
169
+ const list = String(env.AGENTBOX_CLOUD_ACCOUNTS || '').split(',').map((s) => s.trim().toLowerCase()).filter(Boolean);
170
+ return list.length ? list : null;
171
+ }
172
+
173
+ // The cloud account that is this Mac's own account, which the app hands each
174
+ // cloud run it starts (Supervisor#_cloudAccountEnv).
175
+ export const CLOUD_ACCOUNT_ENV = { claude: 'AGENTBOX_CLOUD_CLAUDE_ACCOUNT', codex: 'AGENTBOX_CLOUD_CODEX_ACCOUNT' };
176
+ export function cloudAccountFrom(env = process.env, engine = 'claude') {
177
+ return String(env?.[CLOUD_ACCOUNT_ENV[engine]] ?? '').trim() || null;
178
+ }
179
+
180
+ // THE ACCOUNT A CLOUD RUN USES, AND IT IS ONE ACCOUNT (w-dac993a554).
181
+ //
182
+ // It used to be whichever of this engine's accounts was used least recently,
183
+ // so runs spread over every account added and a limit on one simply moved the
184
+ // work to the next. That is one person's work shared over several
185
+ // subscriptions to get past their limits, which is what the Mac stopped doing
186
+ // in `Supervisor#_theOneAccount` and what the cloud stops doing here.
187
+ //
188
+ // The account is the one this Mac runs on (`prefer`), when it is signed in to
189
+ // the cloud, so picking an account in Settings moves the cloud too; otherwise
190
+ // the first one signed in. A usage limit is never a reason to reach for
191
+ // another: `mayRunOn` turns a limited account down, and the cloud runs nothing
192
+ // and the menu says until when (renderer/src/cloud-menu.ts).
193
+ //
194
+ // `only` (emails or keys) narrows it for a copy of the app under test, which
195
+ // shares this list with the person's own app.
196
+ function accountInUse(all, engine, { now, only, prefer }) {
197
+ const dup = duplicatesIn(Object.values(all).filter((r) => r?.engine === engine));
198
+ const named = (r) => !only || [r.email, r.key ?? r.dir].some((n) => n && only.includes(String(n).toLowerCase()));
199
+ // A login that was never finished or has stopped working is not an account
200
+ // at all. A Codex login due a renewal is the account and simply cannot run
201
+ // yet; `Supervisor#_renewCloudLogins` mends that on its own clock.
202
+ const accounts = Object.entries(all)
203
+ .filter(([, r]) => r?.engine === engine && !dup.has(r.key ?? r.dir) && named(r))
204
+ .filter(([, r]) => !['none', 'broken'].includes(cloudLoginState(r, { now })))
205
+ .sort(([, a], [, b]) => (a.savedAt || 0) - (b.savedAt || 0));
206
+ return (prefer && accounts.find(([, r]) => (r.key ?? r.dir) === prefer)) || accounts[0] || null;
207
+ }
208
+
209
+ /** The account a run would take now, whether or not it can run. Null with none. */
210
+ export function cloudLoginInUse(engine, { home = os.homedir(), now = Date.now(), only = null, prefer = null } = {}) {
211
+ const hit = accountInUse(readCloudLogins({ home }), engine, { now, only, prefer });
212
+ return hit ? { ...hit[1], key: hit[1].key ?? hit[1].dir } : null;
213
+ }
214
+
215
+ export function pickCloudLogin(engine, { home = os.homedir(), now = Date.now(), only = null, prefer = null } = {}) {
216
+ return withLock(home, () => {
217
+ const all = readCloudLogins({ home });
218
+ const hit = accountInUse(all, engine, { now, only, prefer });
219
+ if (!hit || !mayRunOn(cloudLoginState(hit[1], { now }))) return null;
220
+ const [k, r] = hit;
221
+ all[k] = { ...r, lastUsedAt: now };
222
+ writeCloudLogins(all, { home });
223
+ return { ...all[k], key: r.key ?? r.dir };
224
+ });
225
+ }
226
+
227
+ const clock = (t) => new Date(t).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' }).replace(/\s/g, '').toLowerCase();
228
+
229
+ /** Why a run found no account to take, as its thread will quote it. */
230
+ export function noCloudAccountSentence(engine, inUse, { now = Date.now() } = {}) {
231
+ const name = engine === 'codex' ? 'Codex' : 'Claude';
232
+ const state = inUse ? cloudLoginState(inUse, { now }) : 'none';
233
+ if (state === 'limited') return `the ${name} account the cloud runs on is at its usage limit until ${clock(inUse.limitedUntil)}`;
234
+ if (state === 'renew') return `the ${name} account the cloud runs on is renewing its sign-in`;
235
+ return `no ${name} account is signed in for the cloud yet (the cloud button)`;
236
+ }
237
+
238
+ // An account she took out of the cloud. Its object goes too, and every older
239
+ // one it replaced (main/cloud/logins.mjs keeps them only for runs under way).
240
+ export async function removeCloudLogin(engine, key, { storage, home = os.homedir() } = {}) {
241
+ const rec = cloudLoginFor(engine, key, { home });
242
+ if (!rec) return false;
243
+ withLock(home, () => {
244
+ const all = readCloudLogins({ home });
245
+ delete all[accountOf(engine, key)];
246
+ writeCloudLogins(all, { home });
247
+ });
248
+ for (const id of [rec.id, ...(rec.retired || []).map((r) => r.id)]) {
249
+ await Promise.resolve(storage?.del?.(loginObjectKey(engine, id))).catch(() => {});
250
+ }
251
+ return true;
252
+ }
253
+
254
+ // Delete what newer sign-ins replaced, once their grace is over. Never the
255
+ // current one.
256
+ export async function sweepRetiredLogins(engine, dir, { storage, home = os.homedir(), now = Date.now() } = {}) {
257
+ const rec = cloudLoginFor(engine, dir, { home });
258
+ if (!rec?.retired?.length || !storage?.del) return;
259
+ const keep = [];
260
+ for (const r of rec.retired) {
261
+ if (now - r.at < OLD_LOGIN_GRACE_MS || r.id === rec.id) { keep.push(r); continue; }
262
+ try { await storage.del(loginObjectKey(engine, r.id)); } catch { keep.push(r); }
263
+ }
264
+ markCloudLogin(engine, dir, rec.id, { retired: keep }, { home });
265
+ }
266
+
267
+ /* ------------------------------ a session -------------------------------- */
268
+ // One sandbox session that is not a run: a sign-in or a renewal. Starts it,
269
+ // reads its `signin` events until it says how it ended, and lets the session
270
+ // go. `onEvent` hears each event; the session's end is the promise's value.
271
+ async function session({ transport, start, onEvent = () => {}, pollWaitMs = 20_000, isCancelled = () => false }) {
272
+ const sessionId = `agentbox-${start.action}-${start.engine}-${start.runId}`;
273
+ const say = (state, extra = {}) => ({ state, ...extra });
274
+ const invoke = async (payload) => {
275
+ for (let failures = 0; ;) {
276
+ try { return await transport.invoke(sessionId, payload); } catch (err) {
277
+ failures += 1;
278
+ if (failures > 5 || isCancelled()) throw err;
279
+ await new Promise((r) => setTimeout(r, 500 * failures));
280
+ }
281
+ }
282
+ };
283
+ try {
284
+ if (isCancelled()) return say('failed', { message: null });
285
+ const started = await invoke(start);
286
+ if (!started?.ok) return say('failed', { message: 'The cloud could not start the sign-in. Try again.' });
287
+ // A cancel that raced the start: the session that did start is stopped.
288
+ if (isCancelled()) {
289
+ await transport.invoke(sessionId, { action: 'signal', runId: start.runId, signal: 'SIGTERM' }).catch(() => {});
290
+ return say('failed', { message: null });
291
+ }
292
+ let since = 0;
293
+ for (;;) {
294
+ const page = await invoke({ action: 'events', runId: start.runId, since, waitMs: pollWaitMs });
295
+ if (!page?.ok) return say('failed', { message: 'The cloud sign-in went away. Try again.' });
296
+ for (const e of page.events || []) {
297
+ since = Math.max(since, e.seq);
298
+ if (e.kind !== 'signin') continue;
299
+ onEvent(e);
300
+ if (e.state === 'saved' || e.state === 'failed') return e;
301
+ }
302
+ }
303
+ } catch {
304
+ return say('failed', { message: 'The cloud could not be reached. Try again.' });
305
+ } finally {
306
+ await Promise.resolve(transport.stop?.(sessionId)).catch(() => {});
307
+ }
308
+ }
309
+
310
+ /* ------------------------------ signing in ------------------------------- */
311
+ // THE CLOUD MENU'S SIGN-IN for one account. What the menu draws comes from
312
+ // `status()`: 'starting', then for Claude 'needs-code' with the link to open
313
+ // (the person pastes Claude's code into `code()`), for Codex 'waiting' with
314
+ // the link (the browser comes back to this computer, browser-return.mjs), or,
315
+ // from a cloud image older than that, the link and a one-time code to type on
316
+ // OpenAI's page; then 'checking', and 'saved' or 'failed' with a sentence. A
317
+ // Codex sign-in made with a different ChatGPT account than the one this Mac's
318
+ // folder holds is refused.
319
+ // HOW LONG THE CLOUD MAY CHECK A SIGN-IN AFTER HER PART IS DONE before this
320
+ // Mac stops waiting and says so (2026-10-15: she sat on "Adding…" for over a
321
+ // minute). The cloud gives up on a code Claude does not take at once, and on
322
+ // one it never answers in a minute (cloud/sandbox/server.mjs); this is the
323
+ // backstop for a cloud that never says, past its own two-minute check.
324
+ export const CHECK_MS = 150_000;
325
+ // The cloud hands the browser's return to Codex and follows its redirect,
326
+ // each with its own deadline (90 and 30 seconds); past both, it is not coming.
327
+ const RETURN_MS = 125_000;
328
+
329
+ function within(ms, promise) {
330
+ let timer;
331
+ const late = new Promise((_, no) => { timer = setTimeout(() => no(new Error('no answer')), ms); });
332
+ return Promise.race([promise, late]).finally(() => clearTimeout(timer));
333
+ }
334
+
335
+ export function startCloudSignIn({ transport, storage, engine, dir, home = os.homedir(), expectEmail = null, onChange = () => {}, onSaved = () => {}, pollWaitMs, checkMs = CHECK_MS }) {
336
+ const id = crypto.randomUUID();
337
+ const runId = `signin-${id}`;
338
+ const sessionId = `agentbox-signin-${engine}-${runId}`;
339
+ // `attempt` tells one sign-in from the next, which can show the same link.
340
+ let status = { state: 'starting', url: null, code: null, message: null, attempt: runId };
341
+ let cancelled = false;
342
+ let gaveUp = false;
343
+ let checkTimer = null;
344
+ let back = null;
345
+ const letBackGo = () => { back?.close(); back = null; };
346
+ const giveUp = (message) => {
347
+ if (gaveUp || status.state === 'saved' || status.state === 'failed') return;
348
+ gaveUp = true;
349
+ cancelled = true;
350
+ letBackGo();
351
+ status = { ...status, state: 'failed', message };
352
+ onChange(status);
353
+ transport.invoke(sessionId, { action: 'signal', runId, signal: 'SIGTERM' }).catch(() => {});
354
+ };
355
+ const set = (patch) => {
356
+ status = { ...status, ...patch };
357
+ // The check's clock starts when her part ends, and stops when it ends.
358
+ if (status.state === 'checking' && !checkTimer) {
359
+ checkTimer = setTimeout(() => {
360
+ if (status.state === 'checking') giveUp(`${engine === 'codex' ? 'Codex' : 'Claude'} didn’t finish signing in. Try again.`);
361
+ }, checkMs);
362
+ checkTimer.unref?.();
363
+ } else if (status.state !== 'checking' && checkTimer) {
364
+ clearTimeout(checkTimer);
365
+ checkTimer = null;
366
+ }
367
+ onChange(status);
368
+ };
369
+
370
+ // The browser's return, passed to the cloud. Once taken the port goes, so
371
+ // another Codex account can be added while this one is checked; one not
372
+ // taken, short of OpenAI refusing it, leaves the sign-in waiting.
373
+ const handOn = async (query) => {
374
+ if (status.state !== 'waiting') return false;
375
+ set({ state: 'checking', message: null });
376
+ let out = null;
377
+ try { out = await within(RETURN_MS, transport.invoke(sessionId, { action: 'signin-callback', runId, query })); } catch { /* not delivered */ }
378
+ if (out?.ok) letBackGo();
379
+ else if (out?.error !== 'refused' && status.state === 'checking') set({ state: 'waiting' });
380
+ return !!out?.ok;
381
+ };
382
+ // The way back is open before the page is shown, so it is there first.
383
+ const openWayBack = async (url) => {
384
+ const at = wayBack(url);
385
+ if (!at) return giveUp('Codex’s sign-in link had no way back. Try again.');
386
+ try {
387
+ back = await listenForWayBack({ ...at, onReturn: handOn });
388
+ } catch {
389
+ return giveUp('Another Codex sign-in is open on this computer. Close it, then try again.');
390
+ }
391
+ if (cancelled || status.state !== 'starting') return letBackGo();
392
+ set({ state: 'waiting', url, code: null });
393
+ };
394
+
395
+ const done = (async () => {
396
+ const saveUrl = await storage.putUrl(loginObjectKey(engine, id), LINK_SECONDS);
397
+ const end = await session({
398
+ transport,
399
+ pollWaitMs,
400
+ isCancelled: () => cancelled,
401
+ start: { action: 'signin', runId, engine, saveUrl, ...(engine === 'codex' ? { flow: 'browser' } : {}) },
402
+ onEvent: (e) => {
403
+ if (e.state === 'needs-code') set({ state: 'needs-code', url: e.url });
404
+ else if (e.state === 'waiting' && engine === 'codex' && !e.code) void openWayBack(e.url);
405
+ else if (e.state === 'waiting') set({ state: 'waiting', url: e.url, code: e.code });
406
+ else if (e.state === 'checking') set({ state: 'checking' });
407
+ },
408
+ }).finally(letBackGo);
409
+ // Given up on here already: that sentence stands.
410
+ if (gaveUp) return status;
411
+ if (end.state !== 'saved') {
412
+ set({ state: 'failed', message: cancelled ? null : (end.message || 'That did not work. Try again.') });
413
+ return status;
414
+ }
415
+ if (engine === 'codex' && expectEmail && end.email && end.email.toLowerCase() !== String(expectEmail).toLowerCase()) {
416
+ await Promise.resolve(storage.del?.(loginObjectKey(engine, id))).catch(() => {});
417
+ set({ state: 'failed', message: `That signed in as ${end.email}. Sign in with ${expectEmail} instead.` });
418
+ return status;
419
+ }
420
+ recordCloudLogin(engine, dir, { id, expiresAt: end.expiresAt ?? null, email: end.email ?? null, org: end.org ?? null, limits: end.limits ?? null }, { home });
421
+ set({ state: 'saved', url: null, code: null });
422
+ try { onSaved(); } catch { /* the menu redraws anyway */ }
423
+ return status;
424
+ })().catch((err) => {
425
+ letBackGo();
426
+ set({ state: 'failed', message: `That did not work (${err.message}). Try again.` });
427
+ return status;
428
+ });
429
+
430
+ return {
431
+ status: () => status,
432
+ done,
433
+ async code(text) {
434
+ const c = String(text || '').trim();
435
+ if (!c || status.state !== 'needs-code') return false;
436
+ set({ state: 'checking', message: null });
437
+ let ok = false;
438
+ try {
439
+ const out = await transport.invoke(sessionId, { action: 'signin-code', runId, code: c });
440
+ ok = !!out?.ok;
441
+ } catch { /* not delivered */ }
442
+ // A code that did not reach the sign-in can be pasted again (finding 7).
443
+ if (!ok && status.state === 'checking') set({ state: 'needs-code', message: 'That code did not reach the sign-in. Paste it again.' });
444
+ return ok;
445
+ },
446
+ async cancel() {
447
+ cancelled = true;
448
+ letBackGo();
449
+ try { await transport.invoke(sessionId, { action: 'signal', runId, signal: 'SIGTERM' }); } catch { /* gone */ }
450
+ },
451
+ };
452
+ }
453
+
454
+ /* ------------------------------- renewing -------------------------------- */
455
+ // ONE CODEX LOGIN RENEWED: a sandbox session that renews it from the real
456
+ // login and keeps the renewed one as a new object. Fenced by a `renewing`
457
+ // mark, so an app restarted mid-renewal does not start a second one on the
458
+ // same refresh token. Returns 'renewed', 'busy', 'signed-out' or 'failed'.
459
+ export async function renewCloudCodexLogin({ transport, storage, dir, home = os.homedir(), now = Date.now(), pollWaitMs }) {
460
+ const rec = cloudLoginFor('codex', dir, { home });
461
+ if (!rec?.id || rec.broken) return 'failed';
462
+ const was = typeof rec.renewing === 'number' ? { at: rec.renewing, id: null } : rec.renewing;
463
+ if (was && now - was.at < RENEWAL_FENCE_MS) return 'busy';
464
+ // A RENEWAL THE APP NEVER HEARD THE END OF (it quit, or the Mac slept):
465
+ // the renewed login may be in the cloud already, under the id written down
466
+ // before it started. If it is, it is the current one, and the old refresh
467
+ // token, which renewing spent, is never used again (finding 3).
468
+ if (was?.id && await Promise.resolve(storage.exists?.(loginObjectKey('codex', was.id))).catch(() => false)) {
469
+ recordCloudLogin('codex', dir, { id: was.id, expiresAt: was.at + 239 * 3600_000, email: rec.email }, { home, now });
470
+ return 'renewed';
471
+ }
472
+ const id = crypto.randomUUID();
473
+ markCloudLogin('codex', dir, rec.id, { renewing: { at: now, id } }, { home });
474
+ const end = await session({
475
+ transport,
476
+ pollWaitMs,
477
+ start: {
478
+ action: 'renew',
479
+ runId: `renew-${id}`,
480
+ engine: 'codex',
481
+ loginUrl: await storage.getUrl(loginObjectKey('codex', rec.id), LINK_SECONDS),
482
+ saveUrl: await storage.putUrl(loginObjectKey('codex', id), LINK_SECONDS),
483
+ },
484
+ });
485
+ if (end.state === 'saved') {
486
+ recordCloudLogin('codex', dir, { id, expiresAt: end.expiresAt ?? null, email: rec.email }, { home, now: Date.now() });
487
+ return 'renewed';
488
+ }
489
+ if (end.signedOut) {
490
+ markCloudLogin('codex', dir, rec.id, { broken: 'signed-out', renewing: null }, { home });
491
+ return 'signed-out';
492
+ }
493
+ markCloudLogin('codex', dir, rec.id, { renewing: null }, { home });
494
+ return 'failed';
495
+ }
496
+
497
+ /* ------------------------------- looking --------------------------------- */
498
+ // A look's session and week, in the shape the usage meters draw. Null with none.
499
+ export function cloudLimits(read) {
500
+ if (!Array.isArray(read) || !read.length) return null;
501
+ return read.filter((l) => Number.isFinite(l?.percent)).map((l) => ({
502
+ span: l.span === 'week' ? 'week' : 'session',
503
+ qualifier: l.span === 'week' ? 'all models' : null,
504
+ name: l.span === 'week' ? 'This week' : 'This session',
505
+ // A session over its allowance reports utilization past 1.0.
506
+ percent: Math.min(100, l.percent), resetsText: null, resetsOn: null, resetsAt: l.resetsAt ?? null, zone: null,
507
+ }));
508
+ }
509
+
510
+ // WHOSE ONE CLAUDE LOGIN IS, AND HOW MUCH OF IT IS LEFT: a sandbox session
511
+ // that makes one tiny reply on it and reads the reply's headers. Kept on the
512
+ // login's note only while that login is still the current one. A failed look
513
+ // keeps the last reading and notes why beside it. Returns 'read' or 'failed'.
514
+ export async function lookAtCloudLogin({ transport, storage, key, home = os.homedir(), now = Date.now(), pollWaitMs }) {
515
+ const rec = cloudLoginFor('claude', key, { home });
516
+ if (!rec?.id || rec.broken) return 'failed';
517
+ const end = await session({
518
+ transport,
519
+ pollWaitMs,
520
+ start: { action: 'look', runId: `look-${crypto.randomUUID()}`, engine: 'claude', loginUrl: await storage.getUrl(loginObjectKey('claude', rec.id), LINK_SECONDS) },
521
+ });
522
+ if (end.state !== 'saved') {
523
+ markCloudLogin('claude', key, rec.id, { lookFailedAt: now, lookError: end.message ?? null }, { home });
524
+ return 'failed';
525
+ }
526
+ markCloudLogin('claude', key, rec.id, { org: end.org ?? null, limits: end.limits ?? [], readAt: now }, { home });
527
+ return 'read';
528
+ }
529
+
530
+ /* ------------------------ the first build's tokens ----------------------- */
531
+ // The first cloud build kept a Claude token per account in the macOS keychain
532
+ // (service below) or, elsewhere, in ~/.agentbox/cloud-tokens.json. Those were
533
+ // the app's own copies for the cloud, never Claude Code's own login, and
534
+ // nothing reads them now. Deleted once, on start.
535
+ export const OLD_KEYCHAIN_SERVICE_SUFFIX = 'cloud: Claude sign-in';
536
+ export function forgetOldCloudTokens({ home = os.homedir(), platform = process.platform, service, exec } = {}) {
537
+ try { fs.rmSync(path.join(home, '.agentbox', 'cloud-tokens.json'), { force: true }); } catch { /* none */ }
538
+ if (platform !== 'darwin' || !service || !exec) return;
539
+ // One entry per account; delete until there are none (at most 20).
540
+ for (let i = 0; i < 20; i += 1) {
541
+ try { exec('security', ['delete-generic-password', '-s', service]); } catch { break; }
542
+ }
543
+ }
@@ -0,0 +1,115 @@
1
+ // THE CLOUD THROUGH OUR SERVER, FOR A MAC WITH NO AWS ACCOUNT OF ITS OWN.
2
+ //
3
+ // The same two interfaces as main/cloud/aws.mjs (the session and the bucket),
4
+ // spoken to the cloud server (cloud/gateway/) with this Mac's cloud key. Files
5
+ // go straight to and from the bucket on links the server signs; the server
6
+ // only ever sees names. A sign-in is never read from here: a link to one is
7
+ // answered with its name, which the server turns into a link only inside a
8
+ // call to the sandbox.
9
+ import fs from 'node:fs';
10
+ import crypto from 'node:crypto';
11
+ import { Readable } from 'node:stream';
12
+ import { pipeline } from 'node:stream/promises';
13
+ import { LOGIN_NAME } from '../../shared/cloud-login-name.mjs';
14
+
15
+ export { LOGIN_NAME };
16
+
17
+ // Longer than the server's own deadline (scripts/cloud-gateway-setup.mjs),
18
+ // so a slow start or collect is not given up here while it succeeds there. A
19
+ // file of a few hundred MB on a slow network takes minutes.
20
+ export const CALL_MS = 100_000;
21
+ const FILE_MS = 15 * 60_000;
22
+ const LINK_SECONDS = 12 * 60 * 60;
23
+ // Lambda takes at most 6 MB; a call past this goes through the bucket.
24
+ const RELAY_BYTES = 4 * 1024 * 1024;
25
+
26
+ const isLogin = (key) => String(key).startsWith('logins/');
27
+
28
+ export function managedClients({ url, key, timeoutMs = CALL_MS, fileTimeoutMs = FILE_MS, onSignedOut = () => {} }) {
29
+ const base = String(url).replace(/\/+$/, '');
30
+
31
+ async function call(method, path, body) {
32
+ const init = {
33
+ method,
34
+ headers: { authorization: `Bearer ${key}`, ...(body ? { 'content-type': 'application/json' } : {}) },
35
+ body: body ? JSON.stringify(body) : undefined,
36
+ };
37
+ const res = await fetch(`${base}${path}`, { ...init, signal: AbortSignal.timeout(timeoutMs) });
38
+ const out = await res.json().catch(() => ({}));
39
+ if (!res.ok) {
40
+ if (res.status === 401) { try { onSignedOut(); } catch { /* the refusal below says it all the same */ } }
41
+ throw Object.assign(new Error(`the cloud server said ${res.status}: ${out.message || out.error || 'no reason'}`), {
42
+ status: res.status, error: out.error, said: out.message, signedOut: res.status === 401,
43
+ });
44
+ }
45
+ return out;
46
+ }
47
+ const linkFor = async (op, k, seconds = LINK_SECONDS) => (await call('POST', '/v1/link', { op, key: k, seconds })).url;
48
+ const object = (link, init = {}) => fetch(link, { ...init, signal: AbortSignal.timeout(fileTimeoutMs) });
49
+ const refuseLogin = (k) => { if (isLogin(k)) throw new Error('a cloud sign-in is never read on this Mac'); };
50
+
51
+ async function invoke(sessionId, payload) {
52
+ let out;
53
+ try {
54
+ const text = JSON.stringify({ sessionId, payload });
55
+ if (Buffer.byteLength(text) <= RELAY_BYTES) out = await call('POST', '/v1/invoke', { sessionId, payload });
56
+ else {
57
+ const payloadKey = `runs/calls/${crypto.randomUUID()}.json`;
58
+ await storage.put(payloadKey, JSON.stringify(payload));
59
+ out = await call('POST', '/v1/invoke', { sessionId, payloadKey });
60
+ }
61
+ } catch (err) {
62
+ // A refusal is an answer: run.mjs retries what throws, some calls for
63
+ // ever. A poll refused ends as a session that is gone does. A throttle
64
+ // (429) or a server failure passes, so those are thrown and retried.
65
+ if (!err.status || err.status === 429 || err.status >= 500) throw err;
66
+ return { ok: false, error: payload?.action === 'events' ? 'no-run' : err.error || 'refused', message: err.message };
67
+ }
68
+ if (!out?.relayed) return out;
69
+ const res = await object(out.relayed.url);
70
+ if (!res.ok) throw new Error(`the bucket refused the cloud's answer: ${res.status}`);
71
+ const answer = await res.json();
72
+ await call('POST', '/v1/delete', { key: out.relayed.key }).catch(() => {});
73
+ return answer;
74
+ }
75
+
76
+ const transport = {
77
+ invoke,
78
+ stop: (sessionId) => call('POST', '/v1/stop', { sessionId }),
79
+ };
80
+
81
+ const storage = {
82
+ async put(k, body) {
83
+ const res = await object(await linkFor('put', k), { method: 'PUT', body });
84
+ if (!res.ok) throw new Error(`the bucket refused ${k}: ${res.status}`);
85
+ },
86
+ async putFile(k, file) { await storage.put(k, fs.readFileSync(file)); },
87
+ async get(k) {
88
+ refuseLogin(k);
89
+ const res = await object(await linkFor('get', k));
90
+ if (res.status === 404) return null;
91
+ if (!res.ok) throw new Error(`the bucket refused ${k}: ${res.status}`);
92
+ return Buffer.from(await res.arrayBuffer());
93
+ },
94
+ async getToFile(k, file) {
95
+ refuseLogin(k);
96
+ const res = await object(await linkFor('get', k));
97
+ if (res.status === 404) return false;
98
+ if (!res.ok) throw new Error(`the bucket refused ${k}: ${res.status}`);
99
+ await pipeline(Readable.fromWeb(res.body), fs.createWriteStream(file));
100
+ return true;
101
+ },
102
+ async exists(k) {
103
+ const res = await object(await linkFor('head', k), { method: 'HEAD' });
104
+ if (res.status === 404) return false;
105
+ if (!res.ok) throw new Error(`the bucket refused ${k}: ${res.status}`);
106
+ return true;
107
+ },
108
+ async del(k) { await call('POST', '/v1/delete', { key: k }); },
109
+ getUrl: async (k, seconds) => (isLogin(k) ? `${LOGIN_NAME}${k}` : linkFor('get', k, seconds)),
110
+ // A sign-in is saved by the sandbox on a link the server makes from its name.
111
+ putUrl: async (k, seconds) => (isLogin(k) ? `${LOGIN_NAME}${k}` : linkFor('put', k, seconds)),
112
+ };
113
+
114
+ return { transport, storage, me: () => call('GET', '/v1/me') };
115
+ }