openmausbot 0.1.97 → 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-k1UCNXb7.js → abnfDiagram-O67JEVCF-BsjcFX0-.js} +1 -1
- package/dist/assets/activity-BxOkayvD.js +1 -0
- package/dist/assets/{arc-BUWGa9UU.js → arc-CJNA94c6.js} +1 -1
- package/dist/assets/architectureDiagram-NJMV4G6O-dz2ud7lP.js +36 -0
- package/dist/assets/{blockDiagram-BEXU5L5S-Ag9nLW4H.js → blockDiagram-BEXU5L5S-ChBEALoR.js} +1 -1
- package/dist/assets/brain-CFZ7PEXc.js +1 -0
- package/dist/assets/{c4Diagram-YGBWAQC7-Bh0sjgUa.js → c4Diagram-YGBWAQC7-KZNRKDQe.js} +1 -1
- package/dist/assets/channel-WhFyujhZ.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BFBcIIqu.js → chunk-2Q5K7J3B-BLLwmCET.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-Dvfpex-f.js → chunk-3FUC2YCW-CQxyreBZ.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-CMjP2YZW.js → chunk-5VM5RSS4-D6sFMW0u.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-BryoeTc7.js → chunk-GTNCS2PH-BaZ1YgLF.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-BhfvcchO.js → chunk-GWA4HPMP-CcalphmC.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-bcCVp8rp.js → chunk-JWPE2WC7-CSk8R3ve.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-yKIjIlkQ.js → chunk-NETBCI7D-CTmLjA57.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-D3Zqbmup.js → chunk-WEXAMYUT-CM9Z77Ne.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-CjVoIMG6.js → chunk-XXDRQBXY-B1fxpMS2.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-mUCWdVyH.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-BpRrxoO4.js → cynefin-OW5HDTMX-fsZc6MGa.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-D3VBWCPK.js → cynefinDiagram-VND7K2PF-CGmHsShB.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-DxEIACOO.js → dagre-6A5THRUB-Dah1h-CH.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-BANgZEOS.js → diagram-22UHCM2B-BIu71OyV.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-Cnq7nJir.js → diagram-3UASUU5V-gxhUB_Mu.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-4TBDIWnz.js → diagram-ATOU4E4O-CBhusUBn.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-CD_B39D4.js → diagram-CDSNMT55-BdPrc86x.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-CzvnFhEC.js → diagram-MLGK6HIB-Clp110R_.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-C4w2SNml.js → diagram-MPIPVDR6-DJKfbugw.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-gkUEBm2s.js → ebnfDiagram-ZINNZB2B-DnsltoBu.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-B_56X6mw.js → elk-276RUBZZ-jWdq6gqH.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-C_oByoDS.js → erDiagram-OPXOYQCR-DArPVf8u.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-CR9xXJkC.js → flowDiagram-KWPJA3E3-D2HZ-2f7.js} +1 -1
- package/dist/assets/ganttDiagram-FUAMR5RP--o0KIxEo.js +292 -0
- package/dist/assets/{gitGraphDiagram-X574FWY7-VJ6-Tp_p.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-Cp4S4NOL.js → index-CJTp9JEp.js} +1 -1
- package/dist/assets/{infoDiagram-VRGFBTTK-2Lu9te4D.js → infoDiagram-VRGFBTTK-g-6lNp7r.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-DR49XY-e.js → ishikawaDiagram-OU5B5YK6-B1d6qVTV.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-ZoR0M305.js → journeyDiagram-ZHPQQLJL-BTPuiU6T.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-DLKHxrO-.js → kanban-definition-PNTS6WVX-bDDuRJvP.js} +1 -1
- package/dist/assets/{linear-BASsfWly.js → linear-BVVKbEMy.js} +1 -1
- package/dist/assets/{mermaid.core-BVidiPkk.js → mermaid.core-JJdoKyKB.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-g_Qx0U5C.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-DI9Cjs8w.js → pegDiagram-GJSIUBJH-DFKQWpR7.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-sRI0Uhr5.js → pieDiagram-5QR66LMP-Iv7OKVJi.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-CamKTby3.js → quadrantDiagram-O4NWA36T-U2YnfwBk.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-DOLlkbf3.js → railroadDiagram-XR7U4H2S-CuPy6RqM.js} +1 -1
- package/dist/assets/remote-desktop-panel-BNEj7tUx.js +1 -0
- package/dist/assets/{requirementDiagram-PLB6GJNP-DrRQ5r97.js → requirementDiagram-PLB6GJNP-bMI5AK7T.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-DGS_lXcX.js → sankeyDiagram-IPEJSGJF-BKcFE2WQ.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-CkjWqwtf.js → sequenceDiagram-PO4LG4MO-Bn9q88kl.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-CepRgful.js → sizeCapture-INFHLROL-Cvmdq9HD.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-hykr-9RS.js → stateDiagram-v2-GCMORJYK-JJQYw3eQ.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-CKOHEZbI.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-CB8pJ6AC.js → timeline-definition-EJHVYXUP-DsaM4GHr.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-DrclJHdl.js → usecaseDiagram-POWQR4AR-3FOwLq7B.js} +1 -1
- package/dist/assets/user-DcZBnAuv.js +1 -0
- package/dist/assets/{vennDiagram-UO4OBE2U-zS23qjv8.js → vennDiagram-UO4OBE2U-DyNjJsKt.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-52jgeKxz.js → wardleyDiagram-VNRHLVJA-Y4ZMcnBC.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-DSdl9rQ8.js → xychartDiagram-PMCCYNJV-BXZpg955.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/cloud-home-start.js +19 -8
- package/dist-server/container-mcp.js +8 -0
- package/dist-server/desktop-entry.mjs +54 -0
- package/dist-server/drivers/agents-proxy.js +21 -4
- package/dist-server/index.js +1412 -821
- package/dist-server/openmausbot.js +837 -571
- package/dist-server/pair-cli.js +837 -571
- package/dist-server/permission-proxy.js +2 -0
- package/dist-server/server/atomic.js +70 -2
- package/dist-server/server/cloud-home-start.js +3 -3
- package/dist-server/server/config.js +22 -1
- package/dist-server/server/connector-card-text.js +15 -0
- package/dist-server/server/default-model-selection.js +7 -4
- package/dist-server/server/drivers/acp/core.js +10 -0
- package/dist-server/server/drivers/acp/grok.js +47 -5
- package/dist-server/server/drivers/agents-catalog.js +24 -4
- package/dist-server/server/drivers/agents-proxy.js +1 -0
- 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 +35 -11
- package/dist-server/server/drivers/codex-device-auth.js +27 -319
- package/dist-server/server/drivers/device-auth.js +342 -0
- package/dist-server/server/drivers/openai-chat.js +12 -4
- 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 +268 -54
- package/dist-server/server/key-rejections.js +57 -0
- package/dist-server/server/message-file.js +7 -4
- 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-timeout.js +28 -0
- package/dist-server/server/routes/thread-models.js +36 -0
- package/dist-server/server/routines.js +2 -2
- package/dist-server/server/skills.js +3 -2
- package/dist-server/server/store.js +148 -60
- 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 +8 -2
- package/dist-server/server/webhook-ingress.js +8 -3
- package/dist-server/server/webhooks.js +40 -5
- package/dist-server/shared/device-sign-in.js +57 -0
- package/dist-server/shared/team-backup.js +2 -0
- package/dist-server/shared/thread-model.js +13 -0
- 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-CPidMCtt.js +0 -5
- package/dist/assets/architectureDiagram-NJMV4G6O-DiPor3UT.js +0 -36
- package/dist/assets/channel-DR8eD6QW.js +0 -1
- package/dist/assets/cose-bilkent-JH36ORCC-BBmR1bbX.js +0 -1
- package/dist/assets/ganttDiagram-FUAMR5RP-gtYBTu0B.js +0 -292
- package/dist/assets/index-CpaIXRLp.js +0 -614
- package/dist/assets/index-Z81SMsgR.css +0 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-D-8vqZ4-.js +0 -8
|
@@ -157,6 +157,7 @@ var allowQuestion = allowsTool(selection.scope, { kind: "mcp", server: "ogb", na
|
|
|
157
157
|
var socketPath = process.argv[2] ?? "";
|
|
158
158
|
var waiting = /* @__PURE__ */ new Map();
|
|
159
159
|
var conn = connect(socketPath);
|
|
160
|
+
conn.setEncoding("utf8");
|
|
160
161
|
var dead = () => {
|
|
161
162
|
for (const resolve of waiting.values()) {
|
|
162
163
|
resolve({ behavior: "deny", message: "OpenMausBot: permission broker unavailable \u2014 skip this action" });
|
|
@@ -295,6 +296,7 @@ async function handle(msg) {
|
|
|
295
296
|
}
|
|
296
297
|
}
|
|
297
298
|
var inBuf = "";
|
|
299
|
+
process.stdin.setEncoding("utf8");
|
|
298
300
|
process.stdin.on("data", (chunk) => {
|
|
299
301
|
inBuf += chunk;
|
|
300
302
|
let nl;
|
|
@@ -4,8 +4,15 @@
|
|
|
4
4
|
// reader always sees either the complete old contents or the complete new
|
|
5
5
|
// ones. Without this, an interrupted writeFileSync produces half-written JSON
|
|
6
6
|
// that fails to parse on next boot and is silently treated as empty state.
|
|
7
|
+
//
|
|
8
|
+
// The fsync is the expensive part: on macOS it is F_FULLFSYNC, about 4 ms of
|
|
9
|
+
// blocked event loop per call. `durable: false` drops only the fsync, for
|
|
10
|
+
// files whose loss after a power cut costs nothing (a derived cache, a
|
|
11
|
+
// session-scoped token). The temp file and rename stay, so an app crash or
|
|
12
|
+
// kill -9 still never leaves a torn file; only a power loss or kernel panic
|
|
13
|
+
// can leave such a file empty.
|
|
7
14
|
import { randomUUID } from "node:crypto";
|
|
8
|
-
import { closeSync, fsyncSync, openSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { closeSync, constants, fstatSync, fsyncSync, lstatSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
9
16
|
/** Windows refuses a rename onto an existing path while anything else holds a
|
|
10
17
|
* handle to either file, and a virus scanner or the search indexer opening a
|
|
11
18
|
* just-closed file for a few milliseconds is enough. It surfaces as EPERM or
|
|
@@ -51,7 +58,8 @@ export function writeFileAtomic(path, data, options = {}) {
|
|
|
51
58
|
// config file visible between the write and a later chmod.
|
|
52
59
|
fd = openSync(tmp, "w", options.mode);
|
|
53
60
|
writeFileSync(fd, data);
|
|
54
|
-
|
|
61
|
+
if (options.durable !== false)
|
|
62
|
+
fsyncSync(fd);
|
|
55
63
|
closeSync(fd);
|
|
56
64
|
fd = null;
|
|
57
65
|
renameWithRetry(tmp, path);
|
|
@@ -74,3 +82,63 @@ export function writeFileAtomic(path, data, options = {}) {
|
|
|
74
82
|
throw e;
|
|
75
83
|
}
|
|
76
84
|
}
|
|
85
|
+
/** True only when `path` is already exactly `data`: a regular file (never a
|
|
86
|
+
* symlink, FIFO or directory), owned by this user, with permissions no
|
|
87
|
+
* broader than `mode`. Any doubt or error answers false, so the caller falls
|
|
88
|
+
* back to a full write — this check must never be the reason a save fails.
|
|
89
|
+
*
|
|
90
|
+
* It reads the file on disk rather than remembering the last write: a
|
|
91
|
+
* workspace restore or a hand edit can change the file underneath, and the
|
|
92
|
+
* next save has to put its own bytes back exactly as before. */
|
|
93
|
+
function holdsExactly(path, data, mode) {
|
|
94
|
+
let fd = null;
|
|
95
|
+
try {
|
|
96
|
+
if (process.platform === "win32") {
|
|
97
|
+
// No O_NOFOLLOW on Windows; a reparse point is replaced, not read.
|
|
98
|
+
if (!lstatSync(path).isFile())
|
|
99
|
+
return false;
|
|
100
|
+
fd = openSync(path, "r");
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
// Inspect and read the same inode the open landed on, so a swap
|
|
104
|
+
// between a stat and the read cannot be mistaken for a match.
|
|
105
|
+
// O_NONBLOCK keeps a FIFO planted at the path from hanging the open.
|
|
106
|
+
fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
107
|
+
}
|
|
108
|
+
const stat = fstatSync(fd);
|
|
109
|
+
if (!stat.isFile() || stat.size !== Buffer.byteLength(data))
|
|
110
|
+
return false;
|
|
111
|
+
const uid = process.getuid?.();
|
|
112
|
+
if (uid !== undefined && stat.uid !== uid)
|
|
113
|
+
return false;
|
|
114
|
+
// Windows reports 0o666 for any writable file; its mode bits mean nothing.
|
|
115
|
+
if (mode !== undefined && process.platform !== "win32" && (stat.mode & 0o777 & ~mode) !== 0)
|
|
116
|
+
return false;
|
|
117
|
+
// Bytes, not strings: decoding and comparing a megabyte-sized registry as
|
|
118
|
+
// a JS string costs several times more than a memcmp of the raw bytes.
|
|
119
|
+
return readFileSync(fd).equals(Buffer.from(data));
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
if (fd !== null) {
|
|
126
|
+
try {
|
|
127
|
+
closeSync(fd);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
/* best-effort cleanup */
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/** writeFileAtomic for saves that often repeat what the file already holds.
|
|
136
|
+
* A save that changes nothing costs one open and read instead of a temp
|
|
137
|
+
* file, a rename and an fsync; a save whose size differs pays only an
|
|
138
|
+
* open and fstat before the normal write. Returns whether it wrote. */
|
|
139
|
+
export function writeFileAtomicIfChanged(path, data, options = {}) {
|
|
140
|
+
if (holdsExactly(path, data, options.mode))
|
|
141
|
+
return false;
|
|
142
|
+
writeFileAtomic(path, data, options);
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Entry point of the OMB Cloud Pro home image (
|
|
1
|
+
// Entry point of the OMB Cloud Pro home image (the Dockerfile's cloud-home target).
|
|
2
2
|
//
|
|
3
3
|
// Starts as root, hands a fresh Fly volume (mounted root-owned at /data) to
|
|
4
4
|
// the unprivileged `maus` user, and stays a small root supervisor of two
|
|
@@ -77,7 +77,7 @@ export function cloudHomeChildEnvironments(config, env, home) {
|
|
|
77
77
|
/** Why the root supervisor must not run or trust `files`, or null: each
|
|
78
78
|
* must be root's and not writable by anyone else (nor any folder above it),
|
|
79
79
|
* and none may live on the volume `home`, which `maus` owns. The image makes its code
|
|
80
|
-
* root's (
|
|
80
|
+
* root's (the Dockerfile's cloud-home target); a file `maus` could rewrite would run as
|
|
81
81
|
* root at the next start, or be handed the secrets. */
|
|
82
82
|
export function codeTrustProblem(files, home, stat = statSync) {
|
|
83
83
|
const volume = posix.join(home, "/");
|
|
@@ -155,7 +155,7 @@ export function startCloudHome(env = process.env) {
|
|
|
155
155
|
if (ids) {
|
|
156
156
|
const problem = codeTrustProblem([process.execPath, fileURLToPath(import.meta.url), join(here, "index.js"), edgeBin, edgeConfig], home);
|
|
157
157
|
if (problem)
|
|
158
|
-
throw new Error(`This image's code is not safe to run as root: ${problem}. Rebuild it
|
|
158
|
+
throw new Error(`This image's code is not safe to run as root: ${problem}. Rebuild it with docker build --target cloud-home.`);
|
|
159
159
|
}
|
|
160
160
|
const { server, edge, secrets, dropped } = cloudHomeChildEnvironments(config, env, home);
|
|
161
161
|
// Names only, never values: what the server does not get from here.
|
|
@@ -24,6 +24,13 @@ export const FISH_TTS_MODELS = ["s2.1-pro", "s2.1-pro-free"];
|
|
|
24
24
|
export const DEFAULT_ROOM_TURN_TIMEOUT_MINUTES = 5;
|
|
25
25
|
export const MIN_ROOM_TURN_TIMEOUT_MINUTES = 1;
|
|
26
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;
|
|
27
34
|
export const DEFAULT_ROOM_HANDOFF_LIFETIME_MINUTES = 30;
|
|
28
35
|
export const DEFAULT_ROOM_HANDOFF_MIN_RUNWAY_MINUTES = 10;
|
|
29
36
|
export const DEFAULT_ROOM_HANDOFF_HARD_CAP_MINUTES = 240;
|
|
@@ -105,6 +112,16 @@ const roomConfigSchema = z.object({
|
|
|
105
112
|
(rooms.handoffLifetimeMinutes ?? DEFAULT_ROOM_HANDOFF_LIFETIME_MINUTES) &&
|
|
106
113
|
(rooms.handoffLifetimeMinutes ?? DEFAULT_ROOM_HANDOFF_LIFETIME_MINUTES) <=
|
|
107
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();
|
|
108
125
|
/** Isolation for bot desktops. Migration note (issue #1654): switching
|
|
109
126
|
* modes changes lease keys and vm-home directories, so desktops cold-start
|
|
110
127
|
* under the new mode — a pool seat lives in vm-homes/pool-N — while the old
|
|
@@ -503,6 +520,7 @@ const appConfigSchema = z.object({
|
|
|
503
520
|
* system language. Unknown tags degrade to English in the renderer. */
|
|
504
521
|
language: optionalText,
|
|
505
522
|
rooms: roomConfigSchema.optional(),
|
|
523
|
+
mcp: mcpConfigSchema.optional(),
|
|
506
524
|
context: z.object({
|
|
507
525
|
rebuildBytes: z.number().int().min(1_024).max(1_000_000).optional(),
|
|
508
526
|
compactAt: z.number().positive().max(10_000_000).optional(),
|
|
@@ -635,6 +653,9 @@ export function browserEngineAttachCdpUrl(cfg) {
|
|
|
635
653
|
export function roomTurnTimeoutMinutes(cfg) {
|
|
636
654
|
return cfg.rooms?.turnTimeoutMinutes ?? DEFAULT_ROOM_TURN_TIMEOUT_MINUTES;
|
|
637
655
|
}
|
|
656
|
+
export function mcpCallTimeoutMinutes(cfg) {
|
|
657
|
+
return cfg.mcp?.callTimeoutMinutes ?? DEFAULT_MCP_CALL_TIMEOUT_MINUTES;
|
|
658
|
+
}
|
|
638
659
|
export const LIVE_IDLE_MINUTES_DEFAULT = 5;
|
|
639
660
|
/** Non-secret Live settings. The key only shows up as `configured`. */
|
|
640
661
|
export function liveSettingsFor(cfg) {
|
|
@@ -1204,7 +1225,7 @@ export function saveConfig(patch, options = {}) {
|
|
|
1204
1225
|
const storedProfiles = storedBrowserProfilesSchema.safeParse(disk.browserProfiles);
|
|
1205
1226
|
if (storedProfiles.success)
|
|
1206
1227
|
disk.browserProfiles = storedProfiles.data;
|
|
1207
|
-
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"]) {
|
|
1208
1229
|
const section = checkedPatch[key];
|
|
1209
1230
|
if (!section)
|
|
1210
1231
|
continue;
|
|
@@ -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
|
+
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
const claudeFirst = (instances) => instances.find((instance) => instance.driverKind === "claudeAgent") ?? instances[0];
|
|
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
|
+
}
|
|
2
7
|
/** The ready rule: an engine can run a turn now when it is available, the
|
|
3
|
-
* organisation allows it, and it is not signed out.
|
|
4
|
-
* its own credential, so its sign-in state does not count. */
|
|
8
|
+
* organisation allows it, and it is not signed out (signedOut). */
|
|
5
9
|
export function readyToRun(instance, context = {}) {
|
|
6
|
-
return instance.snapshot.state === "available" && context.refusal?.(instance) === undefined &&
|
|
7
|
-
(instance.access === "custom" || instance.snapshot.authenticated !== false);
|
|
10
|
+
return instance.snapshot.state === "available" && context.refusal?.(instance) === undefined && !signedOut(instance);
|
|
8
11
|
}
|
|
9
12
|
/** The engine a new bot gets, and the one a bot moved off a removed engine
|
|
10
13
|
* gets (computer-engine-removal.ts): one picker for both.
|
|
@@ -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";
|
|
@@ -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.
|
|
@@ -26,6 +26,7 @@ export function catalogProfileFromEnv(env) {
|
|
|
26
26
|
voiceNotes: env.OMB_VOICE_NOTES === "1",
|
|
27
27
|
cloudHome: env.OMB_CLOUD_HOME === "1",
|
|
28
28
|
memoryEnabled: env.OMB_MEMORY_ENABLED !== "0",
|
|
29
|
+
chief: env.OMB_CHIEF_OF_STAFF === "1",
|
|
29
30
|
botId: env.OMB_BOT_ID ?? "",
|
|
30
31
|
};
|
|
31
32
|
}
|
|
@@ -146,7 +147,7 @@ const ROUTINE_FIELDS_SCHEMA = {
|
|
|
146
147
|
},
|
|
147
148
|
continuity: {
|
|
148
149
|
type: "boolean",
|
|
149
|
-
description: "Opt in to using the latest completed run's bounded report as historical context. Defaults to false; set false in an update to start fresh again. Included in the applied result or pending confirmation.",
|
|
150
|
+
description: "Opt in to using the latest completed run's bounded report as historical context. Use it for recurring work that builds on last time, such as QA passes, monitoring or follow-ups. Defaults to false; set false in an update to start fresh again. Included in the applied result or pending confirmation.",
|
|
150
151
|
},
|
|
151
152
|
overlap: {
|
|
152
153
|
type: "string",
|
|
@@ -154,6 +155,9 @@ const ROUTINE_FIELDS_SCHEMA = {
|
|
|
154
155
|
description: "While this routine is still working, skip scheduled occurrences (default) or queue at most one run. Queue skips further occurrences until the pending run starts; it never builds an unlimited backlog. Manual and webhook requests are separate.",
|
|
155
156
|
},
|
|
156
157
|
};
|
|
158
|
+
// propose_profile's one sentence about for_bot_id, which a bot that is not a
|
|
159
|
+
// Chief is not shown (catalogTools), along with the parameter itself.
|
|
160
|
+
const CHIEF_PROFILE_TARGET = " A Chief of Staff may pass for_bot_id (from list_bots) for a requested change to another bot in its section.";
|
|
157
161
|
const PROPOSAL_OUTCOME = " Read the result: granted Full Access may apply the change immediately. If applied, continue the requested work without another confirmation. Only a pending result requires ending the turn and waiting for the in-app decision. Never claim success from the permission mode alone; report failed or cancelled results honestly. This does not elevate another bot's execution permissions.";
|
|
158
162
|
/** Every tool, in the order it is listed. Four peer tools are worded
|
|
159
163
|
* differently for an external runtime, which may poll inside one process. */
|
|
@@ -652,7 +656,7 @@ const toolDefinitions = (externalRuntime) => [
|
|
|
652
656
|
},
|
|
653
657
|
{
|
|
654
658
|
name: "propose_profile",
|
|
655
|
-
description: "Submit user-requested changes to your own name, title, description, standing instructions (SOUL.md), working folder (cwd), or your alert and voice toggles (notifications, speakReplies). Keep SOUL.md short — who you are and the rules you never break; put step-by-step procedure into a skill instead.
|
|
659
|
+
description: "Submit user-requested changes to your own name, title, description, standing instructions (SOUL.md), working folder (cwd), or your alert and voice toggles (notifications, speakReplies). Keep SOUL.md short — who you are and the rules you never break; put step-by-step procedure into a skill instead." + CHIEF_PROFILE_TARGET + PROPOSAL_OUTCOME,
|
|
656
660
|
inputSchema: {
|
|
657
661
|
type: "object",
|
|
658
662
|
additionalProperties: false,
|
|
@@ -842,6 +846,14 @@ export const SHARED_COMPUTER_TOOL_NAMES = new Set(["list_shared_computers", "sha
|
|
|
842
846
|
// tool whose every call would end in a setup error. The route behind it
|
|
843
847
|
// refuses regardless; this keeps the catalog honest about what can work.
|
|
844
848
|
const VOICE_TOOL_NAMES = new Set(["send_voice_note"]);
|
|
849
|
+
// And for a role: every route behind these refuses a bot that is not its
|
|
850
|
+
// section's Chief of Staff, as it does a for_bot_id naming another bot on
|
|
851
|
+
// propose_profile or propose_model. (A routine's for_bot_id is open to any
|
|
852
|
+
// bot that can reach that peer, so it stays.)
|
|
853
|
+
const CHIEF_ONLY_TOOL_NAMES = new Set([
|
|
854
|
+
"create_bot", "list_team_setup", "propose_team_setup", "propose_bot_deletion", "create_room", "manage_room", "retry_thread",
|
|
855
|
+
]);
|
|
856
|
+
const CHIEF_TARGET_TOOL_NAMES = new Set(["propose_profile", "propose_model"]);
|
|
845
857
|
// One teamwork path in room turns; keep all unrelated integrations available.
|
|
846
858
|
// Ordinary direct chats use this same bounded coordinator. Goal-owned turns
|
|
847
859
|
// retain their independent loop and cannot start a second coordinator.
|
|
@@ -878,10 +890,18 @@ function catalogTools(profile) {
|
|
|
878
890
|
const VOICE_READY_TOOLS = profile.voiceNotes
|
|
879
891
|
? SHAREABLE_TOOLS
|
|
880
892
|
: SHAREABLE_TOOLS.filter((tool) => !VOICE_TOOL_NAMES.has(tool.name));
|
|
893
|
+
const ROLE_TOOLS = profile.chief
|
|
894
|
+
? VOICE_READY_TOOLS
|
|
895
|
+
: VOICE_READY_TOOLS.filter((tool) => !CHIEF_ONLY_TOOL_NAMES.has(tool.name)).map((tool) => {
|
|
896
|
+
if (!CHIEF_TARGET_TOOL_NAMES.has(tool.name))
|
|
897
|
+
return tool;
|
|
898
|
+
const properties = Object.fromEntries(Object.entries(tool.inputSchema.properties).filter(([key]) => key !== "for_bot_id"));
|
|
899
|
+
return { ...tool, description: tool.description.replace(CHIEF_PROFILE_TARGET, ""), inputSchema: { ...tool.inputSchema, properties } };
|
|
900
|
+
});
|
|
881
901
|
return profile.externalRuntime
|
|
882
902
|
? BOT_SCOPED_TOOLS.filter(tool => EXTERNAL_TOOL_NAMES.has(tool.name))
|
|
883
903
|
: profile.coordinating
|
|
884
|
-
?
|
|
904
|
+
? ROLE_TOOLS.filter(tool => !ROOM_REPLACED_TOOLS.has(tool.name) || (tool.name === "start_thread" && profile.ownThreadCreation))
|
|
885
905
|
.map(tool => tool.name === "start_thread" ? {
|
|
886
906
|
...tool,
|
|
887
907
|
description: "Open a separate job on yourself with its own history and run, without switching the person's selected conversation. Use only when the user requests independent jobs (for example one review per pull request). Give a short specific title and complete instructions; you can open at most five per turn. This is not a teammate handoff: use coordinate_bots for teammates and their automatic replies. Self-opened jobs cannot recursively open more jobs. If refused, do not retry; explain what remains.",
|
|
@@ -889,5 +909,5 @@ function catalogTools(profile) {
|
|
|
889
909
|
bot_id: { type: "string", enum: [profile.botId], description: "Leave out, or use your own bot ID. For teammates use coordinate_bots." },
|
|
890
910
|
} },
|
|
891
911
|
} : tool)
|
|
892
|
-
:
|
|
912
|
+
: ROLE_TOOLS.filter(tool => !ROOM_ONLY_TOOLS.has(tool.name));
|
|
893
913
|
}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// OMB_COMMS_TOKEN shared secret for the localhost-only internal endpoints
|
|
13
13
|
// OMB_TURN_DEPTH this turn's comms depth (the harness refuses recursion)
|
|
14
14
|
// OMB_EXTERNAL_RUNTIME "1" for a standing process: peer tools and polling only
|
|
15
|
+
// OMB_CHIEF_OF_STAFF "1" for a Chief of Staff, the only bot shown the Chief-only tools
|
|
15
16
|
//
|
|
16
17
|
// This file is the stdio front end only. What the tools are and which a turn
|
|
17
18
|
// sees: agents-catalog.ts. What a call does: agents-call.ts. How the harness
|
|
@@ -14,7 +14,9 @@ import { allowsTool, canUseMcpServer, parseToolScope } from "../../shared/tool-s
|
|
|
14
14
|
export class ChatToolSessionError extends Error {
|
|
15
15
|
}
|
|
16
16
|
const STARTUP_MS = 8_000;
|
|
17
|
-
|
|
17
|
+
/** Historic per-call ceiling for a bot's MCP tools; a turn can override it
|
|
18
|
+
* with SendTurnInput.mcpCallTimeoutMs (server config `mcp.callTimeoutMinutes`). */
|
|
19
|
+
const DEFAULT_CALL_MS = 10 * 60_000;
|
|
18
20
|
const FRAME_BYTES = 2 * 1024 * 1024;
|
|
19
21
|
const OUTPUT_BYTES = 50 * 1024;
|
|
20
22
|
const TOOL_COUNT = 128;
|
|
@@ -283,7 +285,7 @@ function omitBlankDefaults(builtInBrowser, tool, args) {
|
|
|
283
285
|
delete args[field];
|
|
284
286
|
}
|
|
285
287
|
}
|
|
286
|
-
export async function mountChatTools(integrations, signal, computerUse = false, toolScope) {
|
|
288
|
+
export async function mountChatTools(integrations, signal, computerUse = false, toolScope, callTimeoutMs = DEFAULT_CALL_MS) {
|
|
287
289
|
const parsed = parseToolScope(toolScope);
|
|
288
290
|
if (!parsed.ok)
|
|
289
291
|
throw new Error(parsed.error);
|
|
@@ -411,7 +413,7 @@ export async function mountChatTools(integrations, signal, computerUse = false,
|
|
|
411
413
|
}
|
|
412
414
|
const tool = registered.get(name);
|
|
413
415
|
try {
|
|
414
|
-
const result = await tool.client.call("tools/call", { name: tool.name, arguments: args }, AbortSignal.any([signal, callSignal]),
|
|
416
|
+
const result = await tool.client.call("tools/call", { name: tool.name, arguments: args }, AbortSignal.any([signal, callSignal]), callTimeoutMs);
|
|
415
417
|
if (signal.aborted || callSignal.aborted)
|
|
416
418
|
throw aborted();
|
|
417
419
|
if (!object(result) || !Array.isArray(result.content) || (result.isError !== undefined && typeof result.isError !== "boolean"))
|
|
@@ -3,7 +3,7 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { isAbsolute, join, resolve } from "node:path";
|
|
4
4
|
import { stripVTControlCharacters } from "node:util";
|
|
5
5
|
import { killCliTree, spawnCli } from "../procs.js";
|
|
6
|
-
import { canonicalPath } from "./
|
|
6
|
+
import { canonicalPath } from "./device-auth.js";
|
|
7
7
|
const MAX_OUTPUT = 32_768;
|
|
8
8
|
/** Where Anthropic's sign-in lives. Anything else in the CLI's output is not a link we show. */
|
|
9
9
|
const SIGN_IN_HOSTS = ["claude.com", "claude.ai", "console.anthropic.com", "platform.claude.com"];
|
|
@@ -15,7 +15,7 @@ import { createServer as createNetServer } from "node:net";
|
|
|
15
15
|
import { homedir, tmpdir } from "node:os";
|
|
16
16
|
import { join, dirname, isAbsolute, normalize } from "node:path";
|
|
17
17
|
import { DATA_DIR, stripWorkspaceCredentialEnv } from "../config.js";
|
|
18
|
-
import { writeFileAtomic } from "../atomic.js";
|
|
18
|
+
import { writeFileAtomic, writeFileAtomicIfChanged } from "../atomic.js";
|
|
19
19
|
import { augmentedPath } from "../env-path.js";
|
|
20
20
|
import { brokerSocketPath, describeSpawnFailure, execCli, killCliTree, spawnCli } from "../procs.js";
|
|
21
21
|
import { classifyResumeFailure, mayReplay, recoveryPromptFor } from "../resume-recovery.js";
|
|
@@ -599,6 +599,9 @@ export async function createPermissionBroker(opts) {
|
|
|
599
599
|
let boundPath = opts.socketPaths[0] ?? "";
|
|
600
600
|
const connectionHandler = (conn) => {
|
|
601
601
|
conn.on("error", () => { });
|
|
602
|
+
// An ask carries the whole tool input (a Write's file content), so one
|
|
603
|
+
// line spans many reads. Decode as a stream so no character is split.
|
|
604
|
+
conn.setEncoding("utf8");
|
|
602
605
|
let buf = "";
|
|
603
606
|
conn.on("data", (chunk) => {
|
|
604
607
|
buf += chunk;
|
|
@@ -932,8 +935,10 @@ function recordCostState(sessionId, state) {
|
|
|
932
935
|
const ids = Object.keys(history);
|
|
933
936
|
for (const id of ids.slice(0, Math.max(0, ids.length - COST_HISTORY_SESSIONS)))
|
|
934
937
|
delete history[id];
|
|
938
|
+
// Written after every turn, so not fsynced: what a power cut could lose is
|
|
939
|
+
// the same thing a failed write already gives up (below).
|
|
935
940
|
try {
|
|
936
|
-
writeFileAtomic(COST_HISTORY_FILE, JSON.stringify(history), { mode: 0o600 });
|
|
941
|
+
writeFileAtomic(COST_HISTORY_FILE, JSON.stringify(history), { mode: 0o600, durable: false });
|
|
937
942
|
}
|
|
938
943
|
catch {
|
|
939
944
|
// a lost state only means a later resume keeps its whole figure
|
|
@@ -1051,6 +1056,26 @@ export const ClaudeDriver = {
|
|
|
1051
1056
|
const readCliVersion = (env) => new Promise((resolve) => {
|
|
1052
1057
|
execCli(config.cli, ["--version"], { timeout: 8000, env }, (err, stdout) => resolve(err ? null : stdout.trim() || null));
|
|
1053
1058
|
});
|
|
1059
|
+
// The --help read while it runs: snapshots that overlap (the server's
|
|
1060
|
+
// read at start and the app's first one) share it.
|
|
1061
|
+
let helpRead = null;
|
|
1062
|
+
const readCliHelp = (version, env) => {
|
|
1063
|
+
if (helpRead?.version === version)
|
|
1064
|
+
return helpRead.done;
|
|
1065
|
+
const read = {
|
|
1066
|
+
version,
|
|
1067
|
+
done: new Promise((resolve) => {
|
|
1068
|
+
execCli(config.cli, ["--help"], { timeout: 8000, env }, (err, stdout) => resolve(err ? null : stdout));
|
|
1069
|
+
}).then((help) => {
|
|
1070
|
+
cliHasAutocompact = help === null ? null : /^\s*--autocompact\b/m.test(help);
|
|
1071
|
+
cliHelpVersion = version;
|
|
1072
|
+
if (helpRead === read)
|
|
1073
|
+
helpRead = null;
|
|
1074
|
+
}),
|
|
1075
|
+
};
|
|
1076
|
+
helpRead = read;
|
|
1077
|
+
return read.done;
|
|
1078
|
+
};
|
|
1054
1079
|
const listeners = new Set();
|
|
1055
1080
|
// one active turn per thread; a second send while busy is a caller bug
|
|
1056
1081
|
const active = new Map();
|
|
@@ -1411,14 +1436,18 @@ export const ClaudeDriver = {
|
|
|
1411
1436
|
? readClaudeAuthSettings(env, input.environment) : {};
|
|
1412
1437
|
// Harness hooks (item 0.2): one helper command for the events the
|
|
1413
1438
|
// harness observes. The helper reads its bearer from a per-thread file
|
|
1414
|
-
// the harness
|
|
1439
|
+
// the harness refreshes every turn, so a long-lived CLI process never
|
|
1415
1440
|
// presents a stale token. Registered through the same private
|
|
1416
1441
|
// --settings file as the auth override; both are 0600 and per launch.
|
|
1417
1442
|
const hooks = turn.integrations?.hooks;
|
|
1418
1443
|
const hookTokenPath = hooks ? hookTokenFile(threadId, botId) : null;
|
|
1419
1444
|
if (hooks && hookTokenPath) {
|
|
1420
1445
|
mkdirSync(dirname(hookTokenPath), { recursive: true, mode: 0o700 });
|
|
1421
|
-
|
|
1446
|
+
// The bearer is usually the same as last turn, and it only lives in
|
|
1447
|
+
// this process's memory, so a restart invalidates the file anyway:
|
|
1448
|
+
// skip identical bytes and the fsync. A rotated bearer differs from
|
|
1449
|
+
// what is on disk, so it is always written before the CLI launches.
|
|
1450
|
+
writeFileAtomicIfChanged(hookTokenPath, hooks.token, { mode: 0o600, durable: false });
|
|
1422
1451
|
env.OMB_HOOK_URL = hooks.url;
|
|
1423
1452
|
env.OMB_HOOK_TOKEN_FILE = hookTokenPath;
|
|
1424
1453
|
env.OMB_HOOK_NODE = process.execPath;
|
|
@@ -2303,13 +2332,8 @@ export const ClaudeDriver = {
|
|
|
2303
2332
|
return { state: "unavailable", reason: `\`${config.cli}\` CLI not found` };
|
|
2304
2333
|
cliVersion = parseClaudeCliVersion(version);
|
|
2305
2334
|
cliVersionChecked = true;
|
|
2306
|
-
if (version !== cliHelpVersion)
|
|
2307
|
-
|
|
2308
|
-
execCli(config.cli, ["--help"], { timeout: 8000, env }, (err, stdout) => resolve(err ? null : stdout));
|
|
2309
|
-
});
|
|
2310
|
-
cliHasAutocompact = help === null ? null : /^\s*--autocompact\b/m.test(help);
|
|
2311
|
-
cliHelpVersion = version;
|
|
2312
|
-
}
|
|
2335
|
+
if (version !== cliHelpVersion)
|
|
2336
|
+
await readCliHelp(version, env);
|
|
2313
2337
|
const update = claudeCliUpdate(version, config.cli);
|
|
2314
2338
|
const warning = claudeInheritWarning(env);
|
|
2315
2339
|
if (config.requireApiKey) {
|