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
@@ -1,120 +1,42 @@
1
- // A CODEX WORKER'S GATED COMMAND AND ITS PATCH, ON THE CARD SHE ALREADY KNOWS.
1
+ // The approval handler for Codex workers. It writes each card into the same
2
+ // spool as Claude Code's (main/approvals.mjs), so the renderer draws one card
3
+ // format for both engines, and a file change goes on the same card as a command.
2
4
  //
3
- // main/codex-app-server.mjs raises a server request to a callback and answers
4
- // whatever that callback returns, with a fifteen-minute deadline and a deny on
5
- // every failure path. It shipped with nobody on the other end and said so:
6
- // "the slice that builds the card is the slice that passes a handler". This is
7
- // the handler, and it is joined to the spool Agentbox already has rather than to
8
- // a second one -- main/approvals.mjs writes and reads
9
- // `<storeRoot>/.approvals/<uuid>.request.json`, main/ipc.mjs answers it, and
10
- // renderer/src/App.tsx draws the card's `<pre>` off `input` without knowing
11
- // which engine asked. A second card format would be a second thing to keep
12
- // true, so a file change goes on the SAME card as a command.
5
+ // The founder's answer never comes off the disk on this path. It arrives
6
+ // through main/ipc.mjs and reaches `settle` in process. The spool is inside the
7
+ // store root, which workers can write, so any `<id>.answer.json` that appears
8
+ // for a waiting request is a forgery, and the poll deletes it.
13
9
  //
14
- // ------------------------------------------------------------------------
15
- // THE ANSWER DOES NOT COME OFF THE DISK, AND THAT IS THE WHOLE SECURITY
16
- // ARGUMENT OF THIS FILE.
17
- //
18
- // The spool sits INSIDE the store root, which workers are given. A Claude Code
19
- // worker is granted it outright (`writeWorkerSettings` puts the store root in
20
- // `additionalDirectories`, and `worker-permissions.json` allows Bash broadly),
21
- // so on that engine a worker able to run a shell could in principle write
22
- // `<uuid>.answer.json` and approve itself. That path has no alternative: the
23
- // thing waiting for the answer is a separate process
24
- // (main/approval-prompt-server.mjs) and a file is the only channel it has.
25
- //
26
- // HERE THE THING WAITING IS AGENTBOX ITSELF, so it does not need one. The
27
- // founder's decision arrives through main/ipc.mjs's single answering door and
28
- // is handed to `settle` in process; nothing on this path ever reads an answer
29
- // file. A `<id>.answer.json` that appears for a request still waiting was
30
- // therefore not written by her, and the poll below DELETES it -- so a forgery
31
- // is not a weaker approval, it is not an approval, and it does not even leave
32
- // her card hidden.
33
- //
34
- // What the sandbox contributes, measured on this Mac 2026-09-04 against
35
- // codex-cli 0.148.0 by running `command/exec` under `sandboxPolicy: {type:
36
- // "workspaceWrite", writableRoots: []}` with the cwd set to a product folder
37
- // inside a store root under $HOME:
38
- //
39
- // write inside the cwd exit 0
40
- // write <storeRoot>/.approvals/x.answer.json "Operation not permitted"
41
- // write <storeRoot>/poke.txt "Operation not permitted"
42
- // write under $TMPDIR exit 0
43
- //
44
- // So a Codex worker's own commands cannot reach the spool -- with the caveat on
45
- // the last line, which is real: `workspace-write` leaves /tmp and $TMPDIR
46
- // writable (`excludeSlashTmp` and `excludeTmpdirEnvVar` both default to false),
47
- // so a store root under either of those would be reachable. Hers is `~/Zero`.
48
- // The first run of that probe put its fixture store in `os.tmpdir` and every
49
- // write "succeeded", which is the false negative worth remembering.
50
- //
51
- // That residue is closed as of 2026-09-05 and is written here so nobody
52
- // reintroduces it. It used to read: between a forged write and the poll that
53
- // removes it, `listPending` hides that card, because hiding a request that has
54
- // an answer file is main/approvals.mjs's rule and it is shared with the Claude
55
- // path. That shared rule now checks the founder's signature, so an answer file
56
- // she did not write hides nothing on either engine -- and the Claude path,
57
- // where the answer really does come off the disk, stopped believing one at the
58
- // same time. main/approvals.mjs holds that argument and what it does not close.
10
+ // Codex's `workspace-write` sandbox stops a worker's commands from writing to
11
+ // the store root, but it leaves /tmp and $TMPDIR writable, so a store root under
12
+ // either is reachable. main/approvals.mjs explains why a forged answer file
13
+ // still hides no card.
59
14
 
60
15
  import crypto from 'node:crypto';
61
16
  import fs from 'node:fs';
62
17
  import { appendSpoolLine, cardDigest, removeSpoolEntry, spoolDir, spoolEntryExists, writeSpoolFile } from './approvals.mjs';
63
18
 
64
- // Her deadline and her refusal word, both taken from the transport rather than
65
- // written again: it closes its own card on the same clock, and a second copy of
66
- // fifteen minutes would be a second chance to disagree about how long she has.
67
- import { APPROVAL_TIMEOUT_MS, REFUSAL_DECISION } from './codex-app-server.mjs';
19
+ import { REFUSAL_DECISION } from './codex-app-server.mjs';
68
20
 
69
- // The four words a file change is described by, taken from the reader that
70
- // already writes them into her trace rather than written a second time here.
71
21
  import { fileVerbWord } from './codex.mjs';
72
22
  import { Name, isOurSlug } from '../shared/product-name.mjs';
73
23
 
74
- /**
75
- * How often a waiting card looks for a file nobody should have written. The
76
- * same beat main/approval-prompt-server.mjs polls its answers on. */
24
+ /** How often a waiting card sweeps a forged answer and puts itself back on screen. */
77
25
  export const POLL_MS = 2_000;
78
26
 
79
- /** The word that lets her yes through. */
80
27
  const ACCEPT_DECISION = 'accept';
81
28
 
82
29
  /**
83
- * HOW MUCH OF WHAT SHE IS APPROVING GOES ON THE CARD, AND THE ONLY PLACE IT IS
84
- * DECIDED. One number for both shapes, because one card draws both.
85
- *
86
- * The renderer used to slice this to 400 characters, silently, while nothing
87
- * capped it here -- so she read four hundred characters and authorised however
88
- * many there were. THE DISPLAY BOUNDARY AND THE ENFORCEMENT BOUNDARY MUST NOT
89
- * DIFFER, and on this engine that is not a corner case: Codex reads, searches
90
- * and edits BY RUNNING SHELL COMMANDS, so a long command is the ordinary shape
91
- * of the thing she is being asked about.
92
- *
93
- * Four thousand, which is comfortably above any real command (`sed -n
94
- * '1,200p' <path>` and a long `git log` filter are tens of characters, not
95
- * thousands) and short of a card that cannot be read at all. Past it the string
96
- * is cut ONCE, here, with a mark saying exactly how much went -- so the worst
97
- * case is a card that says out loud it is not the whole thing, rather than one
98
- * that looks complete and is not. The `<pre>` on the other end scrolls
99
- * (max-height 120px, overflow-y auto in renderer/src/styles.css), so everything
100
- * on the card really is reachable.
101
- *
102
- * A PATCH IS THE SHAPE THAT WILL REACH IT FIRST, and the trade-off is stated
103
- * rather than left to be found: the cut is taken off the END, so a change
104
- * touching more files than four thousand characters can hold loses the LAST
105
- * files named rather than the first. That is the same direction the command
106
- * path cuts in, and the mark says how much went either way.
30
+ * The most characters of a command or patch that go on a card, and the only
31
+ * place that limit is set. What the card shows must be what the answer
32
+ * authorises, so the renderer must not cut it again. Past the cap the end is
33
+ * cut once, with a mark saying how much went.
107
34
  */
108
35
  export const CARD_CAP = 4_000;
109
36
 
110
- /**
111
- * THE FOUR PROTOCOL WORDS `NetworkApprovalContext` CAN CARRY, and nothing else
112
- * is ever printed as one. Read off the CLI's own schema dump 2026-09-05,
113
- * `NetworkApprovalProtocol`.
114
- */
37
+ /** The protocol values in Codex's `NetworkApprovalProtocol` schema. Any other value is not printed. */
115
38
  const NETWORK_PROTOCOLS = ['http', 'https', 'socks5Tcp', 'socks5Udp'];
116
39
 
117
- /** The destination a network approval opens, as one line, or nothing. */
118
40
  function networkHost(context) {
119
41
  if (!context || typeof context !== 'object' || Array.isArray(context)) return '';
120
42
  const host = typeof context.host === 'string' ? context.host.trim() : '';
@@ -124,46 +46,30 @@ function networkHost(context) {
124
46
  }
125
47
 
126
48
  /**
127
- * THE MARK ON AN ELICITATION THAT MAKES IT A TOOL-CALL APPROVAL rather than a
128
- * form asking her to type something, and the only thing that is read to tell
129
- * the two apart. Measured on this Mac 2026-09-22, codex-cli 0.153.4: an MCP
130
- * tool call under `approvalPolicy: untrusted` arrives as
131
- * `mcpServer/elicitation/request` carrying `_meta.codex_approval_kind:
132
- * "mcp_tool_call"`, a `message` written by Codex naming the server and the
133
- * tool, and a `requestedSchema` with no properties at all.
49
+ * The `_meta.codex_approval_kind` value that marks an elicitation as an MCP
50
+ * tool-call approval rather than a form asking for typed input. Under
51
+ * `approvalPolicy: untrusted`, Codex sends every MCP tool call as
52
+ * `mcpServer/elicitation/request` with this mark and a `message` naming the
53
+ * server and tool.
134
54
  */
135
55
  export const MCP_TOOL_CALL_KIND = 'mcp_tool_call';
136
56
 
137
- /** True for the elicitation that is asking whether an MCP tool may run. */
138
57
  export function isMcpToolCall(params) {
139
58
  const meta = params?._meta;
140
59
  if (!meta || typeof meta !== 'object' || Array.isArray(meta)) return false;
141
60
  return meta.codex_approval_kind === MCP_TOOL_CALL_KIND;
142
61
  }
143
62
 
144
- /** The MCP server this elicitation came from, or ''. */
145
63
  export function elicitationServer(params) {
146
64
  return typeof params?.serverName === 'string' ? params.serverName.trim() : '';
147
65
  }
148
66
 
149
67
  /**
150
- * IS THIS ELICITATION OUR OWN STORE, ASKED SO THAT A RENAME CANNOT ANSWER NO.
151
- *
152
- * `storeServer` is the name this app wrote into THIS thread's config, so on a
153
- * thread that started today the two are the same string and this is an equality
154
- * test. It is not an equality test on a thread that started before the app was
155
- * renamed, and those are ordinary: a Codex thread is resumed for every reply
156
- * she writes, the resume is a deep merge over the thread's own config, and the
157
- * server the old thread registered under the old slug is still on it. The model
158
- * reads its own transcript, calls the tool by the name it called it by last
159
- * time, and the app then fails to recognise its own store and either asks her
160
- * to approve her own plumbing or refuses it.
161
- *
162
- * So the question is asked of the app's name history, which `WAS` already keeps
163
- * for the data folder and the env vars. NOTHING HERE WIDENS THE GRANT BEYOND
164
- * THIS APP: a name we have never had is not ours, a run with no store server is
165
- * not covered at all (see `ask`), and the only servers that can be on a worker
166
- * thread are the ones `mcpIsolation` put there.
68
+ * True when the elicitation comes from Agentbox's own store server. Any of the
69
+ * app's past names also counts, because resuming a Codex thread merges over its
70
+ * old config, so a thread started before a rename still calls the store by the
71
+ * old name. Only names this app has had match, and a run with no store server
72
+ * never matches.
167
73
  */
168
74
  export function isStoreElicitation(params, storeServer) {
169
75
  if (!storeServer) return false;
@@ -173,23 +79,13 @@ export function isStoreElicitation(params, storeServer) {
173
79
  }
174
80
 
175
81
  /**
176
- * ONE MCP TOOL CALL AS A CARD, drawn out of the elicitation itself.
177
- *
178
- * There is no itemId on an elicitation and so no `seen` memory to draw it from:
179
- * what it carries is the server's name, Codex's own sentence naming the tool,
180
- * and the arguments. Those three are the question, and the arguments are the
181
- * half that matters, because `claim_work_item` and `write_document` are the
182
- * same shape of call and two very different things to allow.
183
- *
184
- * NOTHING IS INVENTED WHEN THE SENTENCE IS MISSING. A card that says only
185
- * "an MCP tool" is a card she cannot answer, so this returns null and `ask`
186
- * refuses out loud, which is the rule the rest of this file already follows.
82
+ * One MCP tool call as a card: the server, Codex's sentence naming the tool,
83
+ * and the arguments. Returns null when Codex sent no sentence, so `ask` refuses
84
+ * rather than drawing a card nobody can answer.
187
85
  */
188
86
  export function mcpToolCard(params) {
189
- // ONLY THE APPROVAL, NEVER THE FORM. The same channel carries an MCP server's
190
- // own `elicitation/create`, which is a request for typed input against a real
191
- // `requestedSchema`. Drawing that as an allow-or-deny card would put a
192
- // question with no answer in front of her, so it is left to `ask` to refuse.
87
+ // The same channel carries an MCP server's own request for typed input, which
88
+ // an allow-or-deny card cannot answer, so `ask` refuses it.
193
89
  if (!isMcpToolCall(params)) return null;
194
90
  const server = elicitationServer(params);
195
91
  const said = typeof params?.message === 'string' ? params.message.trim() : '';
@@ -200,12 +96,11 @@ export function mcpToolCard(params) {
200
96
  if (args && typeof args === 'object' && Object.keys(args).length) {
201
97
  try {
202
98
  input.arguments = readable(JSON.stringify(args, null, 2));
203
- } catch { /* an unserialisable argument is left off rather than guessed at */ }
99
+ } catch { /* arguments that cannot be serialised are left off */ }
204
100
  }
205
101
  return { tool: 'Mcp', input };
206
102
  }
207
103
 
208
- /** Whole, or cut once with the size of the cut said out loud. */
209
104
  function readable(text) {
210
105
  if (text.length <= CARD_CAP) return text;
211
106
  const dropped = text.length - CARD_CAP;
@@ -213,24 +108,11 @@ function readable(text) {
213
108
  }
214
109
 
215
110
  /**
216
- * ONE `fileChange` ITEM AS THE CARD ITS APPROVAL WILL BE DRAWN AS, or null when
217
- * there is nothing in it that can be said out loud.
218
- *
219
- * The approval request itself carries none of this (see `codexApprovalCard`);
220
- * the ITEM does, and it goes past first. Every change in the item is on the
221
- * card, because one `apply_patch` can touch several files and she is being
222
- * asked about all of them at once -- measured: the first patch of the 09-04
223
- * turn carried an add and a delete together.
224
- *
225
- * A CREATION IS NOT A DIFF. `diff` is a unified diff for an `update` and RAW
226
- * FILE CONTENT for an `add`, so nothing here parses it -- it is her file,
227
- * printed under the name of her file, whichever of the two shapes it arrived
228
- * in.
229
- *
230
- * `tool` is the word on the card's hint line. One verb for a patch that did one
231
- * thing, and the class word for a patch that did several, because "Write" over
232
- * a card that also deletes something would be the card lying about half of
233
- * itself.
111
+ * One `fileChange` item as the card its approval will be drawn as, or null when
112
+ * it names no file. Every change in the item goes on the card, because one
113
+ * patch can touch several files. `diff` is a unified diff for an update but raw
114
+ * file content for an add, so it is printed as it came and never parsed.
115
+ * `tool` is the one verb when every change shares it, and `Edit` otherwise.
234
116
  */
235
117
  export function fileChangeCard(item) {
236
118
  const blocks = [];
@@ -242,9 +124,8 @@ export function fileChangeCard(item) {
242
124
  const head = word === 'Move'
243
125
  ? `Move ${from} -> ${String(change.kind.move_path).trim()}`
244
126
  : `${word} ${from}`;
245
- // A trailing newline is a line terminator, not an empty last line -- the
246
- // same rule `splitLines` states in main/codex.mjs, and ONE of them, so a
247
- // file that really does end in blank lines still shows them.
127
+ // Drop one trailing newline only, as `splitLines` in main/codex.mjs does, so
128
+ // a file that really ends in blank lines still shows them.
248
129
  const body = String(change.diff ?? '').replace(/\n$/, '');
249
130
  blocks.push(body ? `${head}\n${body}` : head);
250
131
  if (!words.includes(word)) words.push(word);
@@ -254,115 +135,31 @@ export function fileChangeCard(item) {
254
135
  }
255
136
 
256
137
  /**
257
- * HOW MANY OF A WORKER'S CHANGES ARE HELD WAITING FOR AN APPROVAL TO NAME THEM.
258
- *
259
- * Measured, an approval follows its own `item/started` immediately, so one
260
- * would do; this exists because "immediately" is an observation about 0.148.0
261
- * and not a promise, and a turn can have several patches in flight.
262
- *
263
- * It is a real bound and not a gesture, which is the lesson CHANGE_FRAME_CAP in
264
- * main/codex.mjs already records: a frame count is not a memory bound when each
265
- * frame can carry a whole file. What is held here is the CARD, already cut to
266
- * `CARD_CAP`, so the worst case is thirty-two times four thousand characters --
267
- * about 130 KB per worker, and nothing a run that edits in a loop can grow.
138
+ * How many file-change cards one worker holds while waiting for the approvals
139
+ * that name them. Codex sends the approval right after `item/started`, but that
140
+ * is observed, not promised, and a turn can have several patches in flight.
141
+ * Each card is already cut to `CARD_CAP`, so this bounds memory too.
268
142
  */
269
143
  export const REMEMBERED_CHANGES = 32;
270
144
 
271
145
  /**
272
- * ONE APPROVAL REQUEST AS A CARD, or nothing.
273
- *
274
- * `Bash` and `command` are not a new vocabulary invented for this engine: they
275
- * are the two keys renderer/src/App.tsx already reads off `input`, and Codex
276
- * really does read, search and edit by running shell commands, so the honest
277
- * spelling is also the matching one. `reason` becomes `description`, which is
278
- * the line the card draws above the command.
146
+ * One approval request as a card, or null, which `ask` refuses. `Bash` and
147
+ * `command` are the keys renderer/src/App.tsx already reads off `input`.
279
148
  *
280
- * ------------------------------------------------------------------------
281
- * A PATCH IS CARDED FROM THE ITEM, NOT FROM THE APPROVAL, AND THAT IS WHAT
282
- * LETS A CODEX WORKER WRITE A FILE AT ALL.
149
+ * A file-change approval carries no path and no diff. Codex sends the same
150
+ * item on `item/started` before the approval, so the card comes from `seen`,
151
+ * which `remember` fills per worker. An approval for an item this worker never
152
+ * saw is refused, so the founder never answers a card she cannot read.
283
153
  *
284
- * `FileChangeRequestApprovalParams` carries itemId, threadId, turnId,
285
- * startedAtMs, an optional reason and an optional `grantRoot`. No path, no
286
- * diff, no sentence -- measured in full on 2026-09-04:
154
+ * `item/permissions/requestApproval` is always refused, because its params say
155
+ * nothing about what would be allowed.
287
156
  *
288
- * {"threadId":"...","turnId":"...","itemId":"call_eBjH...",
289
- * "startedAtMs":1788504140524,"reason":null,"grantRoot":null}
157
+ * A command approval shows every entry of `commandActions`, not the first, and
158
+ * the network host it would open, so the card shows all that a yes allows.
290
159
  *
291
- * So this file used to refuse it, correctly on its own terms and at a price
292
- * that turned out to be the whole integration. `WORKER_APPROVAL_POLICY` is
293
- * `untrusted` (main/codex-session.mjs), which is the only policy that cards
294
- * risky commands, and it also cards every patch -- so a refusal here was a
295
- * worker that could not write one file. Measured, cwd a fresh git repo,
296
- * sandbox `workspace-write`:
297
- *
298
- * untrusted risky commands CARDED, in-workspace patch raised the
299
- * undrawable fileChange -> refused -> nothing written
300
- * on-request commands NOT carded, patch ran unasked (so did a `curl` to
301
- * the open internet on a second probe)
302
- * granular commands NOT carded, patch ran unasked
303
- *
304
- * THE CARD CAN BE DRAWN HONESTLY AFTER ALL. `item/started` for the same
305
- * fileChange arrives BEFORE the approval and carries the real content --
306
- * three approvals out of three in the measured turn, `item/completed` only
307
- * ever AFTER the answer:
308
- *
309
- * item/started fileChange id=call_ylWd... changes=2
310
- * APPROVAL item/fileChange/requestApproval itemId=call_ylWd...
311
- * item/completed fileChange id=call_ylWd... changes=2
312
- *
313
- * `seen` is that memory, one per worker (see `scope` below), and the card is
314
- * the one `fileChangeCard` already built when the item went past.
315
- *
316
- * AN ITEM THIS WORKER NEVER SAW IS STILL REFUSED, and that is the line that
317
- * must not move: a card she answers blind is the 2026-08-05 flood again, so an
318
- * approval naming an unknown change gets null and the transport's deny, exactly
319
- * as before. Refusing is cheap and visible -- it goes to the worker's own
320
- * stderr, so `troubleCause` and the run trace both carry it.
321
- *
322
- * `item/permissions/requestApproval` IS STILL REFUSED OUTRIGHT, and there is no
323
- * item behind it to rescue it: it is answered with a granted-permissions
324
- * profile rather than a decision, and its params carry nothing that could be
325
- * put in front of her as a sentence saying what she is allowing.
326
- *
327
- * A command approval WITH NO COMMAND is NOT the same judgement any more, and
328
- * that is the correction. The schema makes `command` nullable, and this used to
329
- * look at the first parsed action and then refuse -- while the same params
330
- * carried `networkApprovalContext`, which names the host the sandbox would be
331
- * opened to. Refusing something we could have described honestly is the
332
- * opposite failure to carding something we could not, and it is worse: it
333
- * denies work she would have allowed and tells her nothing about why.
334
- *
335
- * AND THE NETWORK GRANT GOES ON THE CARD BESIDE THE COMMAND, WHICH IS THE HALF
336
- * THAT WAS SILENTLY MISSING. `curl` and the host it reaches are two facts and
337
- * the card drew one of them, so she could authorise a displayed command without
338
- * seeing the destination it opens. Measured from the CLI's own schema dump on
339
- * this Mac 2026-09-05 (`codex app-server generate-json-schema --out <dir>`,
340
- * codex-cli 0.148.0), CommandExecutionRequestApprovalParams:
341
- *
342
- * networkApprovalContext { host: string,
343
- * protocol: "http"|"https"|"socks5Tcp"|"socks5Udp" }
344
- * | null "Optional context for a managed-network
345
- * approval prompt."
346
- * commandActions CommandAction[] | null, every variant carrying its
347
- * own `command` string -- "Best-effort parsed command
348
- * actions for friendly display."
349
- *
350
- * `commandActions` IS READ IN FULL RATHER THAN AT ITS FIRST ENTRY, for the
351
- * reason `readable` exists at all: the display boundary and the enforcement
352
- * boundary must not differ, and `[cat auth.json, curl -T - elsewhere]` shown as
353
- * its first line is a card that is true about a tenth of what it authorises.
354
- *
355
- * THE PROTOCOL IS PRINTED ONLY WHEN IT IS ONE OF THE FOUR WORDS THE SCHEMA
356
- * ENUMERATES. A release that adds a fifth would otherwise put an unvouched-for
357
- * word under a question about her machine; the host is the fact either way, so
358
- * the host is said either way.
359
- *
360
- * WHAT AN ALLOW IS STILL NOT. `CommandExecutionApprovalDecision` also holds
361
- * `acceptWithExecpolicyAmendment` and `applyNetworkPolicyAmendment`, which
362
- * write a PERSISTENT rule for a command shape or a host, and the params carry
363
- * `proposedExecpolicyAmendment` and `proposedNetworkPolicyAmendments` offering
364
- * them. This file answers `accept` or `decline` and nothing else, so one card is
365
- * one request and a yes she gave to one command never becomes a standing grant.
160
+ * Codex also offers decisions that write a lasting rule for a command shape or
161
+ * a host. This app only ever answers `accept` or `decline`, so a yes to one
162
+ * command never becomes a standing grant.
366
163
  */
367
164
  export function codexApprovalCard(method, params, seen = null) {
368
165
  const p = params ?? {};
@@ -376,12 +173,9 @@ export function codexApprovalCard(method, params, seen = null) {
376
173
  const input = { ...known.input };
377
174
  const why = typeof p.reason === 'string' ? p.reason.trim() : '';
378
175
  if (why) input.description = why;
379
- // THE TREE THE PATCH ASKS TO OPEN, WHICH IS NOT IN THE DIFF ABOVE IT.
380
- // `grantRoot`, from the same schema dump: "[UNSTABLE] When set, the agent is
381
- // asking the user to allow writes under this root for the remainder of the
382
- // session." So an accept on one visible file can be read as opening a whole
383
- // tree until the run ends, and the card drew the file. Nearly every patch
384
- // sends null and then nothing is added, exactly as `cwd` behaves.
176
+ // Codex's schema says a set `grantRoot` asks to allow writes under that root
177
+ // for the rest of the session, so an accept can open more than the diff
178
+ // shows, and the card must say so.
385
179
  const root = typeof p.grantRoot === 'string' ? p.grantRoot.trim() : '';
386
180
  if (root) input.grantRoot = root;
387
181
  return { tool: known.tool, input };
@@ -398,14 +192,8 @@ export function codexApprovalCard(method, params, seen = null) {
398
192
  const input = {};
399
193
  if (command) input.command = readable(command);
400
194
  if (host) input.host = host;
401
- // WHERE IT WOULD RUN, WHICH IS HALF OF WHAT THE COMMAND MEANS. `cat
402
- // auth.json` in a scratch clone and `cat auth.json` in her home directory are
403
- // the same eleven characters and two different questions.
404
- // `CommandExecutionRequestApprovalParams` has carried `cwd` all along -- "The
405
- // command's working directory", read out of the CLI's own
406
- // `generate-json-schema` dump on 2026-09-04 -- and nothing was reading it.
407
- // Left OFF entirely when the server did not say one, because a card drawing
408
- // the word "undefined" under a command is worse than a card drawing nothing.
195
+ // The same command means different things in different folders, so the card
196
+ // shows where it would run when Codex says.
409
197
  const cwd = typeof p.cwd === 'string' ? p.cwd.trim() : '';
410
198
  if (cwd) input.cwd = cwd;
411
199
  const reason = typeof p.reason === 'string' ? p.reason.trim() : '';
@@ -414,59 +202,39 @@ export function codexApprovalCard(method, params, seen = null) {
414
202
  }
415
203
 
416
204
  /**
417
- * THE SPOOL, FROM AGENTBOX'S SIDE OF IT.
418
- *
419
- * One of these per supervisor, shared by every Codex worker on the machine:
420
- * the request ids are UUIDs and the founder answers them one at a time, so
421
- * there is nothing per-worker to keep apart.
205
+ * The Codex side of the approval spool, one per supervisor and shared by every
206
+ * Codex worker. `uuid`, `now` and `pollMs` are injectable for tests.
422
207
  *
423
- * `uuid` and `now` are injected so a test can name a card, and `timeoutMs` and
424
- * `pollMs` so it does not have to wait a quarter of an hour to watch a silence
425
- * deny itself.
208
+ * A CARD WAITS FOR THE PERSON IT ASKS, AND NO CLOCK ANSWERS FOR THEM
209
+ * (w-4b00022acb). There was a fifteen-minute deadline here, and what it sent
210
+ * at the end of it was a deny the turn could not tell from theirs: work nobody
211
+ * had refused came back refused. It was safe only while a waiting card was
212
+ * invisible, and it is not now, because the row reads Asking and sits in Needs
213
+ * you (w-fe48447cab). So what settles a card is always an event: their answer,
214
+ * the worker stopping (`close` on its scope), the app shutting down, the
215
+ * connection breaking.
426
216
  */
427
217
  export function createCodexApprovals({
428
218
  storeRoot,
429
- timeoutMs = APPROVAL_TIMEOUT_MS,
430
219
  pollMs = POLL_MS,
431
220
  uuid = () => crypto.randomUUID(),
432
221
  now = () => Date.now(),
433
222
  onChange = null,
434
223
  } = {}) {
435
224
  const dir = spoolDir(storeRoot);
436
- const open = new Map(); // id -> { request, scope, decide, deadline, poll }
225
+ const open = new Map(); // id -> { request, scope, decide, poll }
437
226
 
438
227
  /**
439
- * Ask her one question, and never come back with nothing.
440
- *
441
- * The promise this returns is the one main/codex-app-server.mjs is holding
442
- * the server request open on. It resolves with a word the transport can
443
- * translate, or with the refusal; it never rejects and it never hangs,
444
- * because a promise nobody settles is a worker parked with no exit code and
445
- * nothing on the row, which is the failure that file was written around.
228
+ * The promise main/codex-app-server.mjs holds the server request open on.
229
+ * It always resolves, with accept or the refusal, and never rejects or hangs,
230
+ * because an unsettled promise parks the worker forever.
446
231
  */
447
232
  const ask = (context, method, params, seen = null) => new Promise((resolve) => {
448
- // AGENTBOX'S OWN STORE TOOLS ARE NOT A QUESTION FOR THE FOUNDER, AND ASKING
449
- // HER ONE FOR EACH OF THEM WOULD BE THE SAME FAILURE AS REFUSING THEM.
450
- //
451
- // A Codex worker reaches the store the only way this engine has: an MCP tool
452
- // call, which under `untrusted` raises the elicitation above. Claiming a
453
- // row, writing a checkpoint and finishing one are therefore three cards she
454
- // would have to press before a worker could say anything at all, on every
455
- // run, and that is the plumbing rather than the work. The Claude Code path
456
- // says the same thing in its own grammar and has all along: the store
457
- // server's tools are granted outright in `worker-permissions.json` and never
458
- // reach her spool.
459
- //
460
- // THE NAME IS THE ONE AGENTBOX LAUNCHED, not one a worker can claim. It comes
461
- // from `_codexStoreServer` through `scope`, and the only servers on a worker
462
- // thread are the ones `mcpIsolation` wrote into `thread/start` -- everything
463
- // in her own config is switched off there by name. So a server answering to
464
- // this name is the one this app started, with the account id, the product
465
- // and the work item already fixed in its environment.
466
- //
467
- // EVERY OTHER MCP TOOL CALL STILL GOES TO HER, as a card drawn by
468
- // `mcpToolCard`, and a personal session (which is given no store server at
469
- // all) has nothing that takes this path.
233
+ // Calls to Agentbox's own store server are allowed without a card, as the
234
+ // Claude Code path allows them in `worker-permissions.json`. The server name
235
+ // comes from the app through `scope`, not from the worker, and
236
+ // `mcpIsolation` makes sure the only MCP servers on a worker thread are ours.
237
+ // Every other MCP tool call still goes to the founder as a card.
470
238
  if (method === 'mcpServer/elicitation/request'
471
239
  && isMcpToolCall(params)
472
240
  && isStoreElicitation(params, context?.storeServer)) {
@@ -476,24 +244,10 @@ export function createCodexApprovals({
476
244
 
477
245
  const card = codexApprovalCard(method, params, seen);
478
246
  if (!card) {
479
- // VISIBLE ON THE RUN, NOT ONLY IN A LOG SHE WILL NEVER OPEN.
480
- //
481
- // Anything that cannot be described is refused, and that is right rather
482
- // than lazy: a card she answers blind is the 2026-08-05 flood again. But
483
- // the only signal used to be a `console.warn`, which reaches no screen, no
484
- // row and no trace, so the symptom of a worker being refused every edit it
485
- // tried was a run that talked and changed nothing.
486
- //
487
- // THE TWO REASONS ARE NOT THE SAME REASON and the line says which, because
488
- // one of them is a live defect and the other is a decision. A `permissions`
489
- // request carries nothing describable at all. A `fileChange` naming an item
490
- // this worker never saw means the memory the card is drawn from missed it,
491
- // which is something to go and look at (see `remember` below).
492
- //
493
- // `context.say` is the worker's own stderr emitter, which is the pipe
494
- // `troubleCause` classifies a dead run from and the one the persisted
495
- // trace records. The console line stays for a scope nobody wired a `say`
496
- // to, which is a test and never a worker.
247
+ // Anything that cannot be described is refused. The refusal goes to the
248
+ // worker's own stderr through `context.say`, so `troubleCause` and the run
249
+ // trace both show it, and it names its reason. An unseen file change
250
+ // means `remember` missed an item, which is a bug to look into.
497
251
  const why = method === 'item/fileChange/requestApproval'
498
252
  ? 'this run never saw the file change it names, so there is nothing it could put in front of the founder to answer'
499
253
  : method === 'mcpServer/elicitation/request'
@@ -516,11 +270,9 @@ export function createCodexApprovals({
516
270
  input: card.input,
517
271
  };
518
272
 
519
- // EVERY TOUCH OF THE SPOOL GOES THROUGH main/approvals.mjs, because that
520
- // directory is worker-writable and the rule about what may be opened in it
521
- // belongs in one place. `writeSpoolFile` creates at a name nothing can
522
- // guess and renames onto the real one, so nothing here can be pre-created
523
- // or followed.
273
+ // The spool is worker-writable, so every file operation on it goes through
274
+ // main/approvals.mjs, the one place that guards against planted files and
275
+ // links.
524
276
  let put = false;
525
277
  try {
526
278
  fs.mkdirSync(dir, { recursive: true });
@@ -529,58 +281,68 @@ export function createCodexApprovals({
529
281
  console.warn('zero: could not put a codex approval in the spool:', err?.message ?? err);
530
282
  }
531
283
  if (!put) {
532
- // A question that cannot be asked has already been answered.
533
284
  resolve(REFUSAL_DECISION);
534
285
  return;
535
286
  }
536
287
 
537
288
  const entry = {
538
289
  request,
539
- // WHICH WORKER ASKED. A card is a question one run is parked on, so it
540
- // has no meaning once that run has stopped -- see `scope` below.
290
+ // The worker that asked, so `close` can deny its cards when it stops.
541
291
  scope: context?.scope ?? null,
542
292
  decide(allow, note, extra) {
543
293
  if (!open.delete(id)) return false;
544
- clearTimeout(entry.deadline);
545
294
  clearInterval(entry.poll);
546
295
  finish(id, request, { allow: !!allow, note: note ?? null, at: now(), ...extra });
547
296
  resolve(allow ? ACCEPT_DECISION : REFUSAL_DECISION);
548
297
  return true;
549
298
  },
550
299
  };
551
- entry.deadline = setTimeout(() => entry.decide(false, null, { timeout: true }), timeoutMs);
552
- entry.deadline?.unref?.();
553
- // NOBODY SHOULD EVER WRITE THIS FILE. Her answer comes in through `settle`,
554
- // so one on the disk is a worker's, and it is removed rather than read.
555
- entry.poll = setInterval(() => sweepForgery(id), pollMs);
300
+ // The real answer comes through `settle`, so an answer file is a worker's
301
+ // forgery and is deleted unread. The same beat puts the request back if
302
+ // anything took it out of the spool while this card was still waiting.
303
+ entry.poll = setInterval(() => tend(id), pollMs);
556
304
  entry.poll?.unref?.();
557
305
  open.set(id, entry);
558
306
  onChange?.();
559
307
  });
560
308
 
561
- function sweepForgery(id) {
562
- if (!open.has(id)) return;
309
+ /**
310
+ * One beat of a waiting card: sweep an answer nobody signed, and put the
311
+ * request back on screen if it has gone.
312
+ *
313
+ * THE SPOOL REAPS WHAT NOBODY IS WAITING ON, AND THIS IS HOW IT KNOWS.
314
+ * `listPending` cannot read this map, and a card that waits for a person
315
+ * outlives its REQUEST_TTL_MS, so whoever is waiting keeps its own request
316
+ * there and the sweep only ever takes one nobody is holding up.
317
+ */
318
+ function tend(id) {
319
+ const entry = open.get(id);
320
+ if (!entry) return;
563
321
  const forged = `${id}.answer.json`;
564
- // `lstat`, not `existsSync`: a dangling link is an entry `readdir` can see,
565
- // so `listPending` hides her card for it while `existsSync` reports
566
- // nothing to sweep. And `rm`, not `unlink`, because a worker can squat the
567
- // name with a directory, which `unlink` refuses (EPERM, measured) and
568
- // which would then hide the card until the spool's own TTL.
569
- if (!spoolEntryExists(dir, forged)) return;
570
- removeSpoolEntry(dir, forged);
571
- console.warn(`zero: an answer file appeared for approval ${id} that the founder did not write; it has been deleted and the card is still waiting`);
572
- onChange?.();
322
+ // These helpers also catch a dangling link, which `existsSync` misses, and
323
+ // a directory squatting the name, which `unlink` cannot remove.
324
+ if (spoolEntryExists(dir, forged)) {
325
+ removeSpoolEntry(dir, forged);
326
+ console.warn(`zero: an answer file appeared for approval ${id} that the founder did not write; it has been deleted and the card is still waiting`);
327
+ onChange?.();
328
+ }
329
+ const name = `${id}.request.json`;
330
+ if (spoolEntryExists(dir, name)) return;
331
+ // Written from the request this process has held all along, never read back
332
+ // off the disk: the card that comes back is the card that went.
333
+ if (writeSpoolFile(dir, name, JSON.stringify(entry.request))) { onChange?.(); return; }
334
+ // Failing here leaves the worker parked with nothing on screen, which is
335
+ // the one way the wait goes bad, so it is said out loud. Once per card:
336
+ // this beat runs every two seconds.
337
+ if (!entry.saidItCouldNotGoBack) {
338
+ entry.saidItCouldNotGoBack = true;
339
+ console.warn(`zero: approval ${id} left the spool and could not be put back, so the worker waiting on it has no card on screen`);
340
+ }
573
341
  }
574
342
 
575
- /**
576
- * One audit line per decision, and the pair leaves the spool. The same
577
- * ending main/approval-prompt-server.mjs gives a Claude Code worker's card,
578
- * because nothing else is going to reap these. */
343
+ /** Writes one audit line per decision and removes the request and answer files. */
579
344
  function finish(id, request, decided) {
580
- // Never held hostage to its own log line, which is why the return is
581
- // ignored -- and why `appendSpoolLine` destroys a planted link rather than
582
- // only refusing to follow one: a silent refusal here would hand a worker
583
- // the audit trail for good.
345
+ // A failed log write must not block the decision, so the return is ignored.
584
346
  appendSpoolLine(dir, 'log.jsonl', `${JSON.stringify({ ...request, answer: decided, decidedAt: now() })}\n`);
585
347
  for (const f of [`${id}.request.json`, `${id}.answer.json`]) removeSpoolEntry(dir, f);
586
348
  onChange?.();
@@ -590,48 +352,22 @@ export function createCodexApprovals({
590
352
 
591
353
  return {
592
354
  /**
593
- * ONE WORKER'S CARDS, AND A WAY TO END THEM WITH IT.
594
- *
595
- * Every Codex worker used to raise into one fleet-wide map with nothing
596
- * per-worker in it, so killing a worker ended its facade and interrupted its
597
- * turn and did NOT deny or remove its card. A stopped, preempted, archived
598
- * or app-server-lost worker left a live-looking question on screen for the
599
- * full fifteen minutes.
600
- *
601
- * That is not a card that merely lingers. `listPending` sorts oldest first
602
- * and every answering route acts on `listPending[0]`, so the DEAD card sits
603
- * in front of every real one until its deadline: she presses ⌘Y for the
604
- * card she is reading and answers a worker that stopped ten minutes ago.
605
- *
606
- * `close` is what the worker's own exit calls. It denies this scope's open
607
- * cards, once each, through the same `decide` her answer goes through -- so
608
- * the audit line is written, the request file leaves the spool, and the
609
- * promise the transport is holding the server request open on resolves with
610
- * the refusal rather than being dropped.
355
+ * One worker's cards. The worker's exit must call `close`, which denies its
356
+ * open cards through `decide`. Otherwise a dead worker's card sits first in
357
+ * `listPending`, and the founder's next answer goes to it instead of the card
358
+ * she is reading.
611
359
  */
612
360
  scope: (context = {}) => {
613
361
  const id = `scope-${scopes += 1}`;
614
362
  const bound = { ...context, scope: id };
615
- // WHAT THIS WORKER HAS CHANGED, WAITING FOR THE APPROVAL THAT NAMES IT.
616
- // Per worker and not per fleet, because an `itemId` is the app-server's
617
- // own string and one process carries every Codex thread on the Mac: a
618
- // shared memory would let thread A's patch answer thread B's question.
619
- // It goes when the worker does, which is what `close` is for.
363
+ // Kept per worker because item ids are only unique within one thread, and
364
+ // a shared map would let one thread's patch answer another's approval.
620
365
  const seen = new Map(); // itemId -> the card its approval will be drawn as
621
366
  return {
622
367
  /**
623
- * ONE NOTIFICATION, LOOKED AT ONLY FOR THE CHANGE IT MIGHT CARRY.
624
- *
625
- * `item/started` and nothing else, because that is the event measured to
626
- * arrive BEFORE the approval; `item/completed` carries the same changes
627
- * and arrives only after the answer, so a memory fed from it would
628
- * always be one beat too late and would hold changes nothing will ask
629
- * about again.
630
- *
631
- * Called on the session's own event path, so it is written to be unable
632
- * to throw there: a reader that throws inside the supervisor's stream
633
- * loop takes itself out for the rest of the session, which is a worker
634
- * that keeps running and reports nothing.
368
+ * Remembers the card for each file change on `item/started`, which
369
+ * arrives before the approval. `item/completed` arrives only after it.
370
+ * Must never throw, because it runs inside the supervisor's stream loop.
635
371
  */
636
372
  remember: (method, params) => {
637
373
  try {
@@ -641,8 +377,8 @@ export function createCodexApprovals({
641
377
  const card = fileChangeCard(item);
642
378
  if (!card) return;
643
379
  seen.set(item.id, card);
644
- // Oldest out first. A Map keeps insertion order and re-setting a key
645
- // does not move it, so this is the first change this worker made.
380
+ // Drops the oldest. A Map keeps insertion order, so the first key is
381
+ // the oldest change.
646
382
  while (seen.size > REMEMBERED_CHANGES) seen.delete(seen.keys().next().value);
647
383
  } catch { /* a card is never worth a session */ }
648
384
  },
@@ -660,38 +396,19 @@ export function createCodexApprovals({
660
396
  },
661
397
 
662
398
  /**
663
- * THE FOUNDER ANSWERED, and this is the only thing in the app that can say
664
- * so. Called by main/ipc.mjs beside `approvals.answer`, in the same
665
- * synchronous breath, so the two engines answer through one door.
666
- *
667
- * False for anything that is not an open Codex card: a Claude Code
668
- * worker's request, or one this deadline already denied. Her late click on
669
- * a card the clock closed twenty minutes ago must not approve a command
670
- * the turn stopped waiting for.
399
+ * The founder's answer, and the only way a Codex card is approved. Called by
400
+ * main/ipc.mjs beside `approvals.answer`. Returns false for anything that is
401
+ * not an open Codex card, so a late click on a timed-out card approves nothing.
671
402
  */
672
403
  settle: (id, allow, note) => open.get(String(id))?.decide(allow, note) ?? false,
673
404
 
674
405
  /**
675
- * WHETHER THE CARD SHE READ IS STILL THE QUESTION THIS TURN IS PARKED ON.
676
- *
677
- * The answer does not come off the disk here -- that is the argument at the
678
- * top of this file -- but the CARD does: it is drawn out of the same
679
- * worker-writable spool `listPending` reads, and a Claude Code worker on the
680
- * same Mac is granted that directory outright. So a Codex worker's card can
681
- * be rewritten under it by the other engine, and `settle` would honour her
682
- * Allow against the request still sitting in `open`, which is the one she
683
- * never saw.
684
- *
685
- * `shown` is `approvals.cardShown`, the digest of what was last handed to
686
- * her screen. This compares it against the request Agentbox itself built and
687
- * has held in memory ever since, so nothing in the comparison has been off
688
- * the disk twice. A digest that cannot be taken counts as changed, because
689
- * the safe answer is the one that has to be the accident.
690
- *
691
- * FALSE FOR AN ID THIS ENGINE HAS NEVER HEARD OF, which is every Claude Code
692
- * card and every card the deadline already closed. The door asks this about
693
- * both engines' cards, and turning her Allow on a Claude Code card into a
694
- * deny would be this method inventing a refusal she never got.
406
+ * True when the card the founder saw differs from the request held in
407
+ * memory. The renderer draws the card from the worker-writable spool, so a
408
+ * worker could rewrite it. `shown` is `approvals.cardShown`. A digest that
409
+ * cannot be taken counts as changed. Returns false for ids this engine does
410
+ * not hold, such as Claude Code cards, so it never turns their answer into a
411
+ * deny.
695
412
  */
696
413
  changed: (id, shown) => {
697
414
  const entry = open.get(String(id));
@@ -700,9 +417,7 @@ export function createCodexApprovals({
700
417
  return !mine || mine !== shown;
701
418
  },
702
419
 
703
- /**
704
- * Everything still waiting is denied, once. Agentbox is going away and a
705
- * worker holding an unanswerable question is a worker holding forever. */
420
+ /** Denies every open card, for app shutdown. */
706
421
  close: (why = `${Name} is shutting down`) => {
707
422
  for (const entry of [...open.values()]) entry.decide(false, why, { closed: true });
708
423
  },