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/approvals.mjs
CHANGED
|
@@ -1,93 +1,44 @@
|
|
|
1
|
-
// The app side of the approvals channel: read what workers are waiting on
|
|
2
|
-
// write the founder's decision
|
|
3
|
-
//
|
|
1
|
+
// The app side of the approvals channel: read what workers are waiting on and
|
|
2
|
+
// write the founder's decision, as files in the spool dir the approval-prompt
|
|
3
|
+
// server polls, so a restart on either side loses nothing.
|
|
4
4
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
5
|
+
// A worker can write in this spool. The store root is in its
|
|
6
|
+
// `additionalDirectories` and Bash is allowed broadly; what an agent may do is
|
|
7
|
+
// the user's call (`worker-permissions.json`), so this file treats that as the
|
|
8
|
+
// threat model rather than narrowing it. In Node, writeFileSync and
|
|
9
|
+
// appendFileSync write through a symlink to its target, and readFileSync on a
|
|
10
|
+
// fifo blocks forever. So nothing here is opened through a link, nothing is
|
|
11
|
+
// opened that is not a regular file, and every write is created at an
|
|
12
|
+
// unguessable name and renamed onto the real one, which replaces a link
|
|
13
|
+
// without following it. These helpers are the one place that rule lives.
|
|
8
14
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// do on her machine is section 7 of the terms and hers to decide -- it is the
|
|
14
|
-
// threat model this file is written against. Measured on this Mac, node
|
|
15
|
-
// v22.19.0:
|
|
16
|
-
//
|
|
17
|
-
// fs.writeFileSync(<symlink>, x) -> writes THROUGH it, to the target
|
|
18
|
-
// fs.appendFileSync(<symlink>, x) -> appends THROUGH it
|
|
19
|
-
// fs.readFileSync(<fifo>) -> blocks the process, forever
|
|
20
|
-
// fs.openSync(<symlink>, 'wx') -> EEXIST
|
|
21
|
-
// open(..., O_APPEND|O_CREAT|O_NOFOLLOW) -> ELOOP
|
|
22
|
-
// open(..., O_RDONLY|O_NONBLOCK) + fstat -> returns, and says isFIFO
|
|
23
|
-
// fs.renameSync(tmp, <symlink>) -> replaces the LINK; target intact
|
|
24
|
-
// fs.rmSync(<symlink>, {recursive:true}) -> removes the LINK; target intact
|
|
25
|
-
//
|
|
26
|
-
// The first three lines are what the top three used to be. So: nothing in this
|
|
27
|
-
// directory is ever opened through a link, nothing is opened that is not a
|
|
28
|
-
// regular file, and every write lands by creating at a name nothing can guess
|
|
29
|
-
// and renaming onto the real one. The helpers are here rather than in each
|
|
30
|
-
// caller because two copies of that rule are one edit away from disagreeing,
|
|
31
|
-
// and the half that forgets is the half with the hole.
|
|
32
|
-
//
|
|
33
|
-
// AND NOTHING IN A REQUEST FILE IS BELIEVED ABOUT TIME. `at` is a worker's own
|
|
34
|
-
// account of itself; the order her cards stack in is Agentbox's, see
|
|
35
|
-
// `listPending`.
|
|
36
|
-
//
|
|
37
|
-
// WHAT THIS STILL DOES NOT CLAIM. A worker that can write here can still put A
|
|
38
|
-
// card in front of her: a file is the whole of the Claude Code protocol
|
|
39
|
-
// (main/approval-prompt-server.mjs waits in another process and has no other
|
|
40
|
-
// channel), so a request's provenance cannot be proved. What it can no longer
|
|
41
|
-
// do is aim her click at a file of hers, append to one, put its card in front
|
|
42
|
-
// of every card that came before it, or leave that card there.
|
|
15
|
+
// Nothing in a request file is believed about time; card order is ours, see
|
|
16
|
+
// `listPending`. A worker can still put a card in front of her, because a file
|
|
17
|
+
// is the whole Claude Code protocol and provenance cannot be proved. It cannot
|
|
18
|
+
// aim her click at another file, append to one, or jump or hold the queue.
|
|
43
19
|
|
|
44
20
|
import fs from 'node:fs';
|
|
45
21
|
import path from 'node:path';
|
|
46
22
|
import crypto from 'node:crypto';
|
|
23
|
+
import { cardDigest } from '../shared/approval-card.mjs';
|
|
47
24
|
|
|
48
25
|
export function spoolDir(storeRoot) {
|
|
49
26
|
return path.join(storeRoot, '.approvals');
|
|
50
27
|
}
|
|
51
28
|
|
|
52
|
-
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
// approve another worker's card, or start a watcher and approve its own the
|
|
61
|
-
// instant it raised one.
|
|
62
|
-
//
|
|
63
|
-
// The Codex half was closed by moving the decision in process. This half cannot
|
|
64
|
-
// be: the thing waiting is a SEPARATE PROCESS, spawned by the CLI as the
|
|
65
|
-
// worker's own child, and a file is the whole of the protocol it has.
|
|
66
|
-
//
|
|
67
|
-
// AND NO SHARED SECRET CLOSES IT EITHER, which is worth writing down so nobody
|
|
68
|
-
// re-derives it: anything handed to that server is handed through the worker's
|
|
69
|
-
// own environment, and the worker can read it. A token is a token the forger
|
|
70
|
-
// has.
|
|
29
|
+
// An answer file a worker writes must not count as her approval. The Claude
|
|
30
|
+
// Code side waits in a separate process, the worker's own child, so a file is
|
|
31
|
+
// all it can read. A shared secret does not help: anything handed to that
|
|
32
|
+
// server passes through the worker's environment, which the worker can read.
|
|
33
|
+
// So Agentbox signs each decision with an ed25519 private key held only in its
|
|
34
|
+
// own process, and the server gets the public half, which verifies and cannot
|
|
35
|
+
// sign. The signature covers id, verdict, note, time and card together, so a
|
|
36
|
+
// decision cannot be moved, flipped or replayed.
|
|
71
37
|
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
// decision cannot be moved onto another card, flipped from a deny into an
|
|
77
|
-
// allow, or replayed onto a later one.
|
|
78
|
-
//
|
|
79
|
-
// THE PAIR IS PER RUN OF AGENTBOX, deliberately: a key file in her home is a key
|
|
80
|
-
// a worker can read, and there is nothing to persist for -- every card is
|
|
81
|
-
// answered by the process that raised it, and restarting Agentbox kills every
|
|
82
|
-
// worker anyway (CLAUDE.md, process etiquette). Made on first use rather than
|
|
83
|
-
// at import, so a machine that never raises a card never pays for it.
|
|
84
|
-
//
|
|
85
|
-
// WHAT IS STILL NOT CLOSED, and it is not closeable from this repository. That
|
|
86
|
-
// MCP server is the worker's own child at the same uid. It cannot read Agentbox's
|
|
87
|
-
// memory -- macOS wants root or an entitlement for that -- so no signature can
|
|
88
|
-
// be made. What a same-uid process CAN do is kill it, or attach to it and patch
|
|
89
|
-
// the check out. Stopping that means the worker being allowed less, which is
|
|
90
|
-
// `worker-permissions.json`, which is section 7 of the terms and hers.
|
|
38
|
+
// The pair is per run and never written to disk, because a key file in her
|
|
39
|
+
// home is one a worker can read, and restarting Agentbox kills every worker
|
|
40
|
+
// anyway. Not closed here: a same-uid worker can kill that server or patch its
|
|
41
|
+
// check. macOS keeps it from reading Agentbox's memory, so it cannot sign.
|
|
91
42
|
|
|
92
43
|
let pair = null;
|
|
93
44
|
const keys = () => (pair ??= crypto.generateKeyPairSync('ed25519'));
|
|
@@ -97,95 +48,32 @@ export function approvalPublicKey() {
|
|
|
97
48
|
return keys().publicKey.export({ type: 'spki', format: 'der' }).toString('base64');
|
|
98
49
|
}
|
|
99
50
|
|
|
100
|
-
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
// its CONTENTS are replaced. So a worker raised `rm -rf ~/Zero`, rewrote its own
|
|
107
|
-
// request in place with `echo hi`, `listPending` drew `echo hi`, she pressed
|
|
108
|
-
// Allow -- and the process waiting on that id was STILL HOLDING the first
|
|
109
|
-
// command and verified her signature happily. The signature was real and it
|
|
110
|
-
// said only "yes to <uuid>". Codex has the same shape from the other end:
|
|
111
|
-
// `settle` takes an id and a verdict while `open` holds the original, and a
|
|
112
|
-
// Claude Code worker can write in the spool both engines draw their cards from.
|
|
51
|
+
// A signature over an id alone says nothing about what she agreed to, because
|
|
52
|
+
// a worker can rewrite its request file in place while the waiting process
|
|
53
|
+
// still holds the original. So `answer` signs a digest of the card Agentbox
|
|
54
|
+
// last showed her, and each engine compares it with the request it holds
|
|
55
|
+
// (approval-prompt-server.mjs its `request`, codex-approvals.mjs its `open`
|
|
56
|
+
// entry). A mismatch is a deny.
|
|
113
57
|
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
// `request`, main/codex-approvals.mjs against its own `open` entry. A mismatch
|
|
119
|
-
// is a deny, on both.
|
|
58
|
+
// The digest is of what was displayed, not of the file at answer time: a
|
|
59
|
+
// re-read lets a worker flip the file between a harmless body and the real one.
|
|
60
|
+
// `listPending` is the only reader, and every caller acts on or draws exactly
|
|
61
|
+
// what it returned, so the digest taken there is the card she saw.
|
|
120
62
|
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
// one then wins every time her press lands on a harmless draw and the re-read
|
|
125
|
-
// catches the real body. `listPending` is the only thing in the app that reads
|
|
126
|
-
// a request, and each of its three callers acts on or draws exactly what it
|
|
127
|
-
// returned -- the snapshot (main/ipc.mjs), the ⌘Y/⌘N chord and the Agents menu
|
|
128
|
-
// (main/main.mjs) -- so the digest taken there IS the card she is looking at.
|
|
129
|
-
//
|
|
130
|
-
// AND CANONICALISATION IS THE PART THAT FAILS QUIETLY. A digest that disagrees
|
|
131
|
-
// with itself across the round trip through the file denies every approval on
|
|
132
|
-
// the machine, with nothing on any screen saying why. Both sides hold JSON: the
|
|
133
|
-
// engine's own object, and Agentbox's parse of the file that object was
|
|
134
|
-
// stringified into. So the digest is taken over a canonical form after ONE JSON
|
|
135
|
-
// round trip -- which resolves `toJSON`, drops `undefined` from objects and
|
|
136
|
-
// turns it and NaN into null inside arrays, exactly as the write to disk did --
|
|
137
|
-
// and then sorts keys, because two objects carrying the same fields in a
|
|
138
|
-
// different order are the same card.
|
|
139
|
-
|
|
140
|
-
/** One JSON value as one string, with the key order taken out of it. */
|
|
141
|
-
function canonical(value) {
|
|
142
|
-
if (Array.isArray(value)) return `[${value.map((v) => canonical(v) ?? 'null').join(',')}]`;
|
|
143
|
-
if (value && typeof value === 'object') {
|
|
144
|
-
const fields = [];
|
|
145
|
-
for (const key of Object.keys(value).sort()) {
|
|
146
|
-
const v = canonical(value[key]);
|
|
147
|
-
if (v !== undefined) fields.push(`${JSON.stringify(key)}:${v}`);
|
|
148
|
-
}
|
|
149
|
-
return `{${fields.join(',')}}`;
|
|
150
|
-
}
|
|
151
|
-
return JSON.stringify(value);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* THE CARD, AS ONE STRING EITHER SIDE CAN ARRIVE AT INDEPENDENTLY.
|
|
156
|
-
*
|
|
157
|
-
* Null for anything that is not a request object and for anything JSON cannot
|
|
158
|
-
* express (a cycle, a BigInt). Null is never equal to a digest, so every caller
|
|
159
|
-
* reads it as a mismatch and denies, which is the safe direction in all three.
|
|
160
|
-
*
|
|
161
|
-
* NOTHING IS NORMALISED beyond what JSON itself did. Two byte strings that draw
|
|
162
|
-
* the same glyph are two different commands, and Agentbox deciding otherwise
|
|
163
|
-
* would be Agentbox deciding what her shell will do with them.
|
|
164
|
-
*/
|
|
165
|
-
export function cardDigest(request) {
|
|
166
|
-
if (!request || typeof request !== 'object' || Array.isArray(request)) return null;
|
|
167
|
-
let text;
|
|
168
|
-
try { text = canonical(JSON.parse(JSON.stringify(request))); } catch { return null; }
|
|
169
|
-
if (typeof text !== 'string') return null;
|
|
170
|
-
return crypto.createHash('sha256').update(text, 'utf8').digest('base64');
|
|
171
|
-
}
|
|
63
|
+
// The digest itself is shared/approval-card.mjs, so the cloud server takes the
|
|
64
|
+
// same one of a card it puts on the phone.
|
|
65
|
+
export { cardDigest };
|
|
172
66
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
* them, including WHICH CARD it was a decision about. An array rather than an
|
|
176
|
-
* object, because two JSON objects carrying the same fields in a different
|
|
177
|
-
* order are different bytes, and the verifier rebuilds this from a PARSED file
|
|
178
|
-
* rather than from the text on disk.
|
|
179
|
-
*/
|
|
67
|
+
// An array, not an object, because the verifier rebuilds these bytes from a
|
|
68
|
+
// parsed file and object key order would change them.
|
|
180
69
|
function decisionBytes(id, allow, note, at, card) {
|
|
181
70
|
return Buffer.from(JSON.stringify([String(id), !!allow, note ?? null, at ?? null, card ?? null]), 'utf8');
|
|
182
71
|
}
|
|
183
72
|
|
|
184
73
|
/**
|
|
185
|
-
* Whether this answer
|
|
186
|
-
*
|
|
187
|
-
|
|
188
|
-
* signed -- so the binding cannot be stripped back off a real decision. */
|
|
74
|
+
* Whether Agentbox signed this answer for this request. False for anything a
|
|
75
|
+
* worker writes, including a real answer whose `card` was edited.
|
|
76
|
+
*/
|
|
189
77
|
export function answerIsHers(id, answer, publicKey = approvalPublicKey()) {
|
|
190
78
|
if (!answer || typeof answer !== 'object' || Array.isArray(answer)) return false;
|
|
191
79
|
if (typeof answer.sig !== 'string' || !answer.sig) return false;
|
|
@@ -198,60 +86,40 @@ export function answerIsHers(id, answer, publicKey = approvalPublicKey()) {
|
|
|
198
86
|
}
|
|
199
87
|
|
|
200
88
|
/**
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
* or the leftovers of a worker that died before its own deadline, and in both
|
|
208
|
-
* cases it is a question nothing can answer.
|
|
209
|
-
*
|
|
210
|
-
* A minute over the longest of them, and never under: being early takes a live
|
|
211
|
-
* card off her screen, being late leaves a dead one there one more minute.
|
|
212
|
-
* tests/a-card-a-worker-forged-cannot-hold-the-front-of-her-stack.test.mjs
|
|
213
|
-
* checks that against both of those constants rather than trusting this
|
|
214
|
-
* comment.
|
|
89
|
+
* How long a request file may sit here with nobody waiting on it. A forgery and
|
|
90
|
+
* a dead worker's leftover leave this way, and they are the only two things
|
|
91
|
+
* that should: a card still being waited on is kept here by whoever is waiting,
|
|
92
|
+
* which is a beat of main/codex-approvals.mjs `tend` and of
|
|
93
|
+
* approval-prompt-core.mjs's own loop. Neither gives up on a clock any more
|
|
94
|
+
* (w-4b00022acb), so this is no longer a deadline shadowing theirs.
|
|
215
95
|
*/
|
|
216
96
|
export const REQUEST_TTL_MS = 16 * 60_000;
|
|
217
97
|
|
|
218
98
|
/**
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
|
|
222
|
-
* is only ever a bound on what a worker can make Agentbox hold in memory on
|
|
223
|
-
* every poll of the inbox. Past it the file is skipped exactly the way an
|
|
224
|
-
* unparseable one always has been, and the TTL above takes it away. */
|
|
99
|
+
* Caps what a worker can make Agentbox hold in memory on every poll. A larger
|
|
100
|
+
* file is skipped like an unparseable one, and the TTL removes it.
|
|
101
|
+
*/
|
|
225
102
|
export const MAX_REQUEST_BYTES = 1024 * 1024;
|
|
226
103
|
|
|
227
104
|
const O_NOFOLLOW = fs.constants.O_NOFOLLOW ?? 0; // undefined on Windows, which this app is not
|
|
228
105
|
const READ_FLAGS = fs.constants.O_RDONLY | fs.constants.O_NONBLOCK | O_NOFOLLOW;
|
|
229
106
|
const APPEND_FLAGS = fs.constants.O_WRONLY | fs.constants.O_APPEND | fs.constants.O_CREAT | O_NOFOLLOW;
|
|
230
107
|
|
|
231
|
-
|
|
232
|
-
* Whether the spool holds an entry by this name, WHATEVER it is. `lstat`
|
|
233
|
-
* rather than `existsSync`, which resolves a link and so answers "no" for a
|
|
234
|
-
* dangling one that `readdir` can plainly see. */
|
|
108
|
+
// `lstat`, because `existsSync` follows a link and says no for a dangling one.
|
|
235
109
|
export function spoolEntryExists(dir, name) {
|
|
236
110
|
try { fs.lstatSync(path.join(dir, name)); return true; } catch { return false; }
|
|
237
111
|
}
|
|
238
112
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
* symlink rather than descending it, so this never reaches outside the spool,
|
|
242
|
-
* and it removes the directory that `unlink` refuses (EPERM, measured). */
|
|
113
|
+
// `rm` unlinks a symlink rather than following it, so this never reaches
|
|
114
|
+
// outside the spool, and it removes a directory, which `unlink` refuses.
|
|
243
115
|
export function removeSpoolEntry(dir, name) {
|
|
244
116
|
try { fs.rmSync(path.join(dir, name), { recursive: true, force: true }); } catch { /* already gone */ }
|
|
245
117
|
}
|
|
246
118
|
|
|
247
119
|
/**
|
|
248
|
-
* One spool file as
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
* O_NONBLOCK so a named pipe wearing a request file's name cannot freeze the
|
|
252
|
-
* main process inside a synchronous read, and `fstat` on the descriptor we
|
|
253
|
-
* actually got rather than an `lstat` on the name, which would leave a window
|
|
254
|
-
* to swap underneath.
|
|
120
|
+
* One spool file as parsed JSON, or null. O_NOFOLLOW so a link to her file is
|
|
121
|
+
* never opened, O_NONBLOCK so a fifo cannot freeze the main process, and
|
|
122
|
+
* `fstat` on the open descriptor so the name cannot be swapped after a check.
|
|
255
123
|
*/
|
|
256
124
|
export function readSpoolJson(dir, name) {
|
|
257
125
|
let fd = null;
|
|
@@ -268,19 +136,11 @@ export function readSpoolJson(dir, name) {
|
|
|
268
136
|
}
|
|
269
137
|
|
|
270
138
|
/**
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
* followed AND her decision still lands on the first press. Plain `wx` on the
|
|
277
|
-
* final name would refuse to follow but would also throw her click away
|
|
278
|
-
* whenever anything squatted the name, and a lost Allow is the silence this
|
|
279
|
-
* app is built not to produce.
|
|
280
|
-
*
|
|
281
|
-
* The second attempt is for the one squatter `rename` cannot replace: a
|
|
282
|
-
* directory (EISDIR, measured), which is also what `unlink` refuses. Removing
|
|
283
|
-
* it is safe for the reason `removeSpoolEntry` gives.
|
|
139
|
+
* Write a spool file without writing through whatever is already there. The
|
|
140
|
+
* exclusive create is at an unguessable name, and the rename replaces a planted
|
|
141
|
+
* link instead of following it. Plain `wx` on the final name would also refuse
|
|
142
|
+
* a link, but would lose her click whenever something squatted the name. The
|
|
143
|
+
* second attempt is for a directory, which `rename` cannot replace.
|
|
284
144
|
*/
|
|
285
145
|
export function writeSpoolFile(dir, name, text) {
|
|
286
146
|
const dest = path.join(dir, name);
|
|
@@ -307,21 +167,12 @@ export function writeSpoolFile(dir, name, text) {
|
|
|
307
167
|
}
|
|
308
168
|
|
|
309
169
|
/**
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
* with, appended once per decision, forever.
|
|
317
|
-
*
|
|
318
|
-
* O_NOFOLLOW closes it with no window between a check and the write. But
|
|
319
|
-
* refusing alone would hand the attacker the audit trail: both callers swallow
|
|
320
|
-
* a failure here on purpose (a decision is never held hostage to its log line),
|
|
321
|
-
* so a planted link would end auditing silently and for good. An entry that is
|
|
322
|
-
* not a regular file is therefore destroyed and a real one opened in its place,
|
|
323
|
-
* out loud -- this is the one warning in this area that always means a worker
|
|
324
|
-
* attacked the spool.
|
|
170
|
+
* Append one audit line, never into a file of hers. `log.jsonl` is a fixed
|
|
171
|
+
* name and the line carries the worker's own command, so a link planted there
|
|
172
|
+
* would put agent text into her files. O_NOFOLLOW refuses the link. Refusing
|
|
173
|
+
* alone would end auditing silently, because callers ignore a failed log line,
|
|
174
|
+
* so anything that is not a regular file is removed, with a warning, and a
|
|
175
|
+
* real file opened in its place.
|
|
325
176
|
*/
|
|
326
177
|
export function appendSpoolLine(dir, name, line) {
|
|
327
178
|
if (appendOnce(dir, name, line)) return true;
|
|
@@ -347,25 +198,11 @@ function appendOnce(dir, name, line) {
|
|
|
347
198
|
}
|
|
348
199
|
|
|
349
200
|
/**
|
|
350
|
-
*
|
|
351
|
-
*
|
|
352
|
-
*
|
|
353
|
-
* `
|
|
354
|
-
*
|
|
355
|
-
* stack -- and that is not one bad card, it is every card, because the renderer
|
|
356
|
-
* draws only the front one and ⌘Y/⌘N and the Agents menu all act on
|
|
357
|
-
* `listPending[0]`. Her keystroke answers the decoy while the question she is
|
|
358
|
-
* reading waits behind it.
|
|
359
|
-
*
|
|
360
|
-
* So `at` is not read for ordering at all any more. `seq` is taken at the
|
|
361
|
-
* sighting and only ever increases, which makes the order total as well as
|
|
362
|
-
* unforgeable; the timestamp beside it is what the TTL is measured from. A
|
|
363
|
-
* floor on the worker's own `at` was the other candidate and it is weaker: a
|
|
364
|
-
* legitimate `at` is written moments before Agentbox sees the file, so it needs a
|
|
365
|
-
* tolerance, and that tolerance is exactly the window a forgery is planted in.
|
|
366
|
-
*
|
|
367
|
-
* Keyed by directory as well as by id because the suite runs many spools, and
|
|
368
|
-
* pruned against what is really on disk on every pass, so it cannot grow.
|
|
201
|
+
* When this process first saw each request, the only clock in the spool a
|
|
202
|
+
* worker cannot write. The request's own `at` is never used for order: a
|
|
203
|
+
* forged `at: 0` would take the front card, which ⌘Y/⌘N and the Agents menu
|
|
204
|
+
* act on. `seq` only increases; `at` here is what the TTL counts from. Keyed by
|
|
205
|
+
* directory and id, and pruned to what is on disk on every pass.
|
|
369
206
|
*/
|
|
370
207
|
const seenAt = new Map();
|
|
371
208
|
let sightings = 0;
|
|
@@ -373,28 +210,12 @@ let sightings = 0;
|
|
|
373
210
|
const REQUEST_SUFFIX = '.request.json';
|
|
374
211
|
|
|
375
212
|
/**
|
|
376
|
-
*
|
|
377
|
-
*
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
*
|
|
381
|
-
*
|
|
382
|
-
* a decoy the instant a real card appeared and sort itself in front of it.
|
|
383
|
-
*
|
|
384
|
-
* `ctime` is the right one, and it is the only timestamp here that is any use:
|
|
385
|
-
* an unprivileged process can move a file's mtime and its BIRTHTIME backwards
|
|
386
|
-
* -- measured, `utimes(epoch)` set both to 0 -- and the same call pushed ctime
|
|
387
|
-
* FORWARD to now. A worker can therefore make its own request look younger than
|
|
388
|
-
* it is, which costs it the front of the stack, and can never make it look
|
|
389
|
-
* older. Nanoseconds because APFS keeps them and two files written back to back
|
|
390
|
-
* differ by about 170 microseconds.
|
|
391
|
-
*
|
|
392
|
-
* A coarse clock stamps both files the same. tmpfs does this: two writes in
|
|
393
|
-
* one poll share a ctime, and `readdir` then comes back in name order, which
|
|
394
|
-
* is the forgery the paragraph above is about. The inode breaks that tie.
|
|
395
|
-
* The kernel assigns it, a later create in a fresh directory gets a higher
|
|
396
|
-
* one, and a worker cannot move it backwards. Equal inode and equal ctime
|
|
397
|
-
* keep the order we already had.
|
|
213
|
+
* Orders requests first seen in the same poll. Not by `readdir`, which returns
|
|
214
|
+
* name order on macOS, and a forgery picks its own name. `ctime` is the one
|
|
215
|
+
* timestamp a worker cannot move backwards: `utimes` can set mtime and
|
|
216
|
+
* birthtime to 0 but pushes ctime to now. Nanoseconds because APFS keeps them.
|
|
217
|
+
* On a coarse clock (tmpfs) two files share a ctime, so the kernel-assigned
|
|
218
|
+
* inode breaks the tie.
|
|
398
219
|
*/
|
|
399
220
|
function bornAt(dir, name) {
|
|
400
221
|
try {
|
|
@@ -408,12 +229,10 @@ export function listPending(storeRoot, { now = Date.now } = {}) {
|
|
|
408
229
|
let files = [];
|
|
409
230
|
try { files = fs.readdirSync(dir); } catch { return []; }
|
|
410
231
|
const at = now();
|
|
411
|
-
// NUL cannot occur in a path
|
|
232
|
+
// NUL cannot occur in a path, so two spools never collide.
|
|
412
233
|
const prefix = `${dir}\0`;
|
|
413
234
|
const names = files.filter((f) => f.endsWith(REQUEST_SUFFIX));
|
|
414
|
-
//
|
|
415
|
-
// read, oldest first, so the order does not depend on what `readdir` felt
|
|
416
|
-
// like returning.
|
|
235
|
+
// Queue everything new in this pass before reading any of it, oldest first.
|
|
417
236
|
for (const { f } of names
|
|
418
237
|
.filter((f) => !seenAt.has(prefix + f.slice(0, -REQUEST_SUFFIX.length)))
|
|
419
238
|
.map((f) => ({ f, born: bornAt(dir, f) }))
|
|
@@ -427,23 +246,16 @@ export function listPending(storeRoot, { now = Date.now } = {}) {
|
|
|
427
246
|
const id = f.slice(0, -REQUEST_SUFFIX.length);
|
|
428
247
|
const key = prefix + id;
|
|
429
248
|
const seen = seenAt.get(key);
|
|
430
|
-
// A REQUEST NOBODY IS WAITING ON ANY MORE. Nothing ever removed one whose
|
|
431
|
-
// process was gone, which is what made a forged `at: 0` mean permanently.
|
|
432
249
|
if (at - seen.at > REQUEST_TTL_MS) {
|
|
433
|
-
console.warn(`zero: approval ${id}
|
|
250
|
+
console.warn(`zero: approval ${id} sat in the spool with nothing waiting on it for ${Math.round(REQUEST_TTL_MS / 60_000)} minutes; it has been taken out`);
|
|
434
251
|
removeSpoolEntry(dir, f);
|
|
435
252
|
removeSpoolEntry(dir, `${id}.answer.json`);
|
|
436
253
|
continue;
|
|
437
254
|
}
|
|
438
255
|
present.add(key);
|
|
439
|
-
// A
|
|
440
|
-
//
|
|
441
|
-
//
|
|
442
|
-
// a forgery once a second keeps ANY card, its own or another worker's,
|
|
443
|
-
// hidden until the fifteen-minute deadline denies it in silence. So the
|
|
444
|
-
// file is checked, and one that is not hers is removed rather than
|
|
445
|
-
// believed -- the same sweep main/codex-approvals.mjs already runs, moved
|
|
446
|
-
// to where both engines pass through it.
|
|
256
|
+
// A decided card is hidden while its engine reaps it, but only if the
|
|
257
|
+
// answer is hers. Otherwise any answer file could hide any card until it
|
|
258
|
+
// timed out. An answer she did not sign is removed.
|
|
447
259
|
const answerName = `${id}.answer.json`;
|
|
448
260
|
if (files.includes(answerName)) {
|
|
449
261
|
if (answerIsHers(id, readSpoolJson(dir, answerName))) continue;
|
|
@@ -451,13 +263,9 @@ export function listPending(storeRoot, { now = Date.now } = {}) {
|
|
|
451
263
|
removeSpoolEntry(dir, answerName);
|
|
452
264
|
}
|
|
453
265
|
const request = readSpoolJson(dir, f);
|
|
454
|
-
//
|
|
455
|
-
//
|
|
456
|
-
//
|
|
457
|
-
// card that was on the screen rather than whatever is filed under the id by
|
|
458
|
-
// the time somebody honours it. Kept from the last SUCCESSFUL read: a file
|
|
459
|
-
// that has gone unreadable takes its card off her screen too, so leaving
|
|
460
|
-
// the digest alone is leaving the last thing she was actually shown.
|
|
266
|
+
// `answer` signs this digest, so her press authorises the card she saw.
|
|
267
|
+
// Kept from the last successful read, because an unreadable file also
|
|
268
|
+
// leaves her screen, so the old digest is still the last card she saw.
|
|
461
269
|
if (request) {
|
|
462
270
|
seen.card = cardDigest(request);
|
|
463
271
|
pending.push({ request, seq: seen.seq });
|
|
@@ -467,29 +275,25 @@ export function listPending(storeRoot, { now = Date.now } = {}) {
|
|
|
467
275
|
return pending.sort((a, b) => a.seq - b.seq).map((p) => p.request);
|
|
468
276
|
}
|
|
469
277
|
|
|
470
|
-
/**
|
|
471
|
-
* The digest of the card last put in front of her for this request, or null
|
|
472
|
-
* when nothing ever was. Kept beside the sighting, so it lives exactly as long
|
|
473
|
-
* as the request file does. */
|
|
278
|
+
/** The digest of the card last shown for this request, or null if none was. */
|
|
474
279
|
export function cardShown(storeRoot, id) {
|
|
475
280
|
return seenAt.get(`${spoolDir(storeRoot)}\0${id}`)?.card ?? null;
|
|
476
281
|
}
|
|
477
282
|
|
|
283
|
+
/** Whether `card` is the card last drawn for this request: a phone answers with the one it drew. */
|
|
284
|
+
export function cardStillShown(storeRoot, id, card) {
|
|
285
|
+
return typeof card === 'string' && !!card && cardShown(storeRoot, id) === card;
|
|
286
|
+
}
|
|
287
|
+
|
|
478
288
|
export function answer(storeRoot, id, allow, note) {
|
|
479
289
|
if (!/^[\w-]+$/.test(String(id ?? ''))) return false;
|
|
480
290
|
const dir = spoolDir(storeRoot);
|
|
481
291
|
if (!spoolEntryExists(dir, `${id}.request.json`)) return false;
|
|
482
|
-
//
|
|
483
|
-
//
|
|
484
|
-
// drawn from, the ⌘Y/⌘N chord, the Agents menu -- so in the app there is
|
|
485
|
-
// always a card here. Reading the file instead would be signing something she
|
|
486
|
-
// may never have seen, which is the hole this whole section is about.
|
|
292
|
+
// Every route that answers calls `listPending` first, so a card is always
|
|
293
|
+
// here. Reading the file instead would sign something she may not have seen.
|
|
487
294
|
const card = cardShown(storeRoot, id);
|
|
488
295
|
if (!card) return false;
|
|
489
296
|
const at = Date.now();
|
|
490
|
-
// SIGNED, so the process waiting on it can tell her decision from one a
|
|
491
|
-
// worker wrote, and bound to the card so it cannot be honoured against a
|
|
492
|
-
// different action. See `answerIsHers` above for why nothing weaker works.
|
|
493
297
|
const sig = crypto.sign(null, decisionBytes(id, !!allow, note ?? null, at, card), keys().privateKey).toString('base64');
|
|
494
298
|
return writeSpoolFile(dir, `${id}.answer.json`,
|
|
495
299
|
JSON.stringify({ allow: !!allow, note: note ?? null, at, card, sig }));
|