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