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.
- package/README.md +24 -9
- package/bin/agentbox.mjs +23 -1
- package/briefs/message-rules.md +36 -8
- package/briefs/worker.md +46 -23
- package/main/account-discovery.mjs +122 -30
- package/main/account-tooling.mjs +29 -1
- package/main/account-usage.mjs +46 -0
- package/main/agent-sessions.mjs +282 -268
- package/main/agents.mjs +179 -466
- package/main/analytics.mjs +12 -2
- package/main/answer-modes.mjs +103 -37
- package/main/approval-prompt-core.mjs +197 -0
- package/main/approval-prompt-server.mjs +4 -138
- package/main/approvals.mjs +101 -297
- package/main/artifact-path.mjs +47 -18
- package/main/claude-input.mjs +48 -4
- package/main/claude-plan.mjs +48 -0
- package/main/claude-session-open.mjs +28 -0
- package/main/claude-usage-process.mjs +40 -0
- package/main/claude-usage.mjs +18 -40
- package/main/cloud/account.mjs +179 -0
- package/main/cloud/aws.mjs +127 -0
- package/main/cloud/browser-return.mjs +58 -0
- package/main/cloud/clients.mjs +15 -0
- package/main/cloud/codex-cloud.mjs +53 -0
- package/main/cloud/config.mjs +107 -0
- package/main/cloud/docs.mjs +219 -0
- package/main/cloud/exec.mjs +27 -0
- package/main/cloud/host.mjs +165 -0
- package/main/cloud/kit.mjs +80 -0
- package/main/cloud/logins.mjs +543 -0
- package/main/cloud/managed.mjs +115 -0
- package/main/cloud/marker.mjs +104 -0
- package/main/cloud/org.mjs +53 -0
- package/main/cloud/reuse.mjs +208 -0
- package/main/cloud/run-cli.mjs +177 -0
- package/main/cloud/run.mjs +1934 -0
- package/main/cloud/usage.mjs +76 -0
- package/main/cloud-permission.mjs +51 -0
- package/main/codex-account.mjs +78 -0
- package/main/codex-app-server.mjs +276 -409
- package/main/codex-approvals.mjs +148 -433
- package/main/codex-models.mjs +20 -2
- package/main/codex-session.mjs +212 -507
- package/main/codex-threads-worker.mjs +12 -0
- package/main/codex-threads.mjs +71 -3
- package/main/codex-usage-file.mjs +5 -3
- package/main/codex-usage.mjs +56 -7
- package/main/codex-watch.mjs +17 -3
- package/main/codex.mjs +95 -363
- package/main/config.mjs +11 -1
- package/main/corner-tag.mjs +340 -19
- package/main/dev-window.mjs +1 -1
- package/main/engine-bin.mjs +232 -282
- package/main/engine-setup.mjs +21 -9
- package/main/feedback.mjs +3 -25
- package/main/folder-pictures.mjs +2 -2
- package/main/freeze-recorder-thread.mjs +86 -0
- package/main/freeze-recorder.mjs +146 -0
- package/main/fresh-user.mjs +20 -9
- package/main/git-change-offthread.mjs +17 -3
- package/main/git-change-thread.mjs +5 -2
- package/main/harnesses/claude.mjs +32 -11
- package/main/harnesses/codex.mjs +9 -2
- package/main/importable-threads.mjs +64 -6
- package/main/ipc.mjs +362 -111
- package/main/leftovers.mjs +117 -25
- package/main/liquid-glass.mjs +26 -0
- package/main/live-replies.mjs +10 -4
- package/main/main-went-red.mjs +330 -0
- package/main/main.mjs +199 -84
- package/main/memory-gate-history.mjs +36 -10
- package/main/memory-gate-server.mjs +62 -0
- package/main/memory-gate.mjs +14 -1
- package/main/menu.mjs +4 -2
- package/main/message-priority.mjs +2 -2
- package/main/mic.mjs +38 -0
- package/main/native/liquid-glass/build/liquid-glass.node +0 -0
- package/main/new-chats-watch.mjs +110 -0
- package/main/new-user-window.mjs +25 -0
- package/main/notify.mjs +24 -8
- package/main/phone-alerts.mjs +117 -0
- package/main/phone-inbox.mjs +625 -0
- package/main/phone-link.mjs +29 -9
- package/main/phone-uploads.mjs +476 -0
- package/main/picture-shape.mjs +114 -0
- package/main/quit-sequence.mjs +143 -0
- package/main/refresh-agent-models.mjs +15 -2
- package/main/relay-client.mjs +69 -1
- package/main/repeats.mjs +10 -4
- package/main/row-label.mjs +12 -41
- package/main/run-change.mjs +13 -0
- package/main/run-ledger.mjs +135 -0
- package/main/save-attachments.mjs +41 -0
- package/main/serve.mjs +45 -3
- package/main/settings.mjs +220 -44
- package/main/shell-path.mjs +185 -37
- package/main/ship-conflict-brief.mjs +119 -0
- package/main/ship-queue.mjs +115 -19
- package/main/source-updater.mjs +18 -10
- package/main/stage-attachment.mjs +68 -0
- package/main/stall-watch.mjs +50 -0
- package/main/store/home.mjs +87 -232
- package/main/store/paths.mjs +11 -0
- package/main/store/teammates-files.mjs +99 -0
- package/main/store/work-items.mjs +231 -13
- package/main/store.mjs +226 -52
- package/main/supervisor.mjs +3103 -341
- package/main/task-folders.mjs +430 -30
- package/main/task-terminals.mjs +110 -34
- package/main/team/index.mjs +114 -10
- package/main/team/memory-cloud.mjs +183 -1
- package/main/team/room-hours.mjs +48 -0
- package/main/team/room-setup.mjs +31 -0
- package/main/team/rooms-memory.mjs +454 -0
- package/main/team/rooms-shape.mjs +42 -0
- package/main/team/rooms-supabase.mjs +324 -0
- package/main/team/rooms.mjs +575 -0
- package/main/team/session.mjs +96 -26
- package/main/team/supabase-backend.mjs +180 -0
- package/main/team/sync.mjs +109 -1
- package/main/teammates.mjs +580 -0
- package/main/terminal-host.mjs +143 -0
- package/main/terminal-hosts.mjs +261 -0
- package/main/transcript-search.mjs +80 -0
- package/main/webview-guard.mjs +45 -0
- package/main/worktree-include.mjs +90 -219
- package/mcp/core/documents.mjs +3 -1
- package/mcp/core/repeats.mjs +88 -0
- package/mcp/core/teammates.mjs +222 -0
- package/mcp/core/work.mjs +76 -4
- package/mcp/stdio.mjs +24 -11
- package/mcp/tools.mjs +116 -8
- package/package.json +21 -5
- package/renderer/dist/assets/{abnfDiagram-YUKMZFIV-DPPuMWt-.js → abnfDiagram-YUKMZFIV-CoWB6YUK.js} +1 -1
- package/renderer/dist/assets/{arc-HmFfdseH.js → arc-Ch0NrCul.js} +1 -1
- package/renderer/dist/assets/{architectureDiagram-47P4ROYG-DxJ3zHrH.js → architectureDiagram-47P4ROYG-CXz9M5tQ.js} +1 -1
- package/renderer/dist/assets/{blockDiagram-E7TT5TSB-CbTXzOqQ.js → blockDiagram-E7TT5TSB-DchGUuHH.js} +1 -1
- package/renderer/dist/assets/{c4Diagram-CRA5TL53-X5ItFyLn.js → c4Diagram-CRA5TL53-Cip-zNyr.js} +1 -1
- package/renderer/dist/assets/channel-BQuQSL--.js +1 -0
- package/renderer/dist/assets/{chunk-2Q5K7J3B-DC12Ho_L.js → chunk-2Q5K7J3B-BoXid_2r.js} +1 -1
- package/renderer/dist/assets/{chunk-5VM5RSS4-ClVtfPd0.js → chunk-5VM5RSS4-Cpez1rRz.js} +1 -1
- package/renderer/dist/assets/{chunk-7TKQ45FW-DgcOm0HT.js → chunk-7TKQ45FW-DKA84YkE.js} +1 -1
- package/renderer/dist/assets/{chunk-AW2ZBBNX-C01s4yoF.js → chunk-AW2ZBBNX-Dg-SumPG.js} +1 -1
- package/renderer/dist/assets/{chunk-FVRAUYC3-Bj0zxhR4.js → chunk-FVRAUYC3-06TCvPm1.js} +2 -2
- package/renderer/dist/assets/{chunk-HTAEGDNF-DWDeEhLU.js → chunk-HTAEGDNF-CMEBy1Td.js} +1 -1
- package/renderer/dist/assets/{chunk-JWPE2WC7-Dsqwv3eW.js → chunk-JWPE2WC7-BJ2ObmV-.js} +1 -1
- package/renderer/dist/assets/{chunk-KQW6MTUR-CE86ANL2.js → chunk-KQW6MTUR-CGWOYXUE.js} +1 -1
- package/renderer/dist/assets/{chunk-XXDRQBXY-CoNTASfs.js → chunk-XXDRQBXY-CiZp07XC.js} +1 -1
- package/renderer/dist/assets/{classDiagram-v2-K4WV4PDN-2wprLqtw.js → classDiagram-v2-K4WV4PDN-DJNIX719.js} +1 -1
- package/renderer/dist/assets/cornerTag-CPgvu-q3.js +1 -0
- package/renderer/dist/assets/cornerTag-oMfEjzCO.css +1 -0
- package/renderer/dist/assets/{cose-bilkent-JH36ORCC-enqMiYbn.js → cose-bilkent-JH36ORCC-CbkyZOUH.js} +1 -1
- package/renderer/dist/assets/{cynefin-EF2NZ3EQ-DHngLxvQ.js → cynefin-EF2NZ3EQ-78iOroZO.js} +1 -1
- package/renderer/dist/assets/{cynefinDiagram-3GCD6N5R-C0rZZIl3.js → cynefinDiagram-3GCD6N5R-GcCopMs7.js} +1 -1
- package/renderer/dist/assets/{dagre-W4DXFKR2-DQnaEWO7.js → dagre-W4DXFKR2-DHLlXLkz.js} +1 -1
- package/renderer/dist/assets/{diagram-2UJZ2QOL-CS8AlGPx.js → diagram-2UJZ2QOL-DuZSUHcH.js} +1 -1
- package/renderer/dist/assets/{diagram-OVF4WLC6-yiDMHIkY.js → diagram-OVF4WLC6-Ckxu3cqH.js} +1 -1
- package/renderer/dist/assets/{diagram-PFPMY2P6-XbQKzbBA.js → diagram-PFPMY2P6-CsTw9Ycn.js} +1 -1
- package/renderer/dist/assets/{diagram-UMYRVEAY-dhVvlfl_.js → diagram-UMYRVEAY-CTXd8qVg.js} +1 -1
- package/renderer/dist/assets/{diagram-YEKJPTXX-DZHe8SCf.js → diagram-YEKJPTXX-CJs1T_9m.js} +1 -1
- package/renderer/dist/assets/{diagram-ZIFT7M5P-B5h6aXVR.js → diagram-ZIFT7M5P-CvjI23mf.js} +1 -1
- package/renderer/dist/assets/{ebnfDiagram-VR2GEFS7-PR9ScDco.js → ebnfDiagram-VR2GEFS7-d_IeAzOJ.js} +1 -1
- package/renderer/dist/assets/{elk-IJKZMXRS-Cg-yQXPd.js → elk-IJKZMXRS-zo9KFP3N.js} +1 -1
- package/renderer/dist/assets/{erDiagram-O2IAPWRE-omOrJMxu.js → erDiagram-O2IAPWRE-DqfYf5Dt.js} +1 -1
- package/renderer/dist/assets/{flowDiagram-OXPTDLAJ-aOX2hzvT.js → flowDiagram-OXPTDLAJ-DAWsMLmo.js} +1 -1
- package/renderer/dist/assets/{ganttDiagram-R7TSEDQI-B1H3pWyI.js → ganttDiagram-R7TSEDQI-4ml_bhwR.js} +1 -1
- package/renderer/dist/assets/{gitGraphDiagram-XJZIOB7I-Cvw304HC.js → gitGraphDiagram-XJZIOB7I-B_WcqSId.js} +1 -1
- package/renderer/dist/assets/{infoDiagram-5W2HQ5XZ-_ZXcNViH.js → infoDiagram-5W2HQ5XZ-CI-jYqzc.js} +1 -1
- package/renderer/dist/assets/{ishikawaDiagram-K3B6WC7H-C7Mx_qQk.js → ishikawaDiagram-K3B6WC7H-B56VWRlV.js} +1 -1
- package/renderer/dist/assets/{journeyDiagram-COXZFDF6-fr62QoAt.js → journeyDiagram-COXZFDF6-D1-p09Lr.js} +1 -1
- package/renderer/dist/assets/{kanban-definition-P3RFRI5V-J8zsabCe.js → kanban-definition-P3RFRI5V-Cv3w7JoN.js} +1 -1
- package/renderer/dist/assets/{linear-BmNdEtIi.js → linear-C35V2Spi.js} +1 -1
- package/renderer/dist/assets/main-BcoBiGwv.css +32 -0
- package/renderer/dist/assets/main-CLnIOulD.js +1191 -0
- package/renderer/dist/assets/{mermaid.core-CnYzvFkz.js → mermaid.core-BzGT5Vo9.js} +4 -4
- package/renderer/dist/assets/{mindmap-definition-LZFPQGKD-Bfcxc7PM.js → mindmap-definition-LZFPQGKD-BZUfuymC.js} +1 -1
- package/renderer/dist/assets/{pegDiagram-BGZESJAR-CwUJOq6X.js → pegDiagram-BGZESJAR-DFh9XNGe.js} +1 -1
- package/renderer/dist/assets/{pieDiagram-CAPJLFHJ-BrwMmqfC.js → pieDiagram-CAPJLFHJ-C65tPVsy.js} +1 -1
- package/renderer/dist/assets/{product-name-CTKLYNWY.js → product-name-CSNW6d7s.js} +1 -1
- package/renderer/dist/assets/{quadrantDiagram-GDMTTRAM-BN4Stoj-.js → quadrantDiagram-GDMTTRAM-Cushj_nk.js} +1 -1
- package/renderer/dist/assets/{railroadDiagram-SM67HX2B-CPZ5bbeR.js → railroadDiagram-SM67HX2B-DZGebGx5.js} +1 -1
- package/renderer/dist/assets/{requirementDiagram-X7JNWC4A-CgRnimVe.js → requirementDiagram-X7JNWC4A-DpAB0MNo.js} +1 -1
- package/renderer/dist/assets/{sankeyDiagram-UM26HJRW-Bt2rej__.js → sankeyDiagram-UM26HJRW-D-29rHra.js} +1 -1
- package/renderer/dist/assets/{sequenceDiagram-ZO4K6R2Y-Dd5nKe9G.js → sequenceDiagram-ZO4K6R2Y-FfOQAZol.js} +1 -1
- package/renderer/dist/assets/{sizeCapture-INFHLROL-usyNyXyh.js → sizeCapture-INFHLROL-C_n6fDBY.js} +1 -1
- package/renderer/dist/assets/{stateDiagram-v2-TBUQTH76-CRzUBb0u.js → stateDiagram-v2-TBUQTH76-BZjenzJG.js} +1 -1
- package/renderer/dist/assets/{swimlanes-N4OXWK64-C1GJvMS0.js → swimlanes-N4OXWK64-DxaMU9vX.js} +1 -1
- package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cb2kM6rr.js +8 -0
- package/renderer/dist/assets/{timeline-definition-YOQAKGHF-caeyPlcc.js → timeline-definition-YOQAKGHF-DTg5HJKz.js} +1 -1
- package/renderer/dist/assets/{usecaseDiagram-VIAY4XPW-BN_SV6PC.js → usecaseDiagram-VIAY4XPW-Bc3MZDsl.js} +1 -1
- package/renderer/dist/assets/{vennDiagram-BLWOH2XV-BLGuaUX-.js → vennDiagram-BLWOH2XV-GIVGk9Id.js} +1 -1
- package/renderer/dist/assets/{wardleyDiagram-YMQ3BMBF-Da0cJeNc.js → wardleyDiagram-YMQ3BMBF-NmJnbQOg.js} +1 -1
- package/renderer/dist/assets/{xychartDiagram-TAQBALBS-cGfUffMk.js → xychartDiagram-TAQBALBS-p-kfGfnW.js} +1 -1
- package/renderer/dist/corner-tag.html +3 -3
- package/renderer/dist/index.html +3 -3
- package/scripts/after-install.mjs +30 -8
- package/scripts/cloud-shim.pl +57 -0
- package/shared/account-names.d.mts +6 -0
- package/shared/account-names.mjs +15 -0
- package/shared/agent-hours.mjs +136 -0
- package/shared/agent-import.mjs +60 -34
- package/shared/agents.mjs +126 -522
- package/shared/analytics-events.mjs +12 -0
- package/shared/approval-card.mjs +36 -0
- package/shared/bridge-map.mjs +27 -1
- package/shared/claude-code-shape.mjs +6 -0
- package/shared/claude-usage.d.mts +1 -0
- package/shared/claude-usage.mjs +42 -1
- package/shared/cloud-hand-back.d.mts +7 -0
- package/shared/cloud-hand-back.mjs +20 -0
- package/shared/cloud-kit.d.mts +219 -0
- package/shared/cloud-kit.mjs +341 -0
- package/shared/cloud-login-name.mjs +4 -0
- package/shared/cloud-start.mjs +17 -0
- package/shared/cloud-versions.mjs +23 -0
- package/shared/codex-import.mjs +2 -2
- package/shared/codex-modes.d.mts +22 -9
- package/shared/codex-modes.mjs +67 -11
- package/shared/codex-usage.mjs +108 -2
- package/shared/corner-tag.d.mts +23 -1
- package/shared/corner-tag.mjs +284 -29
- package/shared/dead-run-trace.mjs +19 -2
- package/shared/engines.mjs +70 -309
- package/shared/feedback.d.mts +7 -0
- package/shared/feedback.mjs +25 -0
- package/shared/first-run-practice.mjs +17 -24
- package/shared/harness-definitions.mjs +8 -0
- package/shared/instruction-defaults.json +2 -2
- package/shared/key-caps.d.mts +14 -0
- package/shared/key-caps.mjs +68 -0
- package/shared/message-edits.d.mts +7 -0
- package/shared/message-edits.mjs +35 -0
- package/shared/move-thread.d.mts +24 -0
- package/shared/move-thread.mjs +112 -0
- package/shared/named-threads.mjs +72 -0
- package/shared/not-shipped.d.mts +22 -0
- package/shared/not-shipped.mjs +89 -0
- package/shared/notify-rules.d.mts +6 -0
- package/shared/notify-rules.mjs +36 -8
- package/shared/phone-files.mjs +28 -0
- package/shared/phone-settings.mjs +118 -0
- package/shared/phone-snapshot.d.mts +16 -0
- package/shared/phone-snapshot.mjs +348 -0
- package/shared/picture-formats.d.mts +1 -0
- package/shared/picture-formats.mjs +13 -0
- package/shared/rank.d.mts +2 -1
- package/shared/rank.mjs +32 -19
- package/shared/repeats.d.mts +8 -0
- package/shared/repeats.mjs +33 -2
- package/shared/room-cards.mjs +167 -0
- package/shared/room-links.d.mts +4 -0
- package/shared/room-links.mjs +21 -0
- package/shared/runs-on.d.mts +3 -3
- package/shared/runs-on.mjs +8 -5
- package/shared/shipped-note.d.mts +4 -0
- package/shared/shipped-note.mjs +6 -0
- package/shared/shortcuts.mjs +14 -6
- package/shared/spawn-trouble.d.mts +4 -0
- package/shared/spawn-trouble.mjs +443 -40
- package/shared/team-rules.mjs +9 -2
- package/shared/teammates.d.mts +19 -0
- package/shared/teammates.mjs +756 -0
- package/shared/terminal-focus.d.mts +1 -0
- package/shared/terminal-focus.mjs +13 -0
- package/shared/thread-cards.mjs +4 -1
- package/shared/usage.d.mts +9 -0
- package/shared/usage.mjs +49 -3
- package/shared/websites.d.mts +15 -0
- package/shared/websites.mjs +61 -0
- package/shared/work-items.mjs +127 -30
- package/shared/work-lines.mjs +92 -244
- package/renderer/dist/assets/channel-BlT2g9Sz.js +0 -1
- package/renderer/dist/assets/cornerTag-BLrxiDE5.css +0 -1
- package/renderer/dist/assets/cornerTag-BOT6u3Tn.js +0 -1
- package/renderer/dist/assets/main-ChNdnUvt.js +0 -1132
- package/renderer/dist/assets/main-nj0QR5kg.css +0 -32
- package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cy4Av2_p.js +0 -8
package/main/codex-approvals.mjs
CHANGED
|
@@ -1,120 +1,42 @@
|
|
|
1
|
-
//
|
|
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
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
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
|
-
//
|
|
16
|
-
//
|
|
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
|
-
|
|
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
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
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
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
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
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
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
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
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
|
-
//
|
|
190
|
-
//
|
|
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 { /*
|
|
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
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
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
|
-
//
|
|
246
|
-
//
|
|
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
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
282
|
-
*
|
|
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
|
-
* `
|
|
285
|
-
*
|
|
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
|
-
*
|
|
289
|
-
*
|
|
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
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
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
|
-
//
|
|
380
|
-
//
|
|
381
|
-
//
|
|
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
|
-
//
|
|
402
|
-
//
|
|
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
|
-
*
|
|
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
|
-
*
|
|
424
|
-
*
|
|
425
|
-
* deny
|
|
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,
|
|
225
|
+
const open = new Map(); // id -> { request, scope, decide, poll }
|
|
437
226
|
|
|
438
227
|
/**
|
|
439
|
-
*
|
|
440
|
-
*
|
|
441
|
-
*
|
|
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
|
-
//
|
|
449
|
-
//
|
|
450
|
-
//
|
|
451
|
-
//
|
|
452
|
-
// call
|
|
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
|
-
//
|
|
480
|
-
//
|
|
481
|
-
//
|
|
482
|
-
//
|
|
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
|
-
//
|
|
520
|
-
//
|
|
521
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
552
|
-
|
|
553
|
-
//
|
|
554
|
-
|
|
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
|
-
|
|
562
|
-
|
|
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
|
-
//
|
|
565
|
-
//
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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
|
-
//
|
|
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
|
-
*
|
|
594
|
-
*
|
|
595
|
-
*
|
|
596
|
-
*
|
|
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
|
-
//
|
|
616
|
-
//
|
|
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
|
-
*
|
|
624
|
-
*
|
|
625
|
-
*
|
|
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
|
-
//
|
|
645
|
-
//
|
|
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
|
-
*
|
|
664
|
-
*
|
|
665
|
-
*
|
|
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
|
-
*
|
|
676
|
-
*
|
|
677
|
-
*
|
|
678
|
-
*
|
|
679
|
-
*
|
|
680
|
-
*
|
|
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
|
},
|