openmausbot 0.1.96 → 0.1.98
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/dist/assets/ActivityPanel-H6D3SD39.js +1 -0
- package/dist/assets/BotSettingsDialog-Dqz_AiBZ.js +7 -0
- package/dist/assets/CloudBackendPicker-iezpJFpF.js +1 -0
- package/dist/assets/ComputerPanel-DZPj-IWu.js +1 -0
- package/dist/assets/DesktopViewer-utONms1p.js +5 -0
- package/dist/assets/InspectorPanel-OVaBVrzC.js +5 -0
- package/dist/assets/KeyboardShortcutsModal-gk9mvHj_.js +1 -0
- package/dist/assets/LocalVmWorkspace-DLIvd2kC.js +1 -0
- package/dist/assets/MacLocalControl-bJv8yiPB.js +1 -0
- package/dist/assets/NewBotDialog-1qGOAdUM.js +2 -0
- package/dist/assets/RemoteAgentSettingsPanel-DqKr0HkO.js +1 -0
- package/dist/assets/RoutinesPage-BM2D4qGi.js +1 -0
- package/dist/assets/RoutinesSection-Dny9ra-c.js +1 -0
- package/dist/assets/SettingsModal-gC6XJJmt.js +11 -0
- package/dist/assets/TeamMapPage-B6fuu4r_.js +8 -0
- package/dist/assets/TriggersPanel-j0qTP2CO.js +1 -0
- package/dist/assets/VisibilitySection-DT0FqPvu.js +12 -0
- package/dist/assets/WebhooksPanel--YQyCjKO.js +3 -0
- package/dist/assets/{abnfDiagram-O67JEVCF-Dkbd2hcj.js → abnfDiagram-O67JEVCF-BsjcFX0-.js} +1 -1
- package/dist/assets/activity-BxOkayvD.js +1 -0
- package/dist/assets/{arc-Duj5Coy2.js → arc-CJNA94c6.js} +1 -1
- package/dist/assets/architectureDiagram-NJMV4G6O-dz2ud7lP.js +36 -0
- package/dist/assets/{blockDiagram-BEXU5L5S-CWDlTpXe.js → blockDiagram-BEXU5L5S-ChBEALoR.js} +1 -1
- package/dist/assets/brain-CFZ7PEXc.js +1 -0
- package/dist/assets/{c4Diagram-YGBWAQC7-B0t15PDc.js → c4Diagram-YGBWAQC7-KZNRKDQe.js} +1 -1
- package/dist/assets/channel-WhFyujhZ.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BJSCL-HV.js → chunk-2Q5K7J3B-BLLwmCET.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-D3_ILccd.js → chunk-3FUC2YCW-CQxyreBZ.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-D3dmwT_S.js → chunk-5VM5RSS4-D6sFMW0u.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-Br73HFPD.js → chunk-GTNCS2PH-BaZ1YgLF.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-uSsjxubC.js → chunk-GWA4HPMP-CcalphmC.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-C40VWFyr.js → chunk-JWPE2WC7-CSk8R3ve.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-D6WNNYwU.js → chunk-NETBCI7D-CTmLjA57.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-DOEhq64P.js → chunk-WEXAMYUT-CM9Z77Ne.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-BqJEi6Pi.js → chunk-XXDRQBXY-B1fxpMS2.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-ByU_GnxT.js → classDiagram-v2-NBCMYWYE-DCCyoLpj.js} +1 -1
- package/dist/assets/computer-control-NLov352p.js +1 -0
- package/dist/assets/cose-bilkent-JH36ORCC-KyImBKvE.js +1 -0
- package/dist/assets/{cynefin-OW5HDTMX-CZWO7aYj.js → cynefin-OW5HDTMX-fsZc6MGa.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-C70-w0la.js → cynefinDiagram-VND7K2PF-CGmHsShB.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-CnhjhExB.js → dagre-6A5THRUB-Dah1h-CH.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-bnjWCNb4.js → diagram-22UHCM2B-BIu71OyV.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-A4uTWoFV.js → diagram-3UASUU5V-gxhUB_Mu.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-Zi6m3NUl.js → diagram-ATOU4E4O-CBhusUBn.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-DtCcmd5k.js → diagram-CDSNMT55-BdPrc86x.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-DofuETS3.js → diagram-MLGK6HIB-Clp110R_.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-Dw2T9PuU.js → diagram-MPIPVDR6-DJKfbugw.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-DbrwPiem.js → ebnfDiagram-ZINNZB2B-DnsltoBu.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-BvCt1dCb.js → elk-276RUBZZ-jWdq6gqH.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-Y-QKqPnU.js → erDiagram-OPXOYQCR-DArPVf8u.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-CDkL1B5w.js → flowDiagram-KWPJA3E3-D2HZ-2f7.js} +1 -1
- package/dist/assets/ganttDiagram-FUAMR5RP--o0KIxEo.js +292 -0
- package/dist/assets/{gitGraphDiagram-X574FWY7-nL9iFTCv.js → gitGraphDiagram-X574FWY7-CsoXr2D7.js} +1 -1
- package/dist/assets/image-plus-BbVjGHin.js +1 -0
- package/dist/assets/index-BrPW5HDO.css +1 -0
- package/dist/assets/index-C3NqfdVU.js +585 -0
- package/dist/assets/{index-DreDnjOu.js → index-CJTp9JEp.js} +1 -1
- package/dist/assets/{infoDiagram-VRGFBTTK-C7aHjzsf.js → infoDiagram-VRGFBTTK-g-6lNp7r.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-KUZsNBTh.js → ishikawaDiagram-OU5B5YK6-B1d6qVTV.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-CGUuodFl.js → journeyDiagram-ZHPQQLJL-BTPuiU6T.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-Cy6r868Z.js → kanban-definition-PNTS6WVX-bDDuRJvP.js} +1 -1
- package/dist/assets/{linear-Bl669148.js → linear-BVVKbEMy.js} +1 -1
- package/dist/assets/{mermaid.core-86Xqz5Mr.js → mermaid.core-JJdoKyKB.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-G4DzHLrd.js → mindmap-definition-NLK3R4M7-B4aE3T0q.js} +1 -1
- package/dist/assets/module-DCUsy2BQ.js +3 -0
- package/dist/assets/page-visible-BBo_Zm8g.js +1 -0
- package/dist/assets/{pegDiagram-GJSIUBJH-K4eruzY0.js → pegDiagram-GJSIUBJH-DFKQWpR7.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-DYdR9PPq.js → pieDiagram-5QR66LMP-Iv7OKVJi.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-BCAh0nEy.js → quadrantDiagram-O4NWA36T-U2YnfwBk.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-p0dGRc4g.js → railroadDiagram-XR7U4H2S-CuPy6RqM.js} +1 -1
- package/dist/assets/remote-desktop-panel-BNEj7tUx.js +1 -0
- package/dist/assets/{requirementDiagram-PLB6GJNP-tLeBG0aL.js → requirementDiagram-PLB6GJNP-bMI5AK7T.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-QRuN16rV.js → sankeyDiagram-IPEJSGJF-BKcFE2WQ.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-ueT1nVoa.js → sequenceDiagram-PO4LG4MO-Bn9q88kl.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-wRS2zWg2.js → sizeCapture-INFHLROL-Cvmdq9HD.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-CYzA_hE1.js → stateDiagram-v2-GCMORJYK-JJQYw3eQ.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-kDnv6LMc.js → swimlanes-2SLR337P-BkSCBEwp.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-CmTirbVT.js +8 -0
- package/dist/assets/table-file.worker-KkMx-SNx.js +10 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-D1y2UbbH.js → timeline-definition-EJHVYXUP-DsaM4GHr.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-IqB4v9HX.js → usecaseDiagram-POWQR4AR-3FOwLq7B.js} +1 -1
- package/dist/assets/user-DcZBnAuv.js +1 -0
- package/dist/assets/{vennDiagram-UO4OBE2U-BEi7KYVF.js → vennDiagram-UO4OBE2U-DyNjJsKt.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-Dkybo-Yf.js → wardleyDiagram-VNRHLVJA-Y4ZMcnBC.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-BAPFJLpg.js → xychartDiagram-PMCCYNJV-BXZpg955.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/cloud-home-start.js +20 -9
- package/dist-server/container-mcp.js +8 -0
- package/dist-server/desktop-entry.mjs +54 -0
- package/dist-server/drivers/agents-proxy.js +26 -9
- package/dist-server/index.js +3734 -2360
- package/dist-server/openmausbot.js +1664 -1553
- package/dist-server/pair-cli.js +1664 -1553
- package/dist-server/permission-proxy.js +2 -4
- package/dist-server/server/atomic.js +70 -2
- package/dist-server/server/auto-approve.js +1 -1
- package/dist-server/server/auto-vm-claims.js +13 -1
- package/dist-server/server/automatic-recovery.js +1 -2
- package/dist-server/server/boat.js +68 -84
- package/dist-server/server/bot-overview.js +1 -1
- package/dist-server/server/browser-engine.js +38 -1
- package/dist-server/server/browser-live.js +112 -25
- package/dist-server/server/browser-runtime.js +31 -1
- package/dist-server/server/checkpoints.js +275 -44
- package/dist-server/server/cli-setup.js +3 -3
- package/dist-server/server/cloud-computer-tools.js +11 -4
- package/dist-server/server/cloud-home-start.js +4 -4
- package/dist-server/server/cloud-home.js +3 -9
- package/dist-server/server/cloud-owner.js +1 -1
- package/dist-server/server/computer-engine-removal.js +70 -0
- package/dist-server/server/computer-selection.js +18 -0
- package/dist-server/server/config.js +29 -12
- package/dist-server/server/connector-card-text.js +15 -0
- package/dist-server/server/connector-scope-hint.js +83 -0
- package/dist-server/server/default-model-selection.js +23 -14
- package/dist-server/server/delegations.js +11 -5
- package/dist-server/server/delta-context.js +6 -1
- package/dist-server/server/drivers/acp/core.js +11 -1
- package/dist-server/server/drivers/acp/grok.js +47 -5
- package/dist-server/server/drivers/acp/opencode-go.js +16 -21
- package/dist-server/server/drivers/agents-call.js +1 -1
- package/dist-server/server/drivers/agents-catalog.js +27 -7
- package/dist-server/server/drivers/agents-proxy.js +1 -0
- package/dist-server/server/drivers/builtIn.js +0 -2
- package/dist-server/server/drivers/chat-mcp-tools.js +5 -3
- package/dist-server/server/drivers/claude-login-auth.js +1 -1
- package/dist-server/server/drivers/claude.js +84 -41
- package/dist-server/server/drivers/codex-device-auth.js +27 -319
- package/dist-server/server/drivers/codex.js +2 -2
- package/dist-server/server/drivers/device-auth.js +342 -0
- package/dist-server/server/drivers/openai-chat.js +110 -10
- package/dist-server/server/drivers/pi.js +184 -49
- package/dist-server/server/fleet-agent.js +18 -5
- package/dist-server/server/harness/http.js +5 -2
- package/dist-server/server/harness/registry.js +19 -0
- package/dist-server/server/harness-capabilities.js +7 -4
- package/dist-server/server/hosted-models.js +3 -1
- package/dist-server/server/index.js +823 -333
- package/dist-server/server/key-rejections.js +57 -0
- package/dist-server/server/live-call.js +11 -6
- package/dist-server/server/managed-policy.js +1 -1
- package/dist-server/server/message-file.js +7 -4
- package/dist-server/server/model-catalog/presets.js +8 -0
- package/dist-server/server/peer-roster.js +10 -2
- package/dist-server/server/permission-proxy.js +4 -0
- package/dist-server/server/provider-key-check.js +5 -1
- package/dist-server/server/request-auth.js +1 -1
- package/dist-server/server/room-turn-end.js +15 -0
- package/dist-server/server/room-turn-timeout.js +28 -0
- package/dist-server/server/routes/live.js +3 -3
- package/dist-server/server/routes/thread-models.js +36 -0
- package/dist-server/server/routine-requests.js +1 -1
- package/dist-server/server/routines.js +3 -3
- package/dist-server/server/screen-frame-gate.js +1 -3
- package/dist-server/server/skills.js +3 -2
- package/dist-server/server/store.js +273 -61
- package/dist-server/server/surface.js +29 -49
- package/dist-server/server/system-prompt.js +10 -10
- package/dist-server/server/team-backup.js +19 -4
- package/dist-server/server/thread-model.js +25 -0
- package/dist-server/server/turn-continuation.js +168 -0
- package/dist-server/server/webhook-ingress.js +8 -3
- package/dist-server/server/webhooks.js +40 -5
- package/dist-server/server/workspace.js +1 -1
- package/dist-server/shared/ask-question.js +0 -70
- package/dist-server/shared/cloud-computer.js +4 -6
- package/dist-server/shared/credential-request.js +1 -1
- package/dist-server/shared/device-sign-in.js +57 -0
- package/dist-server/shared/failed-turn.js +1 -4
- package/dist-server/shared/place-view.js +409 -0
- package/dist-server/shared/team-backup.js +2 -0
- package/dist-server/shared/thread-model.js +13 -0
- package/dist-server/shared/tool-scope-support.js +1 -2
- package/dist-server/vps-container-mcp.js +8 -0
- package/dist-server/workspace-backup.worker.js +41 -22
- package/package.json +1 -1
- package/dist/assets/DesktopViewer-DsnA23KM.js +0 -5
- package/dist/assets/architectureDiagram-NJMV4G6O-uYUsDawP.js +0 -36
- package/dist/assets/channel-BZxwC-Gw.js +0 -1
- package/dist/assets/cose-bilkent-JH36ORCC-D-Uglxqq.js +0 -1
- package/dist/assets/ganttDiagram-FUAMR5RP-CKV5D2F6.js +0 -292
- package/dist/assets/index-Du7o6Dsf.js +0 -610
- package/dist/assets/index-KxG4bWSr.css +0 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-C7voMDle.js +0 -8
- package/dist-server/server/drivers/boatagent.js +0 -443
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/** The removed engine's driver kind ("boxAgent" was Boat's historical name). */
|
|
2
|
+
export const REMOVED_COMPUTER_DRIVER = "boxAgent";
|
|
3
|
+
/** The instance id every default fleet gave it. */
|
|
4
|
+
const REMOVED_COMPUTER_INSTANCE = "computer";
|
|
5
|
+
/** Instance ids that named the Computer engine. A saved fleet may hold the
|
|
6
|
+
* default id or one of its own; an id the person reused for another engine
|
|
7
|
+
* is theirs and stays. */
|
|
8
|
+
export function removedComputerInstanceIds(saved) {
|
|
9
|
+
const ids = new Set();
|
|
10
|
+
const reused = saved && Object.hasOwn(saved, REMOVED_COMPUTER_INSTANCE) &&
|
|
11
|
+
saved[REMOVED_COMPUTER_INSTANCE].driver !== REMOVED_COMPUTER_DRIVER;
|
|
12
|
+
if (!reused)
|
|
13
|
+
ids.add(REMOVED_COMPUTER_INSTANCE);
|
|
14
|
+
for (const [id, entry] of Object.entries(saved ?? {})) {
|
|
15
|
+
if (entry.driver === REMOVED_COMPUTER_DRIVER)
|
|
16
|
+
ids.add(id);
|
|
17
|
+
}
|
|
18
|
+
return ids;
|
|
19
|
+
}
|
|
20
|
+
/** A fleet without the removed engine: a saved entry for it is dropped, so
|
|
21
|
+
* it is never offered and the next save leaves it off disk. */
|
|
22
|
+
export function withoutComputerEngine(map) {
|
|
23
|
+
for (const [id, entry] of Object.entries(map)) {
|
|
24
|
+
if (entry.driver === REMOVED_COMPUTER_DRIVER)
|
|
25
|
+
delete map[id];
|
|
26
|
+
}
|
|
27
|
+
return map;
|
|
28
|
+
}
|
|
29
|
+
/** The line a moved conversation shows, in plain words. It names the removed
|
|
30
|
+
* choice as people saw it in the model list ("Computer", not the panel's
|
|
31
|
+
* Computer tab), claims a cloud computer only where one is still used, says
|
|
32
|
+
* so when the new engine can't use the one it had, and names the one setting
|
|
33
|
+
* to change when a turn there would be refused. */
|
|
34
|
+
export function computerEngineMoveText(move, botName, engine) {
|
|
35
|
+
const removed = "The Computer choice in the model list was removed.";
|
|
36
|
+
const bot = move.scope !== "conversation";
|
|
37
|
+
const head = move.scope === "conversation" ? `This conversation now uses ${engine}.`
|
|
38
|
+
: move.scope === "bot-only" ? `${botName} now uses ${engine}; this conversation keeps its own model.`
|
|
39
|
+
: `${botName} now uses ${engine}.`;
|
|
40
|
+
const noComputer = ` ${engine} can't use a computer, so`;
|
|
41
|
+
const place = move.cloud
|
|
42
|
+
? bot ? ` ${botName} still works on its cloud computer.` : ` It still works on ${botName}'s cloud computer.`
|
|
43
|
+
: move.noComputer === "auto"
|
|
44
|
+
? `${noComputer} ${bot ? `${botName} no longer works on its` : `it no longer works on ${botName}'s`}` +
|
|
45
|
+
" cloud computer. To use it again, choose a model that can use a computer."
|
|
46
|
+
: move.noComputer === "works-on"
|
|
47
|
+
? `${noComputer} ${botName} can't reply on ${engine} while its Works on is Cloud computer.` +
|
|
48
|
+
" Choose a model that can use a computer, or set Works on to Auto."
|
|
49
|
+
: move.noComputer === "pin"
|
|
50
|
+
? `${noComputer} ${botName} can't reply here while this conversation is pinned to its cloud computer.` +
|
|
51
|
+
" Choose a model that can use a computer, or clear this conversation's place in the composer."
|
|
52
|
+
: "";
|
|
53
|
+
const ask = !move.askNow ? ""
|
|
54
|
+
: move.scope === "bot-only" ? ` ${botName}'s permissions are now Ask, but this conversation's stay as they were.`
|
|
55
|
+
: " Its permissions are now Ask.";
|
|
56
|
+
return `${head} ${removed}${place}${ask}`;
|
|
57
|
+
}
|
|
58
|
+
/** Writes each moved conversation's line. The move is saved by then, so a
|
|
59
|
+
* later try never finds these conversations again: a line that fails is
|
|
60
|
+
* logged, and every other line is still written. */
|
|
61
|
+
export function writeComputerEngineMoveLines(moves, write) {
|
|
62
|
+
for (const move of moves) {
|
|
63
|
+
try {
|
|
64
|
+
write(move);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
console.warn(`[engines] the Computer engine move line for ${move.threadId} was not written: ${error instanceof Error ? error.message : String(error)}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** The option a request picks, and whether it is the computer this turn
|
|
2
|
+
* already has. `current` is the turn's mounted place. `startsOnFirstCall`
|
|
3
|
+
* says that place's computer is the one this turn's first computer call
|
|
4
|
+
* creates or wakes (the bot's own cloud computer): it is this turn's before
|
|
5
|
+
* it is up, so asking for it, or for Auto, keeps it instead of restarting
|
|
6
|
+
* the request somewhere else. */
|
|
7
|
+
export function pickComputer(options, requested, current, startsOnFirstCall) {
|
|
8
|
+
const isCurrent = (option) => option.surface === current && (option.ready || (startsOnFirstCall && option.available));
|
|
9
|
+
const option = requested === "auto"
|
|
10
|
+
? options.find(isCurrent)
|
|
11
|
+
?? options.find((candidate) => candidate.ready && candidate.surface === "vm")
|
|
12
|
+
?? options.find((candidate) => candidate.ready)
|
|
13
|
+
?? options.find((candidate) => candidate.canStart)
|
|
14
|
+
?? options.find((candidate) => candidate.canCreate && candidate.surface === "vm")
|
|
15
|
+
?? options.find((candidate) => candidate.canCreate)
|
|
16
|
+
: options.find((candidate) => candidate.surface === requested);
|
|
17
|
+
return { option, current: option !== undefined && isCurrent(option) };
|
|
18
|
+
}
|
|
@@ -7,6 +7,7 @@ import { join } from "node:path";
|
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
import { normalizeImageGenerationUrl } from "../shared/image-generation.js";
|
|
9
9
|
import { writeFileAtomic } from "./atomic.js";
|
|
10
|
+
import { withoutComputerEngine } from "./computer-engine-removal.js";
|
|
10
11
|
import { newBotDefaultsSchema } from "./new-bot-defaults.js";
|
|
11
12
|
import { EFFORT_LEVELS } from "../shared/wire.js";
|
|
12
13
|
import { isModelVariant } from "./contracts.js";
|
|
@@ -23,6 +24,13 @@ export const FISH_TTS_MODELS = ["s2.1-pro", "s2.1-pro-free"];
|
|
|
23
24
|
export const DEFAULT_ROOM_TURN_TIMEOUT_MINUTES = 5;
|
|
24
25
|
export const MIN_ROOM_TURN_TIMEOUT_MINUTES = 1;
|
|
25
26
|
export const MAX_ROOM_TURN_TIMEOUT_MINUTES = 1_440;
|
|
27
|
+
/** Per-call ceiling for a bot's MCP tools (tools/call on the chat MCP
|
|
28
|
+
* transport). 10 minutes matches the historic constant in
|
|
29
|
+
* chat-mcp-tools.ts; a single tool call that runs longer is cut at this
|
|
30
|
+
* deadline because its execution outcome can no longer be trusted. */
|
|
31
|
+
export const DEFAULT_MCP_CALL_TIMEOUT_MINUTES = 10;
|
|
32
|
+
export const MIN_MCP_CALL_TIMEOUT_MINUTES = 1;
|
|
33
|
+
export const MAX_MCP_CALL_TIMEOUT_MINUTES = 60;
|
|
26
34
|
export const DEFAULT_ROOM_HANDOFF_LIFETIME_MINUTES = 30;
|
|
27
35
|
export const DEFAULT_ROOM_HANDOFF_MIN_RUNWAY_MINUTES = 10;
|
|
28
36
|
export const DEFAULT_ROOM_HANDOFF_HARD_CAP_MINUTES = 240;
|
|
@@ -104,6 +112,16 @@ const roomConfigSchema = z.object({
|
|
|
104
112
|
(rooms.handoffLifetimeMinutes ?? DEFAULT_ROOM_HANDOFF_LIFETIME_MINUTES) &&
|
|
105
113
|
(rooms.handoffLifetimeMinutes ?? DEFAULT_ROOM_HANDOFF_LIFETIME_MINUTES) <=
|
|
106
114
|
(rooms.handoffHardCapMinutes ?? DEFAULT_ROOM_HANDOFF_HARD_CAP_MINUTES), { message: "rooms handoff bounds must satisfy handoffMinRunwayMinutes <= handoffLifetimeMinutes <= handoffHardCapMinutes" });
|
|
115
|
+
const mcpConfigSchema = z.object({
|
|
116
|
+
/** Ceiling (minutes) for one bot MCP tool call. Missing values resolve to
|
|
117
|
+
* 10, preserving the historic chat-mcp-tools constant. */
|
|
118
|
+
callTimeoutMinutes: z
|
|
119
|
+
.number()
|
|
120
|
+
.int()
|
|
121
|
+
.min(MIN_MCP_CALL_TIMEOUT_MINUTES)
|
|
122
|
+
.max(MAX_MCP_CALL_TIMEOUT_MINUTES)
|
|
123
|
+
.optional(),
|
|
124
|
+
}).strict();
|
|
107
125
|
/** Isolation for bot desktops. Migration note (issue #1654): switching
|
|
108
126
|
* modes changes lease keys and vm-home directories, so desktops cold-start
|
|
109
127
|
* under the new mode — a pool seat lives in vm-homes/pool-N — while the old
|
|
@@ -502,6 +520,7 @@ const appConfigSchema = z.object({
|
|
|
502
520
|
* system language. Unknown tags degrade to English in the renderer. */
|
|
503
521
|
language: optionalText,
|
|
504
522
|
rooms: roomConfigSchema.optional(),
|
|
523
|
+
mcp: mcpConfigSchema.optional(),
|
|
505
524
|
context: z.object({
|
|
506
525
|
rebuildBytes: z.number().int().min(1_024).max(1_000_000).optional(),
|
|
507
526
|
compactAt: z.number().positive().max(10_000_000).optional(),
|
|
@@ -634,6 +653,9 @@ export function browserEngineAttachCdpUrl(cfg) {
|
|
|
634
653
|
export function roomTurnTimeoutMinutes(cfg) {
|
|
635
654
|
return cfg.rooms?.turnTimeoutMinutes ?? DEFAULT_ROOM_TURN_TIMEOUT_MINUTES;
|
|
636
655
|
}
|
|
656
|
+
export function mcpCallTimeoutMinutes(cfg) {
|
|
657
|
+
return cfg.mcp?.callTimeoutMinutes ?? DEFAULT_MCP_CALL_TIMEOUT_MINUTES;
|
|
658
|
+
}
|
|
637
659
|
export const LIVE_IDLE_MINUTES_DEFAULT = 5;
|
|
638
660
|
/** Non-secret Live settings. The key only shows up as `configured`. */
|
|
639
661
|
export function liveSettingsFor(cfg) {
|
|
@@ -1203,7 +1225,7 @@ export function saveConfig(patch, options = {}) {
|
|
|
1203
1225
|
const storedProfiles = storedBrowserProfilesSchema.safeParse(disk.browserProfiles);
|
|
1204
1226
|
if (storedProfiles.success)
|
|
1205
1227
|
disk.browserProfiles = storedProfiles.data;
|
|
1206
|
-
for (const key of ["xai", "anthropic", "mistral", "cerebras", "openai", "openrouter", "openaiCompat", "composio", "box", "opencodeGo", "tts", "decider", "imageGen", "live", "profile", "rooms", "threads", "context", "memory", "localVm", "features", "budgets", "billing", "decisions", "onboarding", "browserEngine", "newBots"]) {
|
|
1228
|
+
for (const key of ["xai", "anthropic", "mistral", "cerebras", "openai", "openrouter", "openaiCompat", "composio", "box", "opencodeGo", "tts", "decider", "imageGen", "live", "profile", "rooms", "mcp", "threads", "context", "memory", "localVm", "features", "budgets", "billing", "decisions", "onboarding", "browserEngine", "newBots"]) {
|
|
1207
1229
|
const section = checkedPatch[key];
|
|
1208
1230
|
if (!section)
|
|
1209
1231
|
continue;
|
|
@@ -1443,10 +1465,10 @@ export function instanceOwnsRouting(cfg, entry, routingDefaults) {
|
|
|
1443
1465
|
}
|
|
1444
1466
|
/** The credential env instanceConfigs() injects for one driver at runtime.
|
|
1445
1467
|
* Each secret goes only to the driver that actually reads it: the API-key
|
|
1446
|
-
* Grok driver reads XAI_API_KEY
|
|
1447
|
-
*
|
|
1448
|
-
*
|
|
1449
|
-
*
|
|
1468
|
+
* Grok driver reads XAI_API_KEY and OpenCode reads OPENCODE_API_KEY. Every
|
|
1469
|
+
* other engine brings its own login, so handing it a key it never uses would
|
|
1470
|
+
* only put that key in the environment of an unrelated child process. The
|
|
1471
|
+
* Boat token reaches no engine at all: the harness alone talks to Boat. */
|
|
1450
1472
|
function injectedEnvironment(cfg, instanceId, driver) {
|
|
1451
1473
|
const environment = new Map();
|
|
1452
1474
|
if (driver === "mistral" && cfg.mistral?.key)
|
|
@@ -1485,11 +1507,6 @@ function injectedEnvironment(cfg, instanceId, driver) {
|
|
|
1485
1507
|
if (driver === "openai-compat" && cfg.openaiCompat?.url)
|
|
1486
1508
|
environment.set("OPENAI_COMPAT_URL", cfg.openaiCompat.url);
|
|
1487
1509
|
}
|
|
1488
|
-
// driverKind "boxAgent" and env BOX_TOKEN keep their historical names.
|
|
1489
|
-
// Only the person's own token: without one the driver itself falls back
|
|
1490
|
-
// to Cloud Pro's included token, which never enters an environment map.
|
|
1491
|
-
if (driver === "boxAgent" && cfg.box?.token)
|
|
1492
|
-
environment.set("BOX_TOKEN", cfg.box.token);
|
|
1493
1510
|
if (driver === "opencodeGo") {
|
|
1494
1511
|
// Keys for OpenCode's other providers, under the names OpenCode reads.
|
|
1495
1512
|
for (const [name, key] of Object.entries(openCodeProviderKeys(cfg)))
|
|
@@ -1551,7 +1568,6 @@ export function instanceConfigs(cfg) {
|
|
|
1551
1568
|
chatgpt: { driver: "codex", displayName: "ChatGPT plan", config: { authMode: "chatgpt-plan" } },
|
|
1552
1569
|
antigravity: { driver: "antigravityAgent" },
|
|
1553
1570
|
opencodeGo: { driver: "opencodeGo" },
|
|
1554
|
-
computer: { driver: "boxAgent" },
|
|
1555
1571
|
openaiCompat: { driver: "openai-compat" },
|
|
1556
1572
|
mistral: { driver: "mistral" },
|
|
1557
1573
|
cerebras: { driver: "cerebras" },
|
|
@@ -1578,7 +1594,8 @@ export function instanceConfigs(cfg) {
|
|
|
1578
1594
|
...CUSTOM_ONLY,
|
|
1579
1595
|
};
|
|
1580
1596
|
const configured = cfg.instances && Object.keys(cfg.instances).length ? cfg.instances : null;
|
|
1581
|
-
|
|
1597
|
+
// A fleet saved before the Computer engine was removed may still name it.
|
|
1598
|
+
const map = withoutComputerEngine(configured ? { ...configured } : { ...DEFAULT_FLEET });
|
|
1582
1599
|
// Product fleets pick up newly shipped engines. A one-off test/shadow map
|
|
1583
1600
|
// (no claude/grok/codex) is left exactly as written.
|
|
1584
1601
|
if (configured &&
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// The plain line a connection card carries beside its `connector` payload.
|
|
2
|
+
//
|
|
3
|
+
// Desktop and current phones draw `kind: "connector"` as a card and never
|
|
4
|
+
// read this. It exists for clients that do not know the kind yet — phone
|
|
5
|
+
// builds from before the in-chat card shipped decode it as "unknown" and draw
|
|
6
|
+
// whatever `text` a message has, so without it they showed nothing at all
|
|
7
|
+
// while the bot sat waiting for a connection. Exports and backups read it too.
|
|
8
|
+
// It names the app and points at the card; it never carries a link, because
|
|
9
|
+
// the authorization URL is only ever handed to the person's own client.
|
|
10
|
+
/** "Connect GitHub to continue." — or, for a second account, the alias too. */
|
|
11
|
+
export function connectorCardText(label, alias) {
|
|
12
|
+
const app = label.replace(/\s+/g, " ").trim() || "this app";
|
|
13
|
+
const account = alias?.replace(/\s+/g, " ").trim();
|
|
14
|
+
return account ? `Connect ${app} as “${account}” to continue.` : `Connect ${app} to continue.`;
|
|
15
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// A connected-app call refused for a missing OAuth permission (MOCA-273).
|
|
2
|
+
//
|
|
3
|
+
// Google answers 403 ACCESS_TOKEN_SCOPE_INSUFFICIENT when the account's grant
|
|
4
|
+
// lacks a scope the action needs, such as gmail.settings.basic for a filter.
|
|
5
|
+
// Composio's default auth config for a toolkit never asks for that scope, so
|
|
6
|
+
// reconnecting cannot fix it and a bot that only sees the 403 retries or
|
|
7
|
+
// asks the person to reconnect. The fix is the person's own auth config in
|
|
8
|
+
// their Composio project, which OpenMausBot already prefers
|
|
9
|
+
// (listCustomAuthConfigs). This appends that explanation to the tool result,
|
|
10
|
+
// leaving the provider's own error untouched.
|
|
11
|
+
const SCOPE_ERROR = /ACCESS_TOKEN_SCOPE_INSUFFICIENT|insufficient authentication scopes|insufficientPermissions/i;
|
|
12
|
+
/** The Google scope a known tool needs, when the tool name says which. */
|
|
13
|
+
function requiredScope(slug) {
|
|
14
|
+
if (!slug.startsWith("GMAIL_"))
|
|
15
|
+
return null;
|
|
16
|
+
if (/FORWARD|SEND_AS|DELEGATE/.test(slug))
|
|
17
|
+
return "https://www.googleapis.com/auth/gmail.settings.sharing";
|
|
18
|
+
if (/FILTER|VACATION|AUTO_REPLY|IMAP|POP|LANGUAGE/.test(slug))
|
|
19
|
+
return "https://www.googleapis.com/auth/gmail.settings.basic";
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
function appName(slug) {
|
|
23
|
+
const toolkit = slug.split("_")[0] ?? "";
|
|
24
|
+
if (toolkit === "GMAIL")
|
|
25
|
+
return "Gmail";
|
|
26
|
+
if (toolkit.startsWith("GOOGLE"))
|
|
27
|
+
return "Google";
|
|
28
|
+
return toolkit.charAt(0) + toolkit.slice(1).toLowerCase();
|
|
29
|
+
}
|
|
30
|
+
/** The note a bot reads after its call was refused for a missing permission. */
|
|
31
|
+
export function scopeHint(slugs) {
|
|
32
|
+
const slug = slugs.find((candidate) => requiredScope(candidate)) ?? slugs[0] ?? "";
|
|
33
|
+
const app = slug ? appName(slug) : "This app";
|
|
34
|
+
const scope = slug ? requiredScope(slug) : null;
|
|
35
|
+
const permission = scope ? `the ${scope} permission` : "the permission this action needs";
|
|
36
|
+
return `OpenMausBot note: ${app} refused this because the connected account has not granted ${permission}. ` +
|
|
37
|
+
`Reconnecting will not add it: the default Composio connection never asks for it. ` +
|
|
38
|
+
`To allow it, the person creates their own ${app} auth config in their Composio project that includes ${permission}, ` +
|
|
39
|
+
`then reconnects ${app} under Connected apps; OpenMausBot uses that config automatically. ` +
|
|
40
|
+
`Tell the person this. Do not retry this call.`;
|
|
41
|
+
}
|
|
42
|
+
/** Adds the note to one JSON-RPC frame whose tool result is a scope refusal. */
|
|
43
|
+
function annotateFrame(frame, slugs) {
|
|
44
|
+
const result = frame?.result;
|
|
45
|
+
if (!result || !Array.isArray(result.content))
|
|
46
|
+
return false;
|
|
47
|
+
const refused = result.content.some((item) => item && typeof item === "object" && typeof item.text === "string" &&
|
|
48
|
+
SCOPE_ERROR.test(item.text));
|
|
49
|
+
if (!refused)
|
|
50
|
+
return false;
|
|
51
|
+
result.content.push({ type: "text", text: scopeHint(slugs) });
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The relayed MCP response, with the note added when a tool call came back
|
|
56
|
+
* refused for a missing OAuth permission. Anything else, including a body it
|
|
57
|
+
* cannot read, is returned byte for byte.
|
|
58
|
+
*/
|
|
59
|
+
export function withScopeHint(bytes, contentType, slugs) {
|
|
60
|
+
const text = new TextDecoder().decode(bytes);
|
|
61
|
+
if (!SCOPE_ERROR.test(text))
|
|
62
|
+
return bytes;
|
|
63
|
+
try {
|
|
64
|
+
if (/text\/event-stream/i.test(contentType)) {
|
|
65
|
+
let changed = false;
|
|
66
|
+
const lines = text.split("\n").map((line) => {
|
|
67
|
+
if (!line.startsWith("data:"))
|
|
68
|
+
return line;
|
|
69
|
+
const frame = JSON.parse(line.slice(5));
|
|
70
|
+
if (!annotateFrame(frame, slugs))
|
|
71
|
+
return line;
|
|
72
|
+
changed = true;
|
|
73
|
+
return `data: ${JSON.stringify(frame)}`;
|
|
74
|
+
});
|
|
75
|
+
return changed ? new TextEncoder().encode(lines.join("\n")) : bytes;
|
|
76
|
+
}
|
|
77
|
+
const frame = JSON.parse(text);
|
|
78
|
+
return annotateFrame(frame, slugs) ? new TextEncoder().encode(JSON.stringify(frame)) : bytes;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return bytes;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
const claudeFirst = (instances) => instances.find((instance) => instance.driverKind === "claudeAgent") ?? instances[0];
|
|
2
|
-
/**
|
|
3
|
-
*
|
|
2
|
+
/** Installed and waiting on a sign-in. A custom endpoint brings its own
|
|
3
|
+
* credential, so its sign-in state does not count. */
|
|
4
|
+
export function signedOut(instance) {
|
|
5
|
+
return instance.snapshot.state === "available" && instance.access !== "custom" && instance.snapshot.authenticated === false;
|
|
6
|
+
}
|
|
7
|
+
/** The ready rule: an engine can run a turn now when it is available, the
|
|
8
|
+
* organisation allows it, and it is not signed out (signedOut). */
|
|
9
|
+
export function readyToRun(instance, context = {}) {
|
|
10
|
+
return instance.snapshot.state === "available" && context.refusal?.(instance) === undefined && !signedOut(instance);
|
|
11
|
+
}
|
|
12
|
+
/** The engine a new bot gets, and the one a bot moved off a removed engine
|
|
13
|
+
* gets (computer-engine-removal.ts): one picker for both.
|
|
14
|
+
*
|
|
15
|
+
* A saved choice is intentional: an unavailable provider or removed model
|
|
16
|
+
* sends new bots to setup instead of silently changing their provider.
|
|
17
|
+
* Without one, an engine that can run a turn now wins (Claude first), then the
|
|
18
|
+
* first available one. On an enrolled desktop a working personal engine
|
|
19
|
+
* still beats a Company one, so billing stays the person's explicit choice. */
|
|
4
20
|
export function selectDefaultModelSelection(instances, preferred, context = {}) {
|
|
5
21
|
if (preferred) {
|
|
6
22
|
const instance = instances.find((candidate) => candidate.instanceId === preferred.instanceId);
|
|
@@ -22,19 +38,12 @@ export function selectDefaultModelSelection(instances, preferred, context = {})
|
|
|
22
38
|
}
|
|
23
39
|
// The organisation's policy is inert (no refusal) unless this desktop is enrolled.
|
|
24
40
|
const available = instances.filter((instance) => instance.snapshot.state === "available" && context.refusal?.(instance) === undefined);
|
|
41
|
+
const ready = available.filter((instance) => readyToRun(instance, context));
|
|
25
42
|
const company = context.company;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
// can run a turn now; a working personal engine still wins, so billing
|
|
31
|
-
// stays the person's explicit choice.
|
|
32
|
-
const ready = available.filter((instance) => instance.access === "custom" || instance.snapshot.authenticated !== false);
|
|
33
|
-
pick = claudeFirst(ready.filter((instance) => !company(instance.instanceId))) ?? claudeFirst(ready) ?? claudeFirst(available);
|
|
34
|
-
}
|
|
35
|
-
else {
|
|
36
|
-
pick = claudeFirst(available);
|
|
37
|
-
}
|
|
43
|
+
const personal = company && instances.some((instance) => company(instance.instanceId))
|
|
44
|
+
? claudeFirst(ready.filter((instance) => !company(instance.instanceId)))
|
|
45
|
+
: undefined;
|
|
46
|
+
const pick = personal ?? claudeFirst(ready) ?? claudeFirst(available);
|
|
38
47
|
return { instanceId: pick?.instanceId ?? "", model: pick?.models.default ?? "" };
|
|
39
48
|
}
|
|
40
49
|
/** Complete a new bot's selection with the workspace's new-bot effort. An
|
|
@@ -195,6 +195,8 @@ export function _loadPending() {
|
|
|
195
195
|
}
|
|
196
196
|
if (item.oneWay === true)
|
|
197
197
|
loaded.oneWay = true;
|
|
198
|
+
if (item.completionOwner === "external")
|
|
199
|
+
loaded.completionOwner = "external";
|
|
198
200
|
return [loaded];
|
|
199
201
|
});
|
|
200
202
|
if (items.length)
|
|
@@ -401,7 +403,7 @@ onSettled) {
|
|
|
401
403
|
if (outcome === "settled" && stillQueued) {
|
|
402
404
|
const receipt = findDelegationReceipt(item.id);
|
|
403
405
|
try {
|
|
404
|
-
if (receipt &&
|
|
406
|
+
if (receipt && wakesSource(item))
|
|
405
407
|
onSettled?.(receipt);
|
|
406
408
|
}
|
|
407
409
|
catch (error) {
|
|
@@ -445,6 +447,10 @@ function appendDeliveryMessage(bus, sourceThreadId, item, message) {
|
|
|
445
447
|
return;
|
|
446
448
|
bus.store.appendMessage(threadId, message);
|
|
447
449
|
}
|
|
450
|
+
/** A settled handoff resumes the bot that queued it, unless its outcome
|
|
451
|
+
* belongs elsewhere: to a one-way send's recipient, or to an external
|
|
452
|
+
* runtime that polls for the receipt. */
|
|
453
|
+
const wakesSource = (item) => !item.oneWay && item.completionOwner !== "external";
|
|
448
454
|
const isExpired = (item, now) => now - item.queuedAt >= DELEGATION_TTL_MS;
|
|
449
455
|
/** Past the busy-hold cap — the tighter bound that fires while its target is
|
|
450
456
|
* still busy, long before the 24-hour window. */
|
|
@@ -528,15 +534,15 @@ export function expireStaleDelegations(bus, now, onSettled) {
|
|
|
528
534
|
expireDelegation(bus, threadId, item, ownerId ?? item.sourceBotId);
|
|
529
535
|
const receipt = findDelegationReceipt(item.id);
|
|
530
536
|
if (receipt)
|
|
531
|
-
expired.push({ receipt,
|
|
537
|
+
expired.push({ receipt, wake: wakesSource(item) });
|
|
532
538
|
}
|
|
533
539
|
}
|
|
534
540
|
if (!expired.length)
|
|
535
541
|
return 0;
|
|
536
542
|
savePending();
|
|
537
|
-
for (const { receipt,
|
|
543
|
+
for (const { receipt, wake } of expired) {
|
|
538
544
|
try {
|
|
539
|
-
if (
|
|
545
|
+
if (wake)
|
|
540
546
|
onSettled?.(receipt);
|
|
541
547
|
}
|
|
542
548
|
catch (error) {
|
|
@@ -717,7 +723,7 @@ async function processOne(bus, approvalBus, from, sourceThreadId, item, runTarge
|
|
|
717
723
|
const prefixed = item.targetThreadId
|
|
718
724
|
? item.message
|
|
719
725
|
: `[Delegated by @${sender.name}, another bot in this OpenMausBot workspace. Do the work and reply directly.]\n\n${item.message}${reasonLine}`;
|
|
720
|
-
await runTarget(item.toBotId, prefixed, item.oneWay ? 0 : item.depth + 1, sourceThreadId, channel, item.id, sender.id, item.targetThreadId, item.oneWay === true);
|
|
726
|
+
await runTarget(item.toBotId, prefixed, item.oneWay ? 0 : item.depth + 1, sourceThreadId, channel, item.id, sender.id, item.targetThreadId, item.oneWay === true, item.completionOwner);
|
|
721
727
|
return "dispatched";
|
|
722
728
|
}
|
|
723
729
|
/** "Is the target free to take this handoff right now?" Both shapes ask for
|
|
@@ -109,6 +109,7 @@ export function recordHanded(state, order, received) {
|
|
|
109
109
|
return {
|
|
110
110
|
...(state.session === undefined ? {} : { session: state.session }),
|
|
111
111
|
...(state.config === undefined ? {} : { config: state.config }),
|
|
112
|
+
...(state.effort === undefined ? {} : { effort: state.effort }),
|
|
112
113
|
...(state.omitted === undefined ? {} : { omitted: state.omitted }),
|
|
113
114
|
...(through === undefined ? {} : { through }),
|
|
114
115
|
ids: sorted,
|
|
@@ -241,10 +242,14 @@ export class Handoffs {
|
|
|
241
242
|
? pending.resumeCursor === undefined ? pending.started : pending.recovery
|
|
242
243
|
: existing === undefined ? pending.resumed : undefined;
|
|
243
244
|
if (!start) {
|
|
245
|
+
if (typeof pending.effort === "string" && existing?.session === session && existing.effort !== pending.effort) {
|
|
246
|
+
this.store.write(pending.botId, threadId, pending.instanceId, { ...existing, effort: pending.effort });
|
|
247
|
+
}
|
|
244
248
|
this.add(threadId, pending, [...pending.placed, ...pending.carried]);
|
|
245
249
|
return;
|
|
246
250
|
}
|
|
247
|
-
this.store.write(pending.botId, threadId, pending.instanceId, recordHanded({ session, config: pending.config, ...(
|
|
251
|
+
this.store.write(pending.botId, threadId, pending.instanceId, recordHanded({ session, config: pending.config, ...(pending.effort === undefined ? {} : { effort: pending.effort }),
|
|
252
|
+
...(start.omitted === undefined ? {} : { omitted: start.omitted }), ids: [] }, this.store.order(threadId), start.sent));
|
|
248
253
|
}
|
|
249
254
|
add(threadId, pending, received) {
|
|
250
255
|
const session = pending.session ?? pending.resumeCursor;
|
|
@@ -31,6 +31,7 @@ import { basename, dirname, isAbsolute, relative, resolve } from "node:path";
|
|
|
31
31
|
import { createHash } from "node:crypto";
|
|
32
32
|
import { PROVIDER_CREDENTIAL_ENV, stripControlPlaneEnv, WORKSPACE_CREDENTIAL_ENV } from "../../config.js";
|
|
33
33
|
import { decodeInjectId } from "../local-inject.js";
|
|
34
|
+
import { DeviceAuthController } from "../device-auth.js";
|
|
34
35
|
import { deletePromptSplitReceipt, promptHalves, readPromptSplitReceipt, splitSessionPrompt, writePromptSplitReceipt } from "../prompt-split.js";
|
|
35
36
|
import { describeSpawnFailure, execCli, killCliTree, spawnCli } from "../../procs.js";
|
|
36
37
|
import { classifyQuiet, describeQuiet, lastSeenPhrase, LogTail, quietKey, sampleProcessTree, } from "./quiet-status.js";
|
|
@@ -350,7 +351,7 @@ export function createAcpDriver(support) {
|
|
|
350
351
|
}
|
|
351
352
|
// The operator's own secrets are outside any driver's allowlist.
|
|
352
353
|
stripControlPlaneEnv(env);
|
|
353
|
-
support.transformEnv?.(env, activeConfig, instanceId);
|
|
354
|
+
support.transformEnv?.(env, activeConfig, instanceId, input.environment);
|
|
354
355
|
return env;
|
|
355
356
|
};
|
|
356
357
|
let models = support.models;
|
|
@@ -373,6 +374,9 @@ export function createAcpDriver(support) {
|
|
|
373
374
|
};
|
|
374
375
|
if (support.resolveModelsOnCreate !== false)
|
|
375
376
|
await refreshModels();
|
|
377
|
+
const deviceSignIn = support.deviceSignIn
|
|
378
|
+
? new DeviceAuthController(support.deviceSignIn, { cli: config.cli, environment: () => childEnv(), onAuthenticated: refreshModels })
|
|
379
|
+
: null;
|
|
376
380
|
const listeners = new Set();
|
|
377
381
|
const active = new Map();
|
|
378
382
|
// One live agent process per thread, kept across turns — the Claude
|
|
@@ -1904,6 +1908,11 @@ export function createAcpDriver(support) {
|
|
|
1904
1908
|
return models;
|
|
1905
1909
|
},
|
|
1906
1910
|
refreshModels: support.resolveModels ? refreshModels : undefined,
|
|
1911
|
+
...(deviceSignIn ? {
|
|
1912
|
+
startAuthentication: () => deviceSignIn.start(),
|
|
1913
|
+
getAuthentication: (flowId) => deviceSignIn.get(flowId),
|
|
1914
|
+
cancelAuthentication: () => deviceSignIn.cancel(),
|
|
1915
|
+
} : {}),
|
|
1907
1916
|
snapshot,
|
|
1908
1917
|
adapter: {
|
|
1909
1918
|
provider: DRIVER_KIND,
|
|
@@ -1950,6 +1959,7 @@ export function createAcpDriver(support) {
|
|
|
1950
1959
|
},
|
|
1951
1960
|
},
|
|
1952
1961
|
dispose: async () => {
|
|
1962
|
+
await deviceSignIn?.dispose();
|
|
1953
1963
|
for (const { stop } of active.values())
|
|
1954
1964
|
stop();
|
|
1955
1965
|
for (const threadId of Array.from(sessions.keys()))
|
|
@@ -20,6 +20,45 @@ export const STATIC_GROK_MODELS = {
|
|
|
20
20
|
],
|
|
21
21
|
};
|
|
22
22
|
const SLUG = /^[a-z0-9][a-z0-9._-]*$/i;
|
|
23
|
+
/** Grok's own home: `$GROK_HOME`, else `~/.grok`. Its grok.com sign-in,
|
|
24
|
+
* config and catalog all live here. */
|
|
25
|
+
export function grokHome(env = process.env) {
|
|
26
|
+
return env.GROK_HOME || harnessHome("grok", env);
|
|
27
|
+
}
|
|
28
|
+
/** The grok.com sign-in a turn runs on is stored. */
|
|
29
|
+
export function grokSignedIn(env) {
|
|
30
|
+
return existsSync(join(grokHome(env), "auth.json"));
|
|
31
|
+
}
|
|
32
|
+
function grokLoginFailure(output) {
|
|
33
|
+
if (/(unexpected argument|unrecognized (argument|option)|unknown option).*device-(auth|code)/i.test(output)) {
|
|
34
|
+
return "This server's Grok is too old to sign in with a code. Update Grok on the server, then try again.";
|
|
35
|
+
}
|
|
36
|
+
if (/device-code login is not available/i.test(output)) {
|
|
37
|
+
return "This Grok account can't sign in with a code. Use an xAI API key instead.";
|
|
38
|
+
}
|
|
39
|
+
if (/expired/i.test(output))
|
|
40
|
+
return "The Grok sign-in code expired. Start sign-in again for a new code.";
|
|
41
|
+
if (/denied|rejected/i.test(output))
|
|
42
|
+
return "Grok sign-in was declined in the browser. Start sign-in again to try once more.";
|
|
43
|
+
return "Grok sign-in did not finish. Check the server's connection, then try again.";
|
|
44
|
+
}
|
|
45
|
+
/** Grok Build's half of the in-app sign-in (device-auth.ts): `grok login
|
|
46
|
+
* --device-auth` on the grok.com subscription. Grok has no status command,
|
|
47
|
+
* so nothing is checked first, and a stored login that a failed turn just
|
|
48
|
+
* refused is signed in again; the sign-in is confirmed the way a turn checks
|
|
49
|
+
* it. Checked against grok 1.0.25 and 1.0.41: `login --help` and the
|
|
50
|
+
* device-code text in their binaries. */
|
|
51
|
+
export const GROK_DEVICE_SIGN_IN = {
|
|
52
|
+
provider: "grok",
|
|
53
|
+
product: "Grok",
|
|
54
|
+
account: "Grok",
|
|
55
|
+
homeEnv: "GROK_HOME",
|
|
56
|
+
homeDir: ".grok",
|
|
57
|
+
loginArgs: ["login", "--device-auth"],
|
|
58
|
+
install: "Install it with xAI's installer (https://x.ai/cli), then try again.",
|
|
59
|
+
failure: grokLoginFailure,
|
|
60
|
+
signedIn: grokSignedIn,
|
|
61
|
+
};
|
|
23
62
|
function unquote(raw) {
|
|
24
63
|
const value = raw.trim();
|
|
25
64
|
if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
|
|
@@ -30,7 +69,7 @@ function unquote(raw) {
|
|
|
30
69
|
/** Local slugs from ~/.grok/config.toml, plus the cloud defaults.
|
|
31
70
|
* `grok -m <slug>` already accepts these; the picker just didn't list them. */
|
|
32
71
|
export function readGrokModelCatalog(env = process.env) {
|
|
33
|
-
const path = join(
|
|
72
|
+
const path = join(grokHome(env), "config.toml");
|
|
34
73
|
let text = "";
|
|
35
74
|
try {
|
|
36
75
|
text = readFileSync(path, "utf8");
|
|
@@ -112,7 +151,7 @@ export function ensureGrokInjectSlug(modelId, env = process.env) {
|
|
|
112
151
|
const host = localHost(inject.host);
|
|
113
152
|
if (!host)
|
|
114
153
|
return modelId;
|
|
115
|
-
const path = join(
|
|
154
|
+
const path = join(grokHome(env), "config.toml");
|
|
116
155
|
let text = "";
|
|
117
156
|
try {
|
|
118
157
|
text = readFileSync(path, "utf8");
|
|
@@ -215,7 +254,7 @@ export function grokInheritedProfile(cli, env, cwd) {
|
|
|
215
254
|
let path = flag >= 0 ? (args[flag].includes("=") ? args[flag].slice(args[flag].indexOf("=") + 1) : args[flag + 1]) : undefined;
|
|
216
255
|
if (flag >= 0 && (!path || path.startsWith("-")))
|
|
217
256
|
return unsupported();
|
|
218
|
-
const files = [join(
|
|
257
|
+
const files = [join(grokHome(env), "config.toml")];
|
|
219
258
|
for (let directory = resolve(cwd);;) {
|
|
220
259
|
const file = join(directory, ".grok", "config.toml");
|
|
221
260
|
if (!files.includes(file))
|
|
@@ -331,7 +370,7 @@ const support = {
|
|
|
331
370
|
effortLevels: ["low", "medium", "high"],
|
|
332
371
|
defaultCli: "grok",
|
|
333
372
|
nativeSource: "grok.acp",
|
|
334
|
-
loginNote: "Grok
|
|
373
|
+
loginNote: "Grok is not signed in to your grok.com account — choose Sign in to Grok in engine setup",
|
|
335
374
|
// No Windows one-liner: the installer is a POSIX shell script, and offering
|
|
336
375
|
// `curl … | bash` there would be advice that cannot run. Windows falls back
|
|
337
376
|
// to docsUrl, which is honest rather than broken.
|
|
@@ -341,8 +380,11 @@ const support = {
|
|
|
341
380
|
linux: "curl -fsSL https://x.ai/cli/install.sh | bash",
|
|
342
381
|
},
|
|
343
382
|
docsUrl: "https://x.ai/cli",
|
|
383
|
+
// The terminal route, for people who prefer it; the app's own sign-in
|
|
384
|
+
// is the device code below, which needs no terminal.
|
|
344
385
|
signInCommand: "grok login",
|
|
345
386
|
},
|
|
387
|
+
deviceSignIn: GROK_DEVICE_SIGN_IN,
|
|
346
388
|
// Write the [model.slug] block with the instance HOME/GROK_HOME, then pass
|
|
347
389
|
// the slug on argv. spawnArgs must not call ensureGrokInjectSlug itself —
|
|
348
390
|
// that helper defaults to process.env and would miss the instance override.
|
|
@@ -458,7 +500,7 @@ const support = {
|
|
|
458
500
|
// an unauthenticated CLI is a user action, not something to paper over.
|
|
459
501
|
pickAuthMethod: (methods) => (methods.some((m) => m.id === "cached_token") ? "cached_token" : null),
|
|
460
502
|
authFailure: "fail",
|
|
461
|
-
isAuthenticated: (env) =>
|
|
503
|
+
isAuthenticated: (env) => grokSignedIn(env),
|
|
462
504
|
// `--append-system-prompt`/`--rules` are accepted by the CLI but do NOT
|
|
463
505
|
// reach the agent-stdio system prompt (verified against 1.0.0), so the
|
|
464
506
|
// persona is prepended codex-style.
|