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,543 @@
|
|
|
1
|
+
// THE CLOUD'S ACCOUNTS, AS THE MAC KNOWS THEM: BY NAME, NEVER BY CONTENT.
|
|
2
|
+
//
|
|
3
|
+
// A cloud account is one a person ADDED for the cloud, signed in through the
|
|
4
|
+
// vendor's own page, and it need not be any account this Mac uses (2026-10-14:
|
|
5
|
+
// "we shouldn't predefine which accounts to sign up with ... maybe I want to
|
|
6
|
+
// sign in with my work account"). Each is filed under a key of its own
|
|
7
|
+
// (`acct-…`), and a cloud run of an engine takes whichever of that engine's
|
|
8
|
+
// accounts was used least recently (`pickCloudLogin`).
|
|
9
|
+
//
|
|
10
|
+
// docs/cloud/PLAN.md, "Sign-ins live in the cloud". A person signs in to
|
|
11
|
+
// Claude or Codex inside a cloud sandbox, through the vendor's own sign-in,
|
|
12
|
+
// and the login is kept in the cloud bucket as `logins/<engine>/<id>.json`.
|
|
13
|
+
// The Mac keeps only a note per account in ~/.agentbox/cloud-logins.json:
|
|
14
|
+
// which object is current, when it was saved, when a Codex one runs out, and
|
|
15
|
+
// whether it stopped working. Nothing here ever reads a login object; it only
|
|
16
|
+
// asks for links to one, which the sandbox uses.
|
|
17
|
+
//
|
|
18
|
+
// Three things the Mac does with that note:
|
|
19
|
+
// - decides whether an account's threads may go to the cloud
|
|
20
|
+
// (`cloudLoginState`);
|
|
21
|
+
// - runs the sign-in from the cloud menu (`startCloudSignIn`), which is a
|
|
22
|
+
// sandbox session the Mac only relays a link and a code to;
|
|
23
|
+
// - renews a Codex login before it runs out (`renewCloudCodexLogin`), one
|
|
24
|
+
// at a time per account, fenced across an app restart.
|
|
25
|
+
import fs from 'node:fs';
|
|
26
|
+
import os from 'node:os';
|
|
27
|
+
import path from 'node:path';
|
|
28
|
+
import crypto from 'node:crypto';
|
|
29
|
+
import { wayBack, listenForWayBack } from './browser-return.mjs';
|
|
30
|
+
|
|
31
|
+
// A Codex access token lasts 240 hours. One with under 3 days left is
|
|
32
|
+
// renewed; one with under 9 hours (an 8-hour run and its homecoming) does not
|
|
33
|
+
// start a run until it has been.
|
|
34
|
+
export const CODEX_RENEW_BEFORE_MS = 3 * 24 * 3600_000;
|
|
35
|
+
export const CODEX_RUN_NEEDS_MS = 9 * 3600_000;
|
|
36
|
+
// A renewal that started this long ago and never said how it ended is over.
|
|
37
|
+
const RENEWAL_FENCE_MS = 10 * 60_000;
|
|
38
|
+
// An object a newer sign-in replaced is deleted this long after, so a run
|
|
39
|
+
// that was handed its link a moment before still finds it.
|
|
40
|
+
const OLD_LOGIN_GRACE_MS = 60 * 60_000;
|
|
41
|
+
const LINK_SECONDS = 30 * 60;
|
|
42
|
+
|
|
43
|
+
export const loginObjectKey = (engine, id) => `logins/${engine}/${id}.json`;
|
|
44
|
+
export const cloudLoginsPath = (home = os.homedir()) => path.join(home, '.agentbox', 'cloud-logins.json');
|
|
45
|
+
// The key is the account's own (`acct-…`), or, in older notes and tests, a
|
|
46
|
+
// folder; either way it is used exactly as given.
|
|
47
|
+
const accountOf = (engine, key) => `${engine}:${key}`;
|
|
48
|
+
export const newCloudAccountKey = () => `acct-${crypto.randomUUID().slice(0, 8)}`;
|
|
49
|
+
|
|
50
|
+
export function readCloudLogins({ home = os.homedir() } = {}) {
|
|
51
|
+
try {
|
|
52
|
+
const all = JSON.parse(fs.readFileSync(cloudLoginsPath(home), 'utf8'));
|
|
53
|
+
return all && typeof all === 'object' ? all : {};
|
|
54
|
+
} catch {
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function writeCloudLogins(all, { home = os.homedir() } = {}) {
|
|
60
|
+
const file = cloudLoginsPath(home);
|
|
61
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
62
|
+
const tmp = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
63
|
+
fs.writeFileSync(tmp, JSON.stringify(all, null, 2), { mode: 0o600 });
|
|
64
|
+
fs.renameSync(tmp, file);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ONE WRITER AT A TIME. The supervisor and every cloud run's own process
|
|
68
|
+
// write this note; a check of the current login and the write after it must
|
|
69
|
+
// be one step, or an old run's report lands over a new sign-in (Codex's
|
|
70
|
+
// review, finding 2). A lock file, taken exclusively; one left by a process
|
|
71
|
+
// that died is stale after ten seconds.
|
|
72
|
+
function withLock(home, fn) {
|
|
73
|
+
const lock = `${cloudLoginsPath(home)}.lock`;
|
|
74
|
+
fs.mkdirSync(path.dirname(lock), { recursive: true });
|
|
75
|
+
const until = Date.now() + 5000;
|
|
76
|
+
const pause = new Int32Array(new SharedArrayBuffer(4));
|
|
77
|
+
for (;;) {
|
|
78
|
+
try {
|
|
79
|
+
fs.closeSync(fs.openSync(lock, 'wx'));
|
|
80
|
+
break;
|
|
81
|
+
} catch (err) {
|
|
82
|
+
if (err.code !== 'EEXIST') throw err;
|
|
83
|
+
try { if (Date.now() - fs.statSync(lock).mtimeMs > 10_000) { fs.rmSync(lock, { force: true }); continue; } } catch { continue; }
|
|
84
|
+
if (Date.now() > until) throw new Error('the cloud sign-in note is busy');
|
|
85
|
+
Atomics.wait(pause, 0, 0, 15);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
try { return fn(); } finally { fs.rmSync(lock, { force: true }); }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function cloudLoginFor(engine, dir, { home = os.homedir() } = {}) {
|
|
92
|
+
return readCloudLogins({ home })[accountOf(engine, dir)] ?? null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// A new sign-in becomes the account's current one. What it replaced is kept
|
|
96
|
+
// on a list to delete once no run can still be about to fetch it.
|
|
97
|
+
export function recordCloudLogin(engine, dir, { id, savedAt = Date.now(), expiresAt = null, email = null, org = null, limits = null }, { home = os.homedir(), now = Date.now() } = {}) {
|
|
98
|
+
return withLock(home, () => {
|
|
99
|
+
const all = readCloudLogins({ home });
|
|
100
|
+
const key = accountOf(engine, dir);
|
|
101
|
+
const was = all[key];
|
|
102
|
+
const retired = [...(was?.retired || [])];
|
|
103
|
+
if (was?.id && was.id !== id) retired.push({ id: was.id, at: now });
|
|
104
|
+
// What the cloud read off a reply at sign-in: whose organization, and the usage.
|
|
105
|
+
const read = org || limits?.length ? { org: org ?? null, limits: limits ?? [], readAt: now } : {};
|
|
106
|
+
all[key] = { engine, key: dir, id, savedAt, expiresAt, email: email ?? was?.email ?? null, broken: null, renewing: null, lastUsedAt: was?.lastUsedAt ?? null, retired, ...read };
|
|
107
|
+
writeCloudLogins(all, { home });
|
|
108
|
+
return all[key];
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// A change to the account's note that applies only while `id` is still its
|
|
113
|
+
// current login: an old run that reports trouble cannot undo a new sign-in.
|
|
114
|
+
export function markCloudLogin(engine, dir, id, patch, { home = os.homedir() } = {}) {
|
|
115
|
+
return withLock(home, () => {
|
|
116
|
+
const all = readCloudLogins({ home });
|
|
117
|
+
const key = accountOf(engine, dir);
|
|
118
|
+
if (!all[key] || (id != null && all[key].id !== id)) return false;
|
|
119
|
+
all[key] = { ...all[key], ...patch };
|
|
120
|
+
writeCloudLogins(all, { home });
|
|
121
|
+
return true;
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// THE SAME ACCOUNT ADDED TWICE: records of one engine whose organization
|
|
126
|
+
// (`org`, main/cloud/org.mjs) matches an earlier record's, by when they were
|
|
127
|
+
// saved. Key -> the earlier record's key. A record with no org yet is no one's
|
|
128
|
+
// duplicate.
|
|
129
|
+
function duplicatesIn(records) {
|
|
130
|
+
const first = new Map();
|
|
131
|
+
const dup = new Map();
|
|
132
|
+
for (const r of [...records].sort((a, b) => (a.savedAt || 0) - (b.savedAt || 0) || String(a.key).localeCompare(String(b.key)))) {
|
|
133
|
+
if (!r?.org) continue;
|
|
134
|
+
const id = `${r.engine}:${r.org}`;
|
|
135
|
+
if (first.has(id)) dup.set(r.key ?? r.dir, first.get(id));
|
|
136
|
+
else first.set(id, r.key ?? r.dir);
|
|
137
|
+
}
|
|
138
|
+
return dup;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 'none' (never signed in), 'broken' (sign in again), 'duplicate' (the same
|
|
142
|
+
// account as an earlier one), 'limited' (at its usage limit until
|
|
143
|
+
// `limitedUntil`), 'renew' (Codex: must be renewed before a run), 'stale'
|
|
144
|
+
// (Codex: may run, renewal due), 'ready'.
|
|
145
|
+
export function cloudLoginState(rec, { now = Date.now() } = {}) {
|
|
146
|
+
if (!rec?.id) return 'none';
|
|
147
|
+
if (rec.broken) return 'broken';
|
|
148
|
+
if (rec.duplicateOf) return 'duplicate';
|
|
149
|
+
if (rec.limitedUntil && now < rec.limitedUntil) return 'limited';
|
|
150
|
+
if (rec.engine !== 'codex' || !rec.expiresAt) return 'ready';
|
|
151
|
+
const left = rec.expiresAt - now;
|
|
152
|
+
if (left < CODEX_RUN_NEEDS_MS) return 'renew';
|
|
153
|
+
if (left < CODEX_RENEW_BEFORE_MS) return 'stale';
|
|
154
|
+
return 'ready';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export const mayRunOn = (state) => state === 'ready' || state === 'stale';
|
|
158
|
+
|
|
159
|
+
// Every cloud account of an engine, oldest first.
|
|
160
|
+
export function listCloudLogins(engine, { home = os.homedir() } = {}) {
|
|
161
|
+
const mine = Object.values(readCloudLogins({ home })).filter((r) => r && r.engine === engine && r.id);
|
|
162
|
+
const dup = duplicatesIn(mine);
|
|
163
|
+
return mine
|
|
164
|
+
.map((r) => ({ ...r, key: r.key ?? r.dir, ...(dup.has(r.key ?? r.dir) ? { duplicateOf: dup.get(r.key ?? r.dir) } : {}) }))
|
|
165
|
+
.sort((a, b) => (a.savedAt || 0) - (b.savedAt || 0));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function cloudAccountsFrom(env = process.env) {
|
|
169
|
+
const list = String(env.AGENTBOX_CLOUD_ACCOUNTS || '').split(',').map((s) => s.trim().toLowerCase()).filter(Boolean);
|
|
170
|
+
return list.length ? list : null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// The cloud account that is this Mac's own account, which the app hands each
|
|
174
|
+
// cloud run it starts (Supervisor#_cloudAccountEnv).
|
|
175
|
+
export const CLOUD_ACCOUNT_ENV = { claude: 'AGENTBOX_CLOUD_CLAUDE_ACCOUNT', codex: 'AGENTBOX_CLOUD_CODEX_ACCOUNT' };
|
|
176
|
+
export function cloudAccountFrom(env = process.env, engine = 'claude') {
|
|
177
|
+
return String(env?.[CLOUD_ACCOUNT_ENV[engine]] ?? '').trim() || null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// THE ACCOUNT A CLOUD RUN USES, AND IT IS ONE ACCOUNT (w-dac993a554).
|
|
181
|
+
//
|
|
182
|
+
// It used to be whichever of this engine's accounts was used least recently,
|
|
183
|
+
// so runs spread over every account added and a limit on one simply moved the
|
|
184
|
+
// work to the next. That is one person's work shared over several
|
|
185
|
+
// subscriptions to get past their limits, which is what the Mac stopped doing
|
|
186
|
+
// in `Supervisor#_theOneAccount` and what the cloud stops doing here.
|
|
187
|
+
//
|
|
188
|
+
// The account is the one this Mac runs on (`prefer`), when it is signed in to
|
|
189
|
+
// the cloud, so picking an account in Settings moves the cloud too; otherwise
|
|
190
|
+
// the first one signed in. A usage limit is never a reason to reach for
|
|
191
|
+
// another: `mayRunOn` turns a limited account down, and the cloud runs nothing
|
|
192
|
+
// and the menu says until when (renderer/src/cloud-menu.ts).
|
|
193
|
+
//
|
|
194
|
+
// `only` (emails or keys) narrows it for a copy of the app under test, which
|
|
195
|
+
// shares this list with the person's own app.
|
|
196
|
+
function accountInUse(all, engine, { now, only, prefer }) {
|
|
197
|
+
const dup = duplicatesIn(Object.values(all).filter((r) => r?.engine === engine));
|
|
198
|
+
const named = (r) => !only || [r.email, r.key ?? r.dir].some((n) => n && only.includes(String(n).toLowerCase()));
|
|
199
|
+
// A login that was never finished or has stopped working is not an account
|
|
200
|
+
// at all. A Codex login due a renewal is the account and simply cannot run
|
|
201
|
+
// yet; `Supervisor#_renewCloudLogins` mends that on its own clock.
|
|
202
|
+
const accounts = Object.entries(all)
|
|
203
|
+
.filter(([, r]) => r?.engine === engine && !dup.has(r.key ?? r.dir) && named(r))
|
|
204
|
+
.filter(([, r]) => !['none', 'broken'].includes(cloudLoginState(r, { now })))
|
|
205
|
+
.sort(([, a], [, b]) => (a.savedAt || 0) - (b.savedAt || 0));
|
|
206
|
+
return (prefer && accounts.find(([, r]) => (r.key ?? r.dir) === prefer)) || accounts[0] || null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** The account a run would take now, whether or not it can run. Null with none. */
|
|
210
|
+
export function cloudLoginInUse(engine, { home = os.homedir(), now = Date.now(), only = null, prefer = null } = {}) {
|
|
211
|
+
const hit = accountInUse(readCloudLogins({ home }), engine, { now, only, prefer });
|
|
212
|
+
return hit ? { ...hit[1], key: hit[1].key ?? hit[1].dir } : null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function pickCloudLogin(engine, { home = os.homedir(), now = Date.now(), only = null, prefer = null } = {}) {
|
|
216
|
+
return withLock(home, () => {
|
|
217
|
+
const all = readCloudLogins({ home });
|
|
218
|
+
const hit = accountInUse(all, engine, { now, only, prefer });
|
|
219
|
+
if (!hit || !mayRunOn(cloudLoginState(hit[1], { now }))) return null;
|
|
220
|
+
const [k, r] = hit;
|
|
221
|
+
all[k] = { ...r, lastUsedAt: now };
|
|
222
|
+
writeCloudLogins(all, { home });
|
|
223
|
+
return { ...all[k], key: r.key ?? r.dir };
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const clock = (t) => new Date(t).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' }).replace(/\s/g, '').toLowerCase();
|
|
228
|
+
|
|
229
|
+
/** Why a run found no account to take, as its thread will quote it. */
|
|
230
|
+
export function noCloudAccountSentence(engine, inUse, { now = Date.now() } = {}) {
|
|
231
|
+
const name = engine === 'codex' ? 'Codex' : 'Claude';
|
|
232
|
+
const state = inUse ? cloudLoginState(inUse, { now }) : 'none';
|
|
233
|
+
if (state === 'limited') return `the ${name} account the cloud runs on is at its usage limit until ${clock(inUse.limitedUntil)}`;
|
|
234
|
+
if (state === 'renew') return `the ${name} account the cloud runs on is renewing its sign-in`;
|
|
235
|
+
return `no ${name} account is signed in for the cloud yet (the cloud button)`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// An account she took out of the cloud. Its object goes too, and every older
|
|
239
|
+
// one it replaced (main/cloud/logins.mjs keeps them only for runs under way).
|
|
240
|
+
export async function removeCloudLogin(engine, key, { storage, home = os.homedir() } = {}) {
|
|
241
|
+
const rec = cloudLoginFor(engine, key, { home });
|
|
242
|
+
if (!rec) return false;
|
|
243
|
+
withLock(home, () => {
|
|
244
|
+
const all = readCloudLogins({ home });
|
|
245
|
+
delete all[accountOf(engine, key)];
|
|
246
|
+
writeCloudLogins(all, { home });
|
|
247
|
+
});
|
|
248
|
+
for (const id of [rec.id, ...(rec.retired || []).map((r) => r.id)]) {
|
|
249
|
+
await Promise.resolve(storage?.del?.(loginObjectKey(engine, id))).catch(() => {});
|
|
250
|
+
}
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Delete what newer sign-ins replaced, once their grace is over. Never the
|
|
255
|
+
// current one.
|
|
256
|
+
export async function sweepRetiredLogins(engine, dir, { storage, home = os.homedir(), now = Date.now() } = {}) {
|
|
257
|
+
const rec = cloudLoginFor(engine, dir, { home });
|
|
258
|
+
if (!rec?.retired?.length || !storage?.del) return;
|
|
259
|
+
const keep = [];
|
|
260
|
+
for (const r of rec.retired) {
|
|
261
|
+
if (now - r.at < OLD_LOGIN_GRACE_MS || r.id === rec.id) { keep.push(r); continue; }
|
|
262
|
+
try { await storage.del(loginObjectKey(engine, r.id)); } catch { keep.push(r); }
|
|
263
|
+
}
|
|
264
|
+
markCloudLogin(engine, dir, rec.id, { retired: keep }, { home });
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/* ------------------------------ a session -------------------------------- */
|
|
268
|
+
// One sandbox session that is not a run: a sign-in or a renewal. Starts it,
|
|
269
|
+
// reads its `signin` events until it says how it ended, and lets the session
|
|
270
|
+
// go. `onEvent` hears each event; the session's end is the promise's value.
|
|
271
|
+
async function session({ transport, start, onEvent = () => {}, pollWaitMs = 20_000, isCancelled = () => false }) {
|
|
272
|
+
const sessionId = `agentbox-${start.action}-${start.engine}-${start.runId}`;
|
|
273
|
+
const say = (state, extra = {}) => ({ state, ...extra });
|
|
274
|
+
const invoke = async (payload) => {
|
|
275
|
+
for (let failures = 0; ;) {
|
|
276
|
+
try { return await transport.invoke(sessionId, payload); } catch (err) {
|
|
277
|
+
failures += 1;
|
|
278
|
+
if (failures > 5 || isCancelled()) throw err;
|
|
279
|
+
await new Promise((r) => setTimeout(r, 500 * failures));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
try {
|
|
284
|
+
if (isCancelled()) return say('failed', { message: null });
|
|
285
|
+
const started = await invoke(start);
|
|
286
|
+
if (!started?.ok) return say('failed', { message: 'The cloud could not start the sign-in. Try again.' });
|
|
287
|
+
// A cancel that raced the start: the session that did start is stopped.
|
|
288
|
+
if (isCancelled()) {
|
|
289
|
+
await transport.invoke(sessionId, { action: 'signal', runId: start.runId, signal: 'SIGTERM' }).catch(() => {});
|
|
290
|
+
return say('failed', { message: null });
|
|
291
|
+
}
|
|
292
|
+
let since = 0;
|
|
293
|
+
for (;;) {
|
|
294
|
+
const page = await invoke({ action: 'events', runId: start.runId, since, waitMs: pollWaitMs });
|
|
295
|
+
if (!page?.ok) return say('failed', { message: 'The cloud sign-in went away. Try again.' });
|
|
296
|
+
for (const e of page.events || []) {
|
|
297
|
+
since = Math.max(since, e.seq);
|
|
298
|
+
if (e.kind !== 'signin') continue;
|
|
299
|
+
onEvent(e);
|
|
300
|
+
if (e.state === 'saved' || e.state === 'failed') return e;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
} catch {
|
|
304
|
+
return say('failed', { message: 'The cloud could not be reached. Try again.' });
|
|
305
|
+
} finally {
|
|
306
|
+
await Promise.resolve(transport.stop?.(sessionId)).catch(() => {});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/* ------------------------------ signing in ------------------------------- */
|
|
311
|
+
// THE CLOUD MENU'S SIGN-IN for one account. What the menu draws comes from
|
|
312
|
+
// `status()`: 'starting', then for Claude 'needs-code' with the link to open
|
|
313
|
+
// (the person pastes Claude's code into `code()`), for Codex 'waiting' with
|
|
314
|
+
// the link (the browser comes back to this computer, browser-return.mjs), or,
|
|
315
|
+
// from a cloud image older than that, the link and a one-time code to type on
|
|
316
|
+
// OpenAI's page; then 'checking', and 'saved' or 'failed' with a sentence. A
|
|
317
|
+
// Codex sign-in made with a different ChatGPT account than the one this Mac's
|
|
318
|
+
// folder holds is refused.
|
|
319
|
+
// HOW LONG THE CLOUD MAY CHECK A SIGN-IN AFTER HER PART IS DONE before this
|
|
320
|
+
// Mac stops waiting and says so (2026-10-15: she sat on "Adding…" for over a
|
|
321
|
+
// minute). The cloud gives up on a code Claude does not take at once, and on
|
|
322
|
+
// one it never answers in a minute (cloud/sandbox/server.mjs); this is the
|
|
323
|
+
// backstop for a cloud that never says, past its own two-minute check.
|
|
324
|
+
export const CHECK_MS = 150_000;
|
|
325
|
+
// The cloud hands the browser's return to Codex and follows its redirect,
|
|
326
|
+
// each with its own deadline (90 and 30 seconds); past both, it is not coming.
|
|
327
|
+
const RETURN_MS = 125_000;
|
|
328
|
+
|
|
329
|
+
function within(ms, promise) {
|
|
330
|
+
let timer;
|
|
331
|
+
const late = new Promise((_, no) => { timer = setTimeout(() => no(new Error('no answer')), ms); });
|
|
332
|
+
return Promise.race([promise, late]).finally(() => clearTimeout(timer));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function startCloudSignIn({ transport, storage, engine, dir, home = os.homedir(), expectEmail = null, onChange = () => {}, onSaved = () => {}, pollWaitMs, checkMs = CHECK_MS }) {
|
|
336
|
+
const id = crypto.randomUUID();
|
|
337
|
+
const runId = `signin-${id}`;
|
|
338
|
+
const sessionId = `agentbox-signin-${engine}-${runId}`;
|
|
339
|
+
// `attempt` tells one sign-in from the next, which can show the same link.
|
|
340
|
+
let status = { state: 'starting', url: null, code: null, message: null, attempt: runId };
|
|
341
|
+
let cancelled = false;
|
|
342
|
+
let gaveUp = false;
|
|
343
|
+
let checkTimer = null;
|
|
344
|
+
let back = null;
|
|
345
|
+
const letBackGo = () => { back?.close(); back = null; };
|
|
346
|
+
const giveUp = (message) => {
|
|
347
|
+
if (gaveUp || status.state === 'saved' || status.state === 'failed') return;
|
|
348
|
+
gaveUp = true;
|
|
349
|
+
cancelled = true;
|
|
350
|
+
letBackGo();
|
|
351
|
+
status = { ...status, state: 'failed', message };
|
|
352
|
+
onChange(status);
|
|
353
|
+
transport.invoke(sessionId, { action: 'signal', runId, signal: 'SIGTERM' }).catch(() => {});
|
|
354
|
+
};
|
|
355
|
+
const set = (patch) => {
|
|
356
|
+
status = { ...status, ...patch };
|
|
357
|
+
// The check's clock starts when her part ends, and stops when it ends.
|
|
358
|
+
if (status.state === 'checking' && !checkTimer) {
|
|
359
|
+
checkTimer = setTimeout(() => {
|
|
360
|
+
if (status.state === 'checking') giveUp(`${engine === 'codex' ? 'Codex' : 'Claude'} didn’t finish signing in. Try again.`);
|
|
361
|
+
}, checkMs);
|
|
362
|
+
checkTimer.unref?.();
|
|
363
|
+
} else if (status.state !== 'checking' && checkTimer) {
|
|
364
|
+
clearTimeout(checkTimer);
|
|
365
|
+
checkTimer = null;
|
|
366
|
+
}
|
|
367
|
+
onChange(status);
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
// The browser's return, passed to the cloud. Once taken the port goes, so
|
|
371
|
+
// another Codex account can be added while this one is checked; one not
|
|
372
|
+
// taken, short of OpenAI refusing it, leaves the sign-in waiting.
|
|
373
|
+
const handOn = async (query) => {
|
|
374
|
+
if (status.state !== 'waiting') return false;
|
|
375
|
+
set({ state: 'checking', message: null });
|
|
376
|
+
let out = null;
|
|
377
|
+
try { out = await within(RETURN_MS, transport.invoke(sessionId, { action: 'signin-callback', runId, query })); } catch { /* not delivered */ }
|
|
378
|
+
if (out?.ok) letBackGo();
|
|
379
|
+
else if (out?.error !== 'refused' && status.state === 'checking') set({ state: 'waiting' });
|
|
380
|
+
return !!out?.ok;
|
|
381
|
+
};
|
|
382
|
+
// The way back is open before the page is shown, so it is there first.
|
|
383
|
+
const openWayBack = async (url) => {
|
|
384
|
+
const at = wayBack(url);
|
|
385
|
+
if (!at) return giveUp('Codex’s sign-in link had no way back. Try again.');
|
|
386
|
+
try {
|
|
387
|
+
back = await listenForWayBack({ ...at, onReturn: handOn });
|
|
388
|
+
} catch {
|
|
389
|
+
return giveUp('Another Codex sign-in is open on this computer. Close it, then try again.');
|
|
390
|
+
}
|
|
391
|
+
if (cancelled || status.state !== 'starting') return letBackGo();
|
|
392
|
+
set({ state: 'waiting', url, code: null });
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
const done = (async () => {
|
|
396
|
+
const saveUrl = await storage.putUrl(loginObjectKey(engine, id), LINK_SECONDS);
|
|
397
|
+
const end = await session({
|
|
398
|
+
transport,
|
|
399
|
+
pollWaitMs,
|
|
400
|
+
isCancelled: () => cancelled,
|
|
401
|
+
start: { action: 'signin', runId, engine, saveUrl, ...(engine === 'codex' ? { flow: 'browser' } : {}) },
|
|
402
|
+
onEvent: (e) => {
|
|
403
|
+
if (e.state === 'needs-code') set({ state: 'needs-code', url: e.url });
|
|
404
|
+
else if (e.state === 'waiting' && engine === 'codex' && !e.code) void openWayBack(e.url);
|
|
405
|
+
else if (e.state === 'waiting') set({ state: 'waiting', url: e.url, code: e.code });
|
|
406
|
+
else if (e.state === 'checking') set({ state: 'checking' });
|
|
407
|
+
},
|
|
408
|
+
}).finally(letBackGo);
|
|
409
|
+
// Given up on here already: that sentence stands.
|
|
410
|
+
if (gaveUp) return status;
|
|
411
|
+
if (end.state !== 'saved') {
|
|
412
|
+
set({ state: 'failed', message: cancelled ? null : (end.message || 'That did not work. Try again.') });
|
|
413
|
+
return status;
|
|
414
|
+
}
|
|
415
|
+
if (engine === 'codex' && expectEmail && end.email && end.email.toLowerCase() !== String(expectEmail).toLowerCase()) {
|
|
416
|
+
await Promise.resolve(storage.del?.(loginObjectKey(engine, id))).catch(() => {});
|
|
417
|
+
set({ state: 'failed', message: `That signed in as ${end.email}. Sign in with ${expectEmail} instead.` });
|
|
418
|
+
return status;
|
|
419
|
+
}
|
|
420
|
+
recordCloudLogin(engine, dir, { id, expiresAt: end.expiresAt ?? null, email: end.email ?? null, org: end.org ?? null, limits: end.limits ?? null }, { home });
|
|
421
|
+
set({ state: 'saved', url: null, code: null });
|
|
422
|
+
try { onSaved(); } catch { /* the menu redraws anyway */ }
|
|
423
|
+
return status;
|
|
424
|
+
})().catch((err) => {
|
|
425
|
+
letBackGo();
|
|
426
|
+
set({ state: 'failed', message: `That did not work (${err.message}). Try again.` });
|
|
427
|
+
return status;
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
return {
|
|
431
|
+
status: () => status,
|
|
432
|
+
done,
|
|
433
|
+
async code(text) {
|
|
434
|
+
const c = String(text || '').trim();
|
|
435
|
+
if (!c || status.state !== 'needs-code') return false;
|
|
436
|
+
set({ state: 'checking', message: null });
|
|
437
|
+
let ok = false;
|
|
438
|
+
try {
|
|
439
|
+
const out = await transport.invoke(sessionId, { action: 'signin-code', runId, code: c });
|
|
440
|
+
ok = !!out?.ok;
|
|
441
|
+
} catch { /* not delivered */ }
|
|
442
|
+
// A code that did not reach the sign-in can be pasted again (finding 7).
|
|
443
|
+
if (!ok && status.state === 'checking') set({ state: 'needs-code', message: 'That code did not reach the sign-in. Paste it again.' });
|
|
444
|
+
return ok;
|
|
445
|
+
},
|
|
446
|
+
async cancel() {
|
|
447
|
+
cancelled = true;
|
|
448
|
+
letBackGo();
|
|
449
|
+
try { await transport.invoke(sessionId, { action: 'signal', runId, signal: 'SIGTERM' }); } catch { /* gone */ }
|
|
450
|
+
},
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/* ------------------------------- renewing -------------------------------- */
|
|
455
|
+
// ONE CODEX LOGIN RENEWED: a sandbox session that renews it from the real
|
|
456
|
+
// login and keeps the renewed one as a new object. Fenced by a `renewing`
|
|
457
|
+
// mark, so an app restarted mid-renewal does not start a second one on the
|
|
458
|
+
// same refresh token. Returns 'renewed', 'busy', 'signed-out' or 'failed'.
|
|
459
|
+
export async function renewCloudCodexLogin({ transport, storage, dir, home = os.homedir(), now = Date.now(), pollWaitMs }) {
|
|
460
|
+
const rec = cloudLoginFor('codex', dir, { home });
|
|
461
|
+
if (!rec?.id || rec.broken) return 'failed';
|
|
462
|
+
const was = typeof rec.renewing === 'number' ? { at: rec.renewing, id: null } : rec.renewing;
|
|
463
|
+
if (was && now - was.at < RENEWAL_FENCE_MS) return 'busy';
|
|
464
|
+
// A RENEWAL THE APP NEVER HEARD THE END OF (it quit, or the Mac slept):
|
|
465
|
+
// the renewed login may be in the cloud already, under the id written down
|
|
466
|
+
// before it started. If it is, it is the current one, and the old refresh
|
|
467
|
+
// token, which renewing spent, is never used again (finding 3).
|
|
468
|
+
if (was?.id && await Promise.resolve(storage.exists?.(loginObjectKey('codex', was.id))).catch(() => false)) {
|
|
469
|
+
recordCloudLogin('codex', dir, { id: was.id, expiresAt: was.at + 239 * 3600_000, email: rec.email }, { home, now });
|
|
470
|
+
return 'renewed';
|
|
471
|
+
}
|
|
472
|
+
const id = crypto.randomUUID();
|
|
473
|
+
markCloudLogin('codex', dir, rec.id, { renewing: { at: now, id } }, { home });
|
|
474
|
+
const end = await session({
|
|
475
|
+
transport,
|
|
476
|
+
pollWaitMs,
|
|
477
|
+
start: {
|
|
478
|
+
action: 'renew',
|
|
479
|
+
runId: `renew-${id}`,
|
|
480
|
+
engine: 'codex',
|
|
481
|
+
loginUrl: await storage.getUrl(loginObjectKey('codex', rec.id), LINK_SECONDS),
|
|
482
|
+
saveUrl: await storage.putUrl(loginObjectKey('codex', id), LINK_SECONDS),
|
|
483
|
+
},
|
|
484
|
+
});
|
|
485
|
+
if (end.state === 'saved') {
|
|
486
|
+
recordCloudLogin('codex', dir, { id, expiresAt: end.expiresAt ?? null, email: rec.email }, { home, now: Date.now() });
|
|
487
|
+
return 'renewed';
|
|
488
|
+
}
|
|
489
|
+
if (end.signedOut) {
|
|
490
|
+
markCloudLogin('codex', dir, rec.id, { broken: 'signed-out', renewing: null }, { home });
|
|
491
|
+
return 'signed-out';
|
|
492
|
+
}
|
|
493
|
+
markCloudLogin('codex', dir, rec.id, { renewing: null }, { home });
|
|
494
|
+
return 'failed';
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/* ------------------------------- looking --------------------------------- */
|
|
498
|
+
// A look's session and week, in the shape the usage meters draw. Null with none.
|
|
499
|
+
export function cloudLimits(read) {
|
|
500
|
+
if (!Array.isArray(read) || !read.length) return null;
|
|
501
|
+
return read.filter((l) => Number.isFinite(l?.percent)).map((l) => ({
|
|
502
|
+
span: l.span === 'week' ? 'week' : 'session',
|
|
503
|
+
qualifier: l.span === 'week' ? 'all models' : null,
|
|
504
|
+
name: l.span === 'week' ? 'This week' : 'This session',
|
|
505
|
+
// A session over its allowance reports utilization past 1.0.
|
|
506
|
+
percent: Math.min(100, l.percent), resetsText: null, resetsOn: null, resetsAt: l.resetsAt ?? null, zone: null,
|
|
507
|
+
}));
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// WHOSE ONE CLAUDE LOGIN IS, AND HOW MUCH OF IT IS LEFT: a sandbox session
|
|
511
|
+
// that makes one tiny reply on it and reads the reply's headers. Kept on the
|
|
512
|
+
// login's note only while that login is still the current one. A failed look
|
|
513
|
+
// keeps the last reading and notes why beside it. Returns 'read' or 'failed'.
|
|
514
|
+
export async function lookAtCloudLogin({ transport, storage, key, home = os.homedir(), now = Date.now(), pollWaitMs }) {
|
|
515
|
+
const rec = cloudLoginFor('claude', key, { home });
|
|
516
|
+
if (!rec?.id || rec.broken) return 'failed';
|
|
517
|
+
const end = await session({
|
|
518
|
+
transport,
|
|
519
|
+
pollWaitMs,
|
|
520
|
+
start: { action: 'look', runId: `look-${crypto.randomUUID()}`, engine: 'claude', loginUrl: await storage.getUrl(loginObjectKey('claude', rec.id), LINK_SECONDS) },
|
|
521
|
+
});
|
|
522
|
+
if (end.state !== 'saved') {
|
|
523
|
+
markCloudLogin('claude', key, rec.id, { lookFailedAt: now, lookError: end.message ?? null }, { home });
|
|
524
|
+
return 'failed';
|
|
525
|
+
}
|
|
526
|
+
markCloudLogin('claude', key, rec.id, { org: end.org ?? null, limits: end.limits ?? [], readAt: now }, { home });
|
|
527
|
+
return 'read';
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/* ------------------------ the first build's tokens ----------------------- */
|
|
531
|
+
// The first cloud build kept a Claude token per account in the macOS keychain
|
|
532
|
+
// (service below) or, elsewhere, in ~/.agentbox/cloud-tokens.json. Those were
|
|
533
|
+
// the app's own copies for the cloud, never Claude Code's own login, and
|
|
534
|
+
// nothing reads them now. Deleted once, on start.
|
|
535
|
+
export const OLD_KEYCHAIN_SERVICE_SUFFIX = 'cloud: Claude sign-in';
|
|
536
|
+
export function forgetOldCloudTokens({ home = os.homedir(), platform = process.platform, service, exec } = {}) {
|
|
537
|
+
try { fs.rmSync(path.join(home, '.agentbox', 'cloud-tokens.json'), { force: true }); } catch { /* none */ }
|
|
538
|
+
if (platform !== 'darwin' || !service || !exec) return;
|
|
539
|
+
// One entry per account; delete until there are none (at most 20).
|
|
540
|
+
for (let i = 0; i < 20; i += 1) {
|
|
541
|
+
try { exec('security', ['delete-generic-password', '-s', service]); } catch { break; }
|
|
542
|
+
}
|
|
543
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// THE CLOUD THROUGH OUR SERVER, FOR A MAC WITH NO AWS ACCOUNT OF ITS OWN.
|
|
2
|
+
//
|
|
3
|
+
// The same two interfaces as main/cloud/aws.mjs (the session and the bucket),
|
|
4
|
+
// spoken to the cloud server (cloud/gateway/) with this Mac's cloud key. Files
|
|
5
|
+
// go straight to and from the bucket on links the server signs; the server
|
|
6
|
+
// only ever sees names. A sign-in is never read from here: a link to one is
|
|
7
|
+
// answered with its name, which the server turns into a link only inside a
|
|
8
|
+
// call to the sandbox.
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import crypto from 'node:crypto';
|
|
11
|
+
import { Readable } from 'node:stream';
|
|
12
|
+
import { pipeline } from 'node:stream/promises';
|
|
13
|
+
import { LOGIN_NAME } from '../../shared/cloud-login-name.mjs';
|
|
14
|
+
|
|
15
|
+
export { LOGIN_NAME };
|
|
16
|
+
|
|
17
|
+
// Longer than the server's own deadline (scripts/cloud-gateway-setup.mjs),
|
|
18
|
+
// so a slow start or collect is not given up here while it succeeds there. A
|
|
19
|
+
// file of a few hundred MB on a slow network takes minutes.
|
|
20
|
+
export const CALL_MS = 100_000;
|
|
21
|
+
const FILE_MS = 15 * 60_000;
|
|
22
|
+
const LINK_SECONDS = 12 * 60 * 60;
|
|
23
|
+
// Lambda takes at most 6 MB; a call past this goes through the bucket.
|
|
24
|
+
const RELAY_BYTES = 4 * 1024 * 1024;
|
|
25
|
+
|
|
26
|
+
const isLogin = (key) => String(key).startsWith('logins/');
|
|
27
|
+
|
|
28
|
+
export function managedClients({ url, key, timeoutMs = CALL_MS, fileTimeoutMs = FILE_MS, onSignedOut = () => {} }) {
|
|
29
|
+
const base = String(url).replace(/\/+$/, '');
|
|
30
|
+
|
|
31
|
+
async function call(method, path, body) {
|
|
32
|
+
const init = {
|
|
33
|
+
method,
|
|
34
|
+
headers: { authorization: `Bearer ${key}`, ...(body ? { 'content-type': 'application/json' } : {}) },
|
|
35
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
36
|
+
};
|
|
37
|
+
const res = await fetch(`${base}${path}`, { ...init, signal: AbortSignal.timeout(timeoutMs) });
|
|
38
|
+
const out = await res.json().catch(() => ({}));
|
|
39
|
+
if (!res.ok) {
|
|
40
|
+
if (res.status === 401) { try { onSignedOut(); } catch { /* the refusal below says it all the same */ } }
|
|
41
|
+
throw Object.assign(new Error(`the cloud server said ${res.status}: ${out.message || out.error || 'no reason'}`), {
|
|
42
|
+
status: res.status, error: out.error, said: out.message, signedOut: res.status === 401,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
const linkFor = async (op, k, seconds = LINK_SECONDS) => (await call('POST', '/v1/link', { op, key: k, seconds })).url;
|
|
48
|
+
const object = (link, init = {}) => fetch(link, { ...init, signal: AbortSignal.timeout(fileTimeoutMs) });
|
|
49
|
+
const refuseLogin = (k) => { if (isLogin(k)) throw new Error('a cloud sign-in is never read on this Mac'); };
|
|
50
|
+
|
|
51
|
+
async function invoke(sessionId, payload) {
|
|
52
|
+
let out;
|
|
53
|
+
try {
|
|
54
|
+
const text = JSON.stringify({ sessionId, payload });
|
|
55
|
+
if (Buffer.byteLength(text) <= RELAY_BYTES) out = await call('POST', '/v1/invoke', { sessionId, payload });
|
|
56
|
+
else {
|
|
57
|
+
const payloadKey = `runs/calls/${crypto.randomUUID()}.json`;
|
|
58
|
+
await storage.put(payloadKey, JSON.stringify(payload));
|
|
59
|
+
out = await call('POST', '/v1/invoke', { sessionId, payloadKey });
|
|
60
|
+
}
|
|
61
|
+
} catch (err) {
|
|
62
|
+
// A refusal is an answer: run.mjs retries what throws, some calls for
|
|
63
|
+
// ever. A poll refused ends as a session that is gone does. A throttle
|
|
64
|
+
// (429) or a server failure passes, so those are thrown and retried.
|
|
65
|
+
if (!err.status || err.status === 429 || err.status >= 500) throw err;
|
|
66
|
+
return { ok: false, error: payload?.action === 'events' ? 'no-run' : err.error || 'refused', message: err.message };
|
|
67
|
+
}
|
|
68
|
+
if (!out?.relayed) return out;
|
|
69
|
+
const res = await object(out.relayed.url);
|
|
70
|
+
if (!res.ok) throw new Error(`the bucket refused the cloud's answer: ${res.status}`);
|
|
71
|
+
const answer = await res.json();
|
|
72
|
+
await call('POST', '/v1/delete', { key: out.relayed.key }).catch(() => {});
|
|
73
|
+
return answer;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const transport = {
|
|
77
|
+
invoke,
|
|
78
|
+
stop: (sessionId) => call('POST', '/v1/stop', { sessionId }),
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const storage = {
|
|
82
|
+
async put(k, body) {
|
|
83
|
+
const res = await object(await linkFor('put', k), { method: 'PUT', body });
|
|
84
|
+
if (!res.ok) throw new Error(`the bucket refused ${k}: ${res.status}`);
|
|
85
|
+
},
|
|
86
|
+
async putFile(k, file) { await storage.put(k, fs.readFileSync(file)); },
|
|
87
|
+
async get(k) {
|
|
88
|
+
refuseLogin(k);
|
|
89
|
+
const res = await object(await linkFor('get', k));
|
|
90
|
+
if (res.status === 404) return null;
|
|
91
|
+
if (!res.ok) throw new Error(`the bucket refused ${k}: ${res.status}`);
|
|
92
|
+
return Buffer.from(await res.arrayBuffer());
|
|
93
|
+
},
|
|
94
|
+
async getToFile(k, file) {
|
|
95
|
+
refuseLogin(k);
|
|
96
|
+
const res = await object(await linkFor('get', k));
|
|
97
|
+
if (res.status === 404) return false;
|
|
98
|
+
if (!res.ok) throw new Error(`the bucket refused ${k}: ${res.status}`);
|
|
99
|
+
await pipeline(Readable.fromWeb(res.body), fs.createWriteStream(file));
|
|
100
|
+
return true;
|
|
101
|
+
},
|
|
102
|
+
async exists(k) {
|
|
103
|
+
const res = await object(await linkFor('head', k), { method: 'HEAD' });
|
|
104
|
+
if (res.status === 404) return false;
|
|
105
|
+
if (!res.ok) throw new Error(`the bucket refused ${k}: ${res.status}`);
|
|
106
|
+
return true;
|
|
107
|
+
},
|
|
108
|
+
async del(k) { await call('POST', '/v1/delete', { key: k }); },
|
|
109
|
+
getUrl: async (k, seconds) => (isLogin(k) ? `${LOGIN_NAME}${k}` : linkFor('get', k, seconds)),
|
|
110
|
+
// A sign-in is saved by the sandbox on a link the server makes from its name.
|
|
111
|
+
putUrl: async (k, seconds) => (isLogin(k) ? `${LOGIN_NAME}${k}` : linkFor('put', k, seconds)),
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
return { transport, storage, me: () => call('GET', '/v1/me') };
|
|
115
|
+
}
|