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,341 @@
1
+ // A THREAD'S NEXT TURN, READY FOR THE CLOUD TO START WITH THE COMPUTER SHUT
2
+ // (docs/cloud/PHONE-PLAN.md step 2, docs/cloud/PLAN.md "Next" step 4).
3
+ //
4
+ // After each turn of a thread that is on the phone, the computer leaves a kit
5
+ // in the person's cloud storage: everything a cloud start needs (the folder's
6
+ // bundle, the files a run takes, the arguments, the sign-in by name) except
7
+ // the reply and the run's own links. A reply the computer has not claimed
8
+ // within CLOUD_TAKES_AFTER_MS is started from the kit by the cloud server
9
+ // (cloud/gateway/phone.mjs), which claims the command `cloud:<run id>`; the
10
+ // computer, waking, joins that run as it joins one it left going
11
+ // (main/cloud/run.mjs `joining`) and brings its work home.
12
+ //
13
+ // Shared by the computer (main/cloud/kit.mjs), the server and the phone, so
14
+ // all three agree on the names. Pure: no file, no network.
15
+
16
+ export const KIT_VERSION = 1;
17
+
18
+ // Where the reply goes in the turn's prompt. Characters no prompt carries, so
19
+ // it is found once or not at all.
20
+ export const REPLY_MARK = '⁣AGENTBOX-REPLY⁣';
21
+
22
+ // A computer that is awake claims a phone command within seconds; one left
23
+ // this long is the cloud's to start.
24
+ export const CLOUD_TAKES_AFTER_MS = 60_000;
25
+ // And one older than this is left to the computer: the phone said it long ago.
26
+ export const CLOUD_TAKES_UNTIL_MS = 24 * 60 * 60_000;
27
+ // Inside the bucket's seven-day expiry of `runs/`, with a day's room.
28
+ export const KIT_FRESH_MS = 6 * 24 * 60 * 60_000;
29
+ const KIT_BYTES = 2 * 1024 * 1024;
30
+
31
+ const CLAIM = /^cloud:([A-Za-z0-9._-]{1,120})$/;
32
+ export const cloudClaim = (runId) => `cloud:${runId}`;
33
+ export const runOfClaim = (claimedBy) => (typeof claimedBy === 'string' && CLAIM.test(claimedBy) ? claimedBy.match(CLAIM)[1] : null);
34
+
35
+ // The same name main/cloud/run.mjs gives a run it starts, so a computer
36
+ // joining a run the server started asks for it by the name it would have used.
37
+ export const cloudSessionId = (item, runId) => `agentbox-${String(item || 'run').replace(/[^a-zA-Z0-9-]/g, '')}-${runId}`;
38
+
39
+ const seg = (s) => String(s ?? '').replace(/[^A-Za-z0-9._-]/g, '_').replace(/^\.+/, '_').slice(0, 120) || '_';
40
+ /** Where a thread's kit lives in the person's own storage. */
41
+ export const kitKey = (device, project, item) => `runs/kits/${seg(device)}/${seg(project)}/${seg(item)}.json`;
42
+ /** What the server keeps about a run it started, for the moment it ends. */
43
+ export const serverRunKey = (runId) => `runs/server/${seg(runId)}.json`;
44
+ /** Written by a computer that has joined a run the server started: the server feeds and closes it no more. */
45
+ export const serverJoinedKey = (runId) => `runs/server/${seg(runId)}.joined.json`;
46
+ /** The objects a run writes as it ends, named as main/cloud/run.mjs names them. */
47
+ export const runKey = (runId, name) => `runs/${runId}/${name}`;
48
+
49
+ /** The text either side of the reply, or null when the prompt has no one place for it. */
50
+ export function replyFrame(prompt) {
51
+ const text = String(prompt ?? '');
52
+ const at = text.indexOf(REPLY_MARK);
53
+ if (at < 0 || text.indexOf(REPLY_MARK, at + 1) >= 0) return null;
54
+ return { before: text.slice(0, at), after: text.slice(at + REPLY_MARK.length) };
55
+ }
56
+
57
+ export const replyPrompt = (frame, reply) => `${frame.before}${String(reply ?? '')}${frame.after}`;
58
+
59
+ // The first line on the agent's input, as the computer writes one
60
+ // (main/claude-input.mjs), with the cloud's note at its top the way
61
+ // main/cloud/run.mjs `claudeLineForTheCloud` puts it on a resumed run.
62
+ export function cloudUserLine(text, { note = null, uuid } = {}) {
63
+ const content = note ? `${note}\n\n${text}` : String(text);
64
+ return `${JSON.stringify({ type: 'user', uuid, message: { role: 'user', content }, parent_tool_use_id: null, session_id: '' })}\n`;
65
+ }
66
+
67
+ const isKey = (k) => typeof k === 'string' && /^runs\/[A-Za-z0-9._@+=/-]+$/.test(k) && !k.includes('..');
68
+ const isLoginKey = (k) => typeof k === 'string' && /^logins\/[A-Za-z0-9._@+=/-]+$/.test(k) && !k.includes('..');
69
+ const isPath = (p) => typeof p === 'string' && p.startsWith('/') && !p.includes('\0');
70
+ const strings = (a) => Array.isArray(a) && a.every((s) => typeof s === 'string');
71
+
72
+ /** Why a kit cannot be started from, or null. Read by the server before it trusts one. */
73
+ export function checkKit(kit, { now = Date.now() } = {}) {
74
+ if (!kit || typeof kit !== 'object') return 'not a kit';
75
+ if (kit.v !== KIT_VERSION) return 'another version of kit';
76
+ if (kit.engine !== 'claude' && kit.engine !== 'codex') return 'only Claude Code and Codex threads carry on in the cloud';
77
+ if (kit.engine === 'codex') { const p = codexKitProblem(kit.codex); if (p) return p; }
78
+ if (!(now - Number(kit.madeAt) < KIT_FRESH_MS)) return 'the kit is too old';
79
+ if (!isPath(kit.cwd)) return 'no folder';
80
+ if (!strings(kit.argv) || !kit.argv.length) return 'no command';
81
+ if (!kit.frame || typeof kit.frame.before !== 'string' || typeof kit.frame.after !== 'string') return 'no place for the reply';
82
+ if (!kit.login || kit.login.engine !== kit.engine || !isLoginKey(kit.login.key)) return 'no sign-in';
83
+ if (kit.env && (typeof kit.env !== 'object' || !Object.values(kit.env).every((v) => typeof v === 'string'))) return 'bad environment';
84
+ if (!kit.plain && typeof kit.branch !== 'string') return 'no branch';
85
+ if (kit.bundleKey !== undefined && kit.bundleKey !== null && !isKey(kit.bundleKey)) return 'bad bundle';
86
+ if (kit.base && (!isKey(kit.base.key) || typeof kit.base.sha !== 'string')) return 'bad base';
87
+ if (!kit.plain && !kit.bundleKey && !kit.base) return 'no copy of the folder';
88
+ for (const f of kit.files ?? []) if (!isPath(f?.path) || !isKey(f.key) || (f.untar !== undefined && !isPath(f.untar))) return 'bad file';
89
+ for (const f of kit.homeFiles ?? []) if (typeof f?.rel !== 'string' || f.rel.startsWith('/') || f.rel.includes('..') || !isKey(f.key)) return 'bad home file';
90
+ for (const b of kit.bringDirs ?? []) if (typeof b?.name !== 'string' || !/^[a-z.]+$/.test(b.name) || !isPath(b.dir)) return 'bad folder to bring home';
91
+ if (!kit.bringDirs?.length) return 'nothing to bring home';
92
+ if (kit.hold !== undefined && !(Array.isArray(kit.hold) && kit.hold.every((s) => typeof s === 'string'))) return 'bad held servers';
93
+ if (kit.handshakes !== undefined && (!kit.handshakes || typeof kit.handshakes !== 'object' || Array.isArray(kit.handshakes))) return 'bad handshakes';
94
+ if (JSON.stringify(kit).length > KIT_BYTES) return 'the kit is too large';
95
+ return null;
96
+ }
97
+
98
+ /**
99
+ * The sandbox's `start` for a run from this kit (cloud/sandbox/server.mjs),
100
+ * built as main/cloud/run.mjs builds one. `getUrl(key)` and `putUrl(key)` make
101
+ * links; the sign-in goes by name (`loginName(key)`) for the server to swap.
102
+ */
103
+ export async function startPayload(kit, { runId, getUrl, putUrl, loginName, offlineAfterMs = 90_000, kept = null, endTurn = 'claude-stream-json', refs = null, away = false }) {
104
+ const results = {
105
+ bundleUrl: await putUrl(runKey(runId, 'out.bundle')),
106
+ changesUrl: await putUrl(runKey(runId, 'changes.bundle')),
107
+ docsUrl: await putUrl(runKey(runId, 'docs.tgz')),
108
+ eventsUrl: await putUrl(runKey(runId, 'events.jsonl')),
109
+ doneUrl: await putUrl(runKey(runId, 'done.json')),
110
+ bring: await Promise.all(kit.bringDirs.map(async (b) => ({ dir: b.dir, url: await putUrl(runKey(runId, b.name)) }))),
111
+ };
112
+ const files = await Promise.all((kit.files ?? []).map(async (f) => ({ path: f.path, url: await getUrl(f.key), ...(f.untar ? { untar: f.untar } : {}) })));
113
+ const homeFiles = await Promise.all((kit.homeFiles ?? []).map(async (f) => ({ rel: f.rel, url: await getUrl(f.key) })));
114
+ return {
115
+ action: 'start', runId, cwd: kit.cwd, branch: kit.branch ?? null,
116
+ ...(kit.bundleKey ? { bundleUrl: await getUrl(kit.bundleKey) } : {}),
117
+ ...(kit.plain ? { plain: true } : {}),
118
+ ...(kit.base ? { base: { url: await getUrl(kit.base.key), sha: kit.base.sha }, refs: refs ? { ...(kit.refs ?? {}), ...refs } : (kit.refs ?? null) } : {}),
119
+ // A new thread's branch, made at the main its kit saw (`fillNewThreadKit`).
120
+ ...(!kit.base && refs ? { refs } : {}),
121
+ argv: kit.argv, env: kit.env ?? {}, files,
122
+ ...(kit.changesRef ? { changesRef: kit.changesRef } : {}),
123
+ ...(kit.remote ? { remote: kit.remote } : {}),
124
+ ...(kit.docs ? { docs: kit.docs } : {}),
125
+ ...(homeFiles.length ? { homeFiles } : {}),
126
+ login: { engine: kit.login.engine, id: kit.login.id ?? null, url: loginName(kit.login.key) },
127
+ results,
128
+ tails: true,
129
+ // Null keeps the agent's input open while the computer is away, for a run
130
+ // the server feeds more replies to (cloud/gateway/phone.mjs).
131
+ offline: { afterMs: offlineAfterMs, endTurn, kept, ...heldFor(kit), ...(away ? { away: true } : {}) },
132
+ };
133
+ }
134
+
135
+ // PERMISSION REQUESTS WITH THE COMPUTER AWAY. A kit for a run that asks names
136
+ // the app's approvals server as held (cloud/sandbox/server.mjs): its calls
137
+ // wait for an answer instead of being refused, and its handshake is the
138
+ // computer's own, carried in the kit. `away`: the server starts it, so the
139
+ // computer is not there from its first moment.
140
+ const heldFor = (kit) => (Array.isArray(kit.hold) && kit.hold.length
141
+ ? { hold: kit.hold, ...(kit.handshakes && typeof kit.handshakes === 'object' ? { handshakes: kit.handshakes } : {}) }
142
+ : {});
143
+
144
+ /** The app's approvals server, by the name a run's tools give it (`mcp__zero-approvals__approval_prompt`). */
145
+ export const APPROVALS_SERVER = 'zero-approvals';
146
+
147
+ /** A held request's card id: one the phone may send back (`^[\w-]{1,80}$`), and marked as the cloud's. */
148
+ export const askId = (runId, conn, id) => `cloud-${String(runId)}-${String(conn)}-${String(id)}`.replace(/[^\w-]/g, '_').slice(0, 80);
149
+ /** Whether a card's id is one the cloud put on the phone, so its answer is the cloud's to give. */
150
+ export const cloudCard = (approval) => typeof approval === 'string' && approval.startsWith('cloud-');
151
+
152
+ // What the app's approvals server answers (main/approval-prompt-core.mjs).
153
+ export const DECLINED = 'The founder declined this action. Work with what you are already allowed to do, or park the remainder as a review.';
154
+ /** The answer to a held approval_prompt call, as the app's own server would write it. */
155
+ export function approvalAnswerLine(id, { allow, note = null, input = {} } = {}) {
156
+ const said = allow ? { behavior: 'allow', updatedInput: input ?? {} } : { behavior: 'deny', message: note || DECLINED };
157
+ return JSON.stringify({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: JSON.stringify(said) }] } });
158
+ }
159
+
160
+ /** Each of Claude Code's `result` lines, in order, off a run's event log. */
161
+ export function answersIn(eventsText) {
162
+ let out = '';
163
+ for (const raw of String(eventsText ?? '').split('\n')) {
164
+ if (!raw.trim()) continue;
165
+ let e;
166
+ try { e = JSON.parse(raw); } catch { continue; }
167
+ if (e?.kind === 'stdout' && typeof e.data === 'string') out += Buffer.from(e.data, 'base64').toString('utf8');
168
+ }
169
+ return resultsInLines(out.split('\n'));
170
+ }
171
+
172
+ /** The `result`s among stream-json lines: { text, isError } each. */
173
+ export function resultsInLines(lines) {
174
+ const found = [];
175
+ for (const line of lines) {
176
+ if (!line.trim()) continue;
177
+ let msg;
178
+ try { msg = JSON.parse(line); } catch { continue; }
179
+ if (msg?.type === 'result') found.push({ text: String(msg.result ?? ''), isError: msg.is_error === true });
180
+ }
181
+ return found;
182
+ }
183
+
184
+ // WHAT THE AGENT LAST SAID, off a run's event log (one JSON event a line, its
185
+ // output in base64): the text of Claude Code's last `result`, and whether it
186
+ // was an error. Null when the run never got that far.
187
+ export function lastAnswerIn(eventsText) {
188
+ let out = '';
189
+ for (const raw of String(eventsText ?? '').split('\n')) {
190
+ if (!raw.trim()) continue;
191
+ let e;
192
+ try { e = JSON.parse(raw); } catch { continue; }
193
+ if (e?.kind === 'stdout' && typeof e.data === 'string') out += Buffer.from(e.data, 'base64').toString('utf8');
194
+ }
195
+ let answer = null;
196
+ for (const line of out.split('\n')) {
197
+ if (!line.trim()) continue;
198
+ let msg;
199
+ try { msg = JSON.parse(line); } catch { continue; }
200
+ if (msg?.type === 'result') answer = { text: String(msg.result ?? ''), isError: msg.is_error === true };
201
+ }
202
+ return answer;
203
+ }
204
+
205
+ // A CODEX KIT'S THREAD (main/codex-app-server.mjs). A Codex run is
206
+ // `codex app-server` spoken to in JSON-RPC, so the kit holds the lines the
207
+ // computer's own client would send to open the thread in the cloud
208
+ // (initialize, initialized, thread/resume rewritten as main/cloud/run.mjs
209
+ // rewrites them) and the turn's settings; her reply goes into a `turn/start`
210
+ // whose input is the turn's prompt. Request ids are fixed: `resumeId` names
211
+ // the resume's answer, and turns go from `firstTurnId` on.
212
+ // A new thread's Codex kit starts its thread (`startId`, the `thread/start`'s
213
+ // own id) instead, and learns the thread's id from that answer.
214
+ function codexKitProblem(c) {
215
+ if (!c || !strings(c.open) || c.open.length < 3) return 'a Codex kit with no lines to open its thread';
216
+ const opens = Number.isInteger(c.startId) ? c.startId : c.resumeId;
217
+ if (!Number.isInteger(opens) || !Number.isInteger(c.firstTurnId) || c.firstTurnId <= opens) return 'a Codex kit with no request ids';
218
+ if (!c.turn || typeof c.turn !== 'object') return 'a Codex kit with no turn';
219
+ if (Number.isInteger(c.startId)) return c.turn.threadId === undefined ? null : 'a Codex kit that starts a thread and resumes one';
220
+ if (typeof c.turn.threadId !== 'string' || !c.turn.threadId) return 'a Codex kit with no thread to resume';
221
+ return null;
222
+ }
223
+
224
+ /** What opens a Codex kit's thread in the cloud, as one input. */
225
+ export const codexOpenLines = (kit) => kit.codex.open.map((l) => (l.endsWith('\n') ? l : `${l}\n`)).join('');
226
+
227
+ /** Her reply as one turn of the kit's thread, with request id `id`. */
228
+ // `raw` is a new thread's first turn, its brief as it stands; `threadId` the
229
+ // id a new thread's start answered with.
230
+ export function codexTurnLines(kit, reply, { id, raw = null, threadId: named = null }) {
231
+ const { threadId: kept, ...settings } = kit.codex.turn;
232
+ const text = typeof raw === 'string' ? raw : replyPrompt(kit.frame, reply);
233
+ return `${JSON.stringify({ jsonrpc: '2.0', id, method: 'turn/start', params: { threadId: named ?? kept, ...settings, input: [{ type: 'text', text }] } })}\n`;
234
+ }
235
+
236
+ // WHAT A CODEX RUN SAID, read as main/codex.mjs `captureCodexEvent` reads it:
237
+ // a turn's answer is its `agentMessage` with `phase: "final_answer"` (one with
238
+ // no phase still counts, a `commentary` never does), or the final answer in
239
+ // `turn.items`; a failed turn says why. `state` carries a turn across reads
240
+ // and is told the resume's answer (`resumed`: true, or why it was refused);
241
+ // a refused `turn/start` (one of `turnIds`) is a failed turn of its own.
242
+ export function codexReadLines(lines, state = {}) {
243
+ const answers = [];
244
+ for (const raw of lines) {
245
+ if (!raw || !raw.trim()) continue;
246
+ let msg;
247
+ try { msg = JSON.parse(raw); } catch { continue; }
248
+ if (!msg || typeof msg !== 'object') continue;
249
+ if (msg.id !== undefined && msg.method === undefined) {
250
+ const why = msg.error ? String(msg.error.message ?? 'refused') : null;
251
+ if (msg.id === state.resumeId) {
252
+ state.resumed = why ?? true;
253
+ if (!why && typeof msg.result?.thread?.id === 'string') state.threadId = msg.result.thread.id;
254
+ }
255
+ else if (why && (state.turnIds ?? []).includes(msg.id)) answers.push({ text: why, isError: true });
256
+ continue;
257
+ }
258
+ const p = msg.params ?? {};
259
+ if (msg.method === 'item/completed') {
260
+ const item = p.item ?? {};
261
+ if (item.type !== 'agentMessage') continue;
262
+ const phase = typeof item.phase === 'string' ? item.phase : null;
263
+ if (phase === 'commentary' || (state.final && phase !== 'final_answer')) continue;
264
+ state.result = String(item.text ?? '');
265
+ if (phase === 'final_answer') state.final = true;
266
+ continue;
267
+ }
268
+ if (msg.method === 'turn/completed' || msg.method === 'turn/failed') {
269
+ const turn = p.turn ?? {};
270
+ const status = typeof turn.status === 'string' ? turn.status : msg.method === 'turn/failed' ? 'failed' : 'completed';
271
+ if (status === 'completed') {
272
+ const kept = Array.isArray(turn.items) ? [...turn.items].reverse().find((i) => i?.type === 'agentMessage' && i.phase === 'final_answer') : null;
273
+ const text = state.result ?? (kept ? String(kept.text ?? '') : null);
274
+ answers.push(text == null ? { text: '', isError: true } : { text, isError: false });
275
+ } else {
276
+ const why = String(turn.error?.message ?? '').trim() || (status === 'interrupted' ? 'the turn was interrupted' : `the turn ${status}`);
277
+ answers.push({ text: why, isError: true });
278
+ }
279
+ state.result = null;
280
+ state.final = false;
281
+ }
282
+ }
283
+ return answers;
284
+ }
285
+
286
+ const stdoutOf = (eventsText) => {
287
+ let out = '';
288
+ for (const raw of String(eventsText ?? '').split('\n')) {
289
+ if (!raw.trim()) continue;
290
+ let e;
291
+ try { e = JSON.parse(raw); } catch { continue; }
292
+ if (e?.kind === 'stdout' && typeof e.data === 'string') out += Buffer.from(e.data, 'base64').toString('utf8');
293
+ }
294
+ return out;
295
+ };
296
+
297
+ /** Each Codex turn's answer, in order, off a run's event log. */
298
+ export const codexAnswersIn = (eventsText, state = {}) => codexReadLines(stdoutOf(eventsText).split('\n'), state);
299
+
300
+ /** A Codex run's last answer off its event log, or null. */
301
+ export function codexAnswerIn(eventsText) {
302
+ const all = codexAnswersIn(eventsText);
303
+ return all.length ? all[all.length - 1] : null;
304
+ }
305
+
306
+ // A NEW THREAD FROM THE PHONE WITH THE COMPUTER SHUT. Each project on the
307
+ // phone keeps a kit for a fresh thread beside its threads' kits, built for a
308
+ // stand-in item whose id, title and body are these marks and packed from
309
+ // main (main/cloud/run.mjs, Supervisor#_leaveNewThreadKit). The item's mark
310
+ // is plain letters, so a folder name made from a path that holds it
311
+ // (Claude Code's transcripts) holds it unchanged too.
312
+ export const NEW_THREAD = '_new';
313
+ export const NEW_ITEM_MARK = 'xagentboxnewitemx';
314
+ export const NEW_TITLE_MARK = '⁣AGENTBOX-NEW-TITLE⁣';
315
+ export const NEW_BODY_MARK = '⁣AGENTBOX-NEW-BODY⁣';
316
+ // A Codex one, when the computer has a Codex sign-in for the cloud, beside it.
317
+ export const NEW_THREAD_CODEX = '_new-codex';
318
+ export const newThreadKitKey = (device, project, engine = 'claude') => kitKey(device, project, engine === 'codex' ? NEW_THREAD_CODEX : NEW_THREAD);
319
+
320
+ const ID = /^[A-Za-z0-9_-]{1,64}$/;
321
+ const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
322
+ const WORDS = new RegExp(`${esc(NEW_TITLE_MARK)}|${esc(NEW_BODY_MARK)}`, 'g');
323
+
324
+ /**
325
+ * A project's new-thread kit made the kit of thread `itemId`, the id the
326
+ * computer gives a thread the phone started (madeHere): every mark of the
327
+ * item put in, her title and body in its first prompt, and the branch to make
328
+ * at the main the kit saw. Null for a kit that is not one, or an id no folder
329
+ * may have.
330
+ */
331
+ export function fillNewThreadKit(kit, { itemId, title = '', body = '' }) {
332
+ const n = kit?.newThread;
333
+ if (!n || typeof n.brief !== 'string' || typeof n.mainSha !== 'string' || !ID.test(String(itemId ?? ''))) return null;
334
+ const swap = (v) => (typeof v === 'string' ? v.split(NEW_ITEM_MARK).join(itemId)
335
+ : Array.isArray(v) ? v.map(swap)
336
+ : v && typeof v === 'object' ? Object.fromEntries(Object.entries(v).map(([k, x]) => [k, swap(x)])) : v);
337
+ const { newThread: _n, ...rest } = kit;
338
+ const filled = { ...swap(rest), item: itemId };
339
+ const prompt = swap(n.brief).replace(WORDS, (m) => (m === NEW_TITLE_MARK ? String(title) : String(body)));
340
+ return { kit: filled, prompt, refs: { [`refs/heads/${filled.branch}`]: n.mainSha }, mainSha: n.mainSha };
341
+ }
@@ -0,0 +1,4 @@
1
+ // How a Mac on the managed cloud names a sign-in it may not read: in place of
2
+ // a link, this prefix and the sign-in's key. The cloud server swaps it for a
3
+ // real link only inside a call to the sandbox (cloud/gateway/gateway.mjs).
4
+ export const LOGIN_NAME = 'agentbox-login:';
@@ -0,0 +1,17 @@
1
+ // THE LINE A CLOUD RUN PRINTS ONCE THE CLOUD HAS ACCEPTED ITS START
2
+ // (main/cloud/run.mjs), with how long this Mac took to pack and send it and
3
+ // the cloud took to take it. From then on the run holds nothing on this Mac,
4
+ // so it stops counting against the starts allowed at once
5
+ // (Supervisor#_cloudStarting). Not `agentbox cloud:`, which is what a run
6
+ // says about why it failed (Supervisor#_noteCloudOutcome).
7
+ export const CLOUD_SENT = 'agentbox cloud-sent:';
8
+
9
+ export function sentLine({ packMs, startMs }) {
10
+ return `${CLOUD_SENT} packed ${Math.round(packMs)} started ${Math.round(startMs)}\n`;
11
+ }
12
+
13
+ /** `{ packMs, startMs }` from any text holding the line, else null. */
14
+ export function readSent(text) {
15
+ const m = new RegExp(`${CLOUD_SENT} packed (\\d+) started (\\d+)`).exec(String(text ?? ''));
16
+ return m ? { packMs: Number(m[1]), startMs: Number(m[2]) } : null;
17
+ }
@@ -0,0 +1,23 @@
1
+ // THE CLOUD IMAGE'S CLAUDE CODE AND CODEX AGAINST THIS MAC'S.
2
+ //
3
+ // scripts/cloud-setup.mjs pins the image to the versions this Mac ran when it
4
+ // was built and writes them into cloud.json. A thread moves between the two,
5
+ // so a gap in major.minor is worth a rebuild, and a gap in major is a known
6
+ // break: that engine's threads stay on this Mac until the image is rebuilt.
7
+
8
+ const minor = (v) => {
9
+ const m = /^(\d+)\.(\d+)\.\d+/.exec(String(v ?? '').trim());
10
+ return m ? { major: Number(m[1]), text: `${m[1]}.${m[2]}` } : null;
11
+ };
12
+
13
+ /** `cloud` and `mac` are `{ claude, codex }` version strings; one entry per engine that differs. */
14
+ export function cloudVersionGaps({ cloud = {}, mac = {} } = {}) {
15
+ const out = [];
16
+ for (const engine of ['claude', 'codex']) {
17
+ const there = minor(cloud[engine]);
18
+ const here = minor(mac[engine]);
19
+ if (!there || !here || there.text === here.text) continue;
20
+ out.push({ engine, cloud: there.text, mac: here.text, blocks: there.major !== here.major });
21
+ }
22
+ return out;
23
+ }
@@ -175,13 +175,13 @@ export function codexImportRow(thread, { projectName, now = Date.now() } = {}) {
175
175
  /**
176
176
  * THE ROW ONCE THE USER SAID YES. Their prompt is the body and Codex's last message
177
177
  * is the result, so the thread reads You, then Codex, the way it did over
178
- * there. The result carries the one line that says where replies go. */
178
+ * there. A reply on it carries the work on here. */
179
179
  export function codexMirrorPatch(thread) {
180
180
  const prompt = plainQuote(thread?.prompt);
181
181
  const last = plainQuote(thread?.last);
182
182
  return {
183
183
  body: prompt || String(thread?.title ?? '').trim(),
184
- result: last ? `${last}\n\nRead from Codex. Replies happen there.` : 'Codex has not answered yet.\n\nRead from Codex. Replies happen there.',
184
+ result: last || 'Codex has not answered yet.',
185
185
  kind: 'directive',
186
186
  labels: [CODEX_MIRROR_LABEL, codexLabel(thread?.id)],
187
187
  };
@@ -8,17 +8,18 @@ import type { CodexModeId } from '../renderer/src/types';
8
8
 
9
9
  /**
10
10
  * THE PAIR A MODE REALLY IS. `sandbox` and `approvalPolicy` go straight into
11
- * Codex's `thread/start`; `approvalsReviewer` is set on `auto` alone, which is
12
- * the only row with asks left to answer. They are plain strings rather than
13
- * unions on purpose: the authority for what Codex accepts is Codex's own
14
- * schema, quoted in the .mjs, and a union here would be a third copy of it
15
- * that nothing checks.
11
+ * Codex's `thread/start`. NO ROW CARRIES A REVIEWER any more: all three get
12
+ * the same one and `codexPosture` supplies it, because a per-row value is how
13
+ * the 2026-10-09 hole stayed open -- two of the three named none, fell through
14
+ * to a config file, and behaved differently from the one that did. They are
15
+ * plain strings rather than unions on purpose: the authority for what Codex
16
+ * accepts is Codex's own schema, quoted in the .mjs, and a union here would be
17
+ * a third copy of it that nothing checks.
16
18
  */
17
19
  export interface CodexMode {
18
20
  label: string;
19
21
  sandbox: string;
20
22
  approvalPolicy: string;
21
- approvalsReviewer?: string;
22
23
  what: string;
23
24
  }
24
25
 
@@ -29,12 +30,24 @@ export declare const CODEX_MODE_COMMAND: Record<CodexModeId, string>;
29
30
  export declare const CODEX_MODE_HINT: Record<CodexModeId, string>;
30
31
  export declare const CODEX_MODE_STATUS: Record<CodexModeId, string>;
31
32
 
33
+ export declare const CODEX_REVIEWERS: string[];
34
+ export declare const ASKS_THE_PERSON: string;
35
+
32
36
  export declare function isCodexMode(word: unknown): boolean;
33
- /** The pair, falling back to the default's pair when the word is not one of ours. */
34
- export declare function codexPosture(mode: string | null | undefined): {
37
+ /** The reviewer to send, falling back to the one that asks the person. */
38
+ export declare function codexReviewer(asked?: string | null): string;
39
+ /**
40
+ * The posture, falling back to the default's when the word is not one of ours.
41
+ * `approvalsReviewer` is ALWAYS present: an absent field lets a Codex home's
42
+ * own `approvals_reviewer` decide whether a worker may leave its sandbox.
43
+ */
44
+ export declare function codexPosture(
45
+ mode: string | null | undefined,
46
+ options?: { reviewer?: string | null },
47
+ ): {
35
48
  sandbox: string;
36
49
  approvalPolicy: string;
37
- approvalsReviewer?: string;
50
+ approvalsReviewer: string;
38
51
  };
39
52
  export declare function codexModeWords(mode: string | null | undefined): string;
40
53
  /**
@@ -37,6 +37,57 @@ export const CODEX_MODE_ORDER = ['read-only', 'auto', 'full-access'];
37
37
  /** The one a worker runs in when nothing has been picked. */
38
38
  export const CODEX_DEFAULT_MODE = 'auto';
39
39
 
40
+ /**
41
+ * WHO A SANDBOX ESCAPE IS ROUTED TO, in Codex's own three words. Read off
42
+ * `codex app-server generate-json-schema` on codex-cli 0.160.0, 2026-10-09.
43
+ * The schema's own description: "Configures who approval requests are routed to
44
+ * for review. Examples include sandbox escapes, blocked network access, MCP
45
+ * approval prompts, and ARC escalations. Defaults to `user`. `auto_review` uses
46
+ * a carefully prompted subagent to gather relevant context and apply a
47
+ * risk-based decision framework before approving or denying the request."
48
+ *
49
+ * A value outside this list is not a near miss: `thread/start` refuses it and
50
+ * the run is lost, so `codexReviewer` below falls back rather than passing one
51
+ * through.
52
+ */
53
+ export const CODEX_REVIEWERS = ['user', 'auto_review', 'guardian_subagent'];
54
+
55
+ /**
56
+ * THE ONE THAT PUTS THE QUESTION IN FRONT OF A PERSON, which is the whole
57
+ * reason this constant has a name instead of being a string in three places.
58
+ * It is also the protocol's own default; Agentbox sends it explicitly anyway,
59
+ * for the reason `codexReviewer` gives.
60
+ */
61
+ export const ASKS_THE_PERSON = 'user';
62
+
63
+ /**
64
+ * The reviewer to send, given whatever a caller asked for.
65
+ *
66
+ * THE FIELD IS ALWAYS SENT, AND THAT IS THE POINT. It was absent before, so
67
+ * the value came from a top-level `approvals_reviewer` in whichever Codex home
68
+ * billed the run. Measured 2026-10-09 on codex-cli 0.160.0 against a real
69
+ * app-server: with the field absent and that key set to `auto_review`, a
70
+ * worker asked to write outside its workspace had the escalation APPROVED by
71
+ * Codex's own subagent, wrote into the home directory, and reached three
72
+ * external sites -- with ZERO approval cards raised and no server request on
73
+ * the wire at all. The same run with `approvalsReviewer: 'user'` raised a card
74
+ * reading "The sandbox blocked this write outside the workspace. Do you
75
+ * approve...". The reproduction is `scripts/qa-codex-live.mjs approvals`.
76
+ *
77
+ * So a file nobody in this app reads was deciding what a worker may do. Naming
78
+ * the reviewer on every start takes that vote away, and `user` is the value,
79
+ * because "ask" in a mode's own sentence has to mean asking the person.
80
+ *
81
+ * THIS IS NOT A REVERSAL OF w-ca48e69535. That row put `auto_review` on `auto`
82
+ * to stop two logins behaving differently -- a home with no config sent asks to
83
+ * her inbox, a home with one did not. Naming the reviewer explicitly fixes that
84
+ * same inconsistency and fixes it in the direction the mode already promised.
85
+ * One mode, one behaviour, whichever account the run is on.
86
+ */
87
+ export function codexReviewer(asked = null) {
88
+ return CODEX_REVIEWERS.includes(asked) ? asked : ASKS_THE_PERSON;
89
+ }
90
+
40
91
  /**
41
92
  * THE PAIR EACH MODE REALLY IS. `sandbox` and `approvalPolicy` go straight into
42
93
  * `thread/start`, so this table is the whole of the mapping and there is no
@@ -46,6 +97,11 @@ export const CODEX_DEFAULT_MODE = 'auto';
46
97
  * the refusing, and `on-request` leaves the agent able to ASK to step outside
47
98
  * rather than simply failing. On `full-access` there is nothing left to ask
48
99
  * about, so it is `never`.
100
+ *
101
+ * NO ROW NAMES A REVIEWER, because all three get the same one and a per-row
102
+ * value is how the last hole stayed open: two of the three named none, fell
103
+ * through to her config, and behaved differently from the one that did.
104
+ * `codexPosture` supplies it, once, below.
49
105
  */
50
106
  export const CODEX_MODES = {
51
107
  'read-only': {
@@ -59,13 +115,7 @@ export const CODEX_MODES = {
59
115
  label: 'Auto',
60
116
  sandbox: 'workspace-write',
61
117
  approvalPolicy: 'on-request',
62
- // WHO ANSWERS THOSE ASKS, SAID HERE RATHER THAN LEFT TO A FILE. This used
63
- // to come only from `approvals_reviewer` in her `~/.codex/config.toml`, so
64
- // a second login's home, which starts with no config at all, sent every ask
65
- // to her inbox instead (2026-09-24, w-ca48e69535). One mode, one behaviour,
66
- // whichever account the run is on.
67
- approvalsReviewer: 'auto_review',
68
- what: 'Works inside the project folder. Asks before going outside it or onto the network.',
118
+ what: 'Works inside the project folder. Asks you before going outside it or onto the network.',
69
119
  },
70
120
  'full-access': {
71
121
  label: 'Full access',
@@ -87,11 +137,17 @@ export function isCodexMode(word) {
87
137
  * start one. The writers (main/settings.mjs) validate on the way IN, which is
88
138
  * where a typo can still be reported to the person who made it.
89
139
  */
90
- export function codexPosture(mode) {
140
+ export function codexPosture(mode, { reviewer = null } = {}) {
91
141
  const row = CODEX_MODES[mode] ?? CODEX_MODES[CODEX_DEFAULT_MODE];
92
- const posture = { sandbox: row.sandbox, approvalPolicy: row.approvalPolicy };
93
- if (row.approvalsReviewer) posture.approvalsReviewer = row.approvalsReviewer;
94
- return posture;
142
+ // The reviewer is named on EVERY mode, `full-access` included. Its policy is
143
+ // `never` so it changes no behaviour there today, and that is exactly why it
144
+ // is named: one answer in the table rather than two, so a later change to the
145
+ // policy cannot silently hand the decision back to a config file.
146
+ return {
147
+ sandbox: row.sandbox,
148
+ approvalPolicy: row.approvalPolicy,
149
+ approvalsReviewer: codexReviewer(reviewer),
150
+ };
95
151
  }
96
152
 
97
153
  /** The label for a mode, for any surface that has to name one. */