openmausbot 0.1.94 → 0.1.96

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.
Files changed (188) hide show
  1. package/dist/assets/{DesktopViewer-bxM7NV3N.js → DesktopViewer-DsnA23KM.js} +1 -1
  2. package/dist/assets/{abnfDiagram-O67JEVCF-BhMdJhRJ.js → abnfDiagram-O67JEVCF-Dkbd2hcj.js} +1 -1
  3. package/dist/assets/{arc-B5itKDcx.js → arc-Duj5Coy2.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-C5d7UY6U.js → architectureDiagram-NJMV4G6O-uYUsDawP.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-BrZIhMmu.js → blockDiagram-BEXU5L5S-CWDlTpXe.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-7I0ArfbD.js → c4Diagram-YGBWAQC7-B0t15PDc.js} +1 -1
  7. package/dist/assets/channel-BZxwC-Gw.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-BnEsyN36.js → chunk-2Q5K7J3B-BJSCL-HV.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-BXATQ52s.js → chunk-3FUC2YCW-D3_ILccd.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-B0BpTkEx.js → chunk-5VM5RSS4-D3dmwT_S.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-BtLTz6FR.js → chunk-GTNCS2PH-Br73HFPD.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-D6gcZOYt.js → chunk-GWA4HPMP-uSsjxubC.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-DCLrosvf.js → chunk-JWPE2WC7-C40VWFyr.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-B9JsknV9.js → chunk-NETBCI7D-D6WNNYwU.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-CsoTxw7z.js → chunk-WEXAMYUT-DOEhq64P.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-0F_frB0c.js → chunk-XXDRQBXY-BqJEi6Pi.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-DkrpnflX.js → classDiagram-v2-NBCMYWYE-ByU_GnxT.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-BNDwYBt9.js → cose-bilkent-JH36ORCC-D-Uglxqq.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-JUkNr1Mk.js → cynefin-OW5HDTMX-CZWO7aYj.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-DKeh9N4M.js → cynefinDiagram-VND7K2PF-C70-w0la.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-ve-6T_wX.js → dagre-6A5THRUB-CnhjhExB.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-BuJ2d8rT.js → diagram-22UHCM2B-bnjWCNb4.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-BgOs_Ni-.js → diagram-3UASUU5V-A4uTWoFV.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-DMK6VmcP.js → diagram-ATOU4E4O-Zi6m3NUl.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-DksASlcY.js → diagram-CDSNMT55-DtCcmd5k.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-Bgn8I5k6.js → diagram-MLGK6HIB-DofuETS3.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-zK0jqO8N.js → diagram-MPIPVDR6-Dw2T9PuU.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-vKrbzTwr.js → ebnfDiagram-ZINNZB2B-DbrwPiem.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-Bzv2mPNf.js → elk-276RUBZZ-BvCt1dCb.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-CvOyYuxu.js → erDiagram-OPXOYQCR-Y-QKqPnU.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-CtbnDbc0.js → flowDiagram-KWPJA3E3-CDkL1B5w.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-c4dMRYyG.js → ganttDiagram-FUAMR5RP-CKV5D2F6.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-B34uaQho.js → gitGraphDiagram-X574FWY7-nL9iFTCv.js} +1 -1
  34. package/dist/assets/{index-CPiqc_DN.js → index-DreDnjOu.js} +1 -1
  35. package/dist/assets/index-Du7o6Dsf.js +610 -0
  36. package/dist/assets/index-KxG4bWSr.css +1 -0
  37. package/dist/assets/{infoDiagram-VRGFBTTK-o64ma4CI.js → infoDiagram-VRGFBTTK-C7aHjzsf.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-DV4C9BAm.js → ishikawaDiagram-OU5B5YK6-KUZsNBTh.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-Bf0AHk1H.js → journeyDiagram-ZHPQQLJL-CGUuodFl.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-B1UMSKmE.js → kanban-definition-PNTS6WVX-Cy6r868Z.js} +1 -1
  41. package/dist/assets/{linear-BpKhuyLm.js → linear-Bl669148.js} +1 -1
  42. package/dist/assets/{mermaid.core-B_BKNCw5.js → mermaid.core-86Xqz5Mr.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-DeARruT1.js → mindmap-definition-NLK3R4M7-G4DzHLrd.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-BOQEk_1D.js → pegDiagram-GJSIUBJH-K4eruzY0.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-EIfoiTZo.js → pieDiagram-5QR66LMP-DYdR9PPq.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-GgZwT-fi.js → quadrantDiagram-O4NWA36T-BCAh0nEy.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-D9rDQomj.js → railroadDiagram-XR7U4H2S-p0dGRc4g.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-DAh1MIeo.js → requirementDiagram-PLB6GJNP-tLeBG0aL.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-CPr6Uz64.js → sankeyDiagram-IPEJSGJF-QRuN16rV.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-BU6VIdcm.js → sequenceDiagram-PO4LG4MO-ueT1nVoa.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-qF2HO41r.js → sizeCapture-INFHLROL-wRS2zWg2.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-DlJ8G4MD.js → stateDiagram-v2-GCMORJYK-CYzA_hE1.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-DzYE8I54.js → swimlanes-2SLR337P-kDnv6LMc.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-C7voMDle.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-B7wzEQ0A.js → timeline-definition-EJHVYXUP-D1y2UbbH.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-BvGgrxIP.js → usecaseDiagram-POWQR4AR-IqB4v9HX.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-CEECdgQg.js → vennDiagram-UO4OBE2U-BEi7KYVF.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-XQHA0P_P.js → wardleyDiagram-VNRHLVJA-Dkybo-Yf.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-B5mLcZI2.js → xychartDiagram-PMCCYNJV-BAPFJLpg.js} +1 -1
  60. package/dist/index.html +2 -2
  61. package/dist-server/cloud-home-start.js +27 -13
  62. package/dist-server/companion/src/listener.js +39 -18
  63. package/dist-server/companion/src/routes.js +39 -0
  64. package/dist-server/container-mcp.js +16 -29
  65. package/dist-server/drivers/agents-proxy.js +161 -82
  66. package/dist-server/enterprise/server/index.js +2 -1
  67. package/dist-server/{browser-proxy.js → harness-mcp-proxy.js} +34 -19
  68. package/dist-server/index.js +4991 -4908
  69. package/dist-server/local-computer.js +2 -1
  70. package/dist-server/openmausbot.js +906 -1785
  71. package/dist-server/pair-cli.js +906 -1785
  72. package/dist-server/proxy-paths.js +2 -1
  73. package/dist-server/server/account-signin.js +0 -2
  74. package/dist-server/server/activity.js +201 -0
  75. package/dist-server/server/admin-activity.js +1 -4
  76. package/dist-server/server/auto-approve.js +25 -4
  77. package/dist-server/server/automatic-recovery.js +1 -1
  78. package/dist-server/server/boat-create-idempotency.js +0 -7
  79. package/dist-server/server/boat-delete-journal.js +0 -4
  80. package/dist-server/server/boat.js +4 -4
  81. package/dist-server/server/bot-package.js +1 -19
  82. package/dist-server/server/browser-runtime.js +1 -2
  83. package/dist-server/server/checkpoints.js +90 -148
  84. package/dist-server/server/chief-of-staff.js +9 -9
  85. package/dist-server/server/cli.js +33 -26
  86. package/dist-server/server/cloud-backend.js +0 -4
  87. package/dist-server/server/cloud-computer-tools.js +110 -0
  88. package/dist-server/server/cloud-home-start.js +12 -15
  89. package/dist-server/server/cloud-home.js +0 -5
  90. package/dist-server/server/cloud-lending.js +0 -3
  91. package/dist-server/server/cloud-move-http.js +44 -30
  92. package/dist-server/server/cloud-move.js +20 -18
  93. package/dist-server/server/composio.js +41 -5
  94. package/dist-server/server/computer-control.js +0 -15
  95. package/dist-server/server/computer-observation.js +0 -74
  96. package/dist-server/server/config.js +121 -65
  97. package/dist-server/server/container-computer.js +23 -24
  98. package/dist-server/server/control-client.js +4 -10
  99. package/dist-server/server/decider/jev.js +1 -4
  100. package/dist-server/server/decider/log.js +1 -3
  101. package/dist-server/server/decision-log.js +1 -4
  102. package/dist-server/server/delegations.js +3 -2
  103. package/dist-server/server/drivers/acp/core.js +11 -8
  104. package/dist-server/server/drivers/acp/opencode-go.js +17 -16
  105. package/dist-server/server/drivers/agents-call.js +88 -92
  106. package/dist-server/server/drivers/agents-catalog.js +90 -11
  107. package/dist-server/server/drivers/agents-proxy.js +10 -31
  108. package/dist-server/server/drivers/antigravity.js +1 -4
  109. package/dist-server/server/drivers/boatagent.js +23 -4
  110. package/dist-server/server/drivers/cerebras.js +3 -3
  111. package/dist-server/server/drivers/chat-mcp-tools.js +4 -7
  112. package/dist-server/server/drivers/claude-login-auth.js +2 -11
  113. package/dist-server/server/drivers/claude.js +16 -14
  114. package/dist-server/server/drivers/codex-device-auth.js +3 -1
  115. package/dist-server/server/drivers/codex.js +5 -4
  116. package/dist-server/server/drivers/grok.js +1 -1
  117. package/dist-server/server/drivers/openai-chat-protocol.js +4 -2
  118. package/dist-server/server/drivers/openai-chat.js +18 -11
  119. package/dist-server/server/drivers/prompt-split.js +4 -4
  120. package/dist-server/server/drivers/retry.js +8 -4
  121. package/dist-server/server/engine-install.js +1 -5
  122. package/dist-server/server/harness/bus.js +49 -0
  123. package/dist-server/server/harness/registry.js +0 -1
  124. package/dist-server/server/{browser-proxy.js → harness-mcp-proxy.js} +40 -20
  125. package/dist-server/server/index.js +1190 -971
  126. package/dist-server/server/lending-memory.js +12 -8
  127. package/dist-server/server/mcp-registry.js +35 -0
  128. package/dist-server/server/memory-entries.js +38 -8
  129. package/dist-server/server/memory-organize.js +1 -8
  130. package/dist-server/server/memory-topics.js +4 -4
  131. package/dist-server/server/memory-upkeep.js +13 -15
  132. package/dist-server/server/message-file.js +9 -13
  133. package/dist-server/server/outbound-counts.js +79 -0
  134. package/dist-server/server/outbound-requests.js +60 -0
  135. package/dist-server/server/package-import.js +1 -1
  136. package/dist-server/server/peer-delivery.js +5 -19
  137. package/dist-server/server/peer-roster.js +30 -4
  138. package/dist-server/server/profile-versions.js +0 -31
  139. package/dist-server/server/provider-key-check.js +2 -1
  140. package/dist-server/server/proxy-paths.js +2 -1
  141. package/dist-server/server/restart.js +40 -0
  142. package/dist-server/server/room-handoffs.js +17 -13
  143. package/dist-server/server/routes/bot-memory.js +0 -24
  144. package/dist-server/server/routine-requests.js +2 -5
  145. package/dist-server/server/routines.js +10 -5
  146. package/dist-server/server/server-launcher.js +53 -0
  147. package/dist-server/server/setup-mode.js +0 -2
  148. package/dist-server/server/skill-learn.js +0 -1
  149. package/dist-server/server/skill-library.js +0 -15
  150. package/dist-server/server/store.js +10 -2
  151. package/dist-server/server/surface.js +69 -28
  152. package/dist-server/server/system-prompt.js +17 -7
  153. package/dist-server/server/team-manifest.js +1 -6
  154. package/dist-server/server/team-memory.js +209 -0
  155. package/dist-server/server/thread-events.js +2 -2
  156. package/dist-server/server/tunnel.js +4 -1
  157. package/dist-server/server/turn-images.js +1 -21
  158. package/dist-server/server/turn-resources.js +19 -150
  159. package/dist-server/server/usage-ledger.js +3 -1
  160. package/dist-server/server/vps-computer.js +4 -14
  161. package/dist-server/server/workspace.js +87 -50
  162. package/dist-server/server-launcher.js +72 -0
  163. package/dist-server/shared/activity.js +3 -0
  164. package/dist-server/shared/ask-question.js +1 -1
  165. package/dist-server/shared/bot-avatar.js +8 -0
  166. package/dist-server/shared/cloud-computer.js +12 -0
  167. package/dist-server/shared/connector-scopes.js +78 -0
  168. package/dist-server/shared/failed-turn.js +25 -0
  169. package/dist-server/shared/live-approval.js +2 -2
  170. package/dist-server/shared/outbound.js +128 -0
  171. package/dist-server/shared/package-format.js +3 -3
  172. package/dist-server/shared/pairing-link.js +144 -0
  173. package/dist-server/shared/tool-surface.js +3 -2
  174. package/dist-server/vps-container-mcp.js +16 -29
  175. package/dist-server/workspace-backup.worker.js +14 -27
  176. package/enterprise/server/index.js +2 -1
  177. package/package.json +1 -1
  178. package/dist/assets/channel-CdJX6i8k.js +0 -1
  179. package/dist/assets/index-CPYrGsv1.js +0 -610
  180. package/dist/assets/index-CzT42GR8.css +0 -1
  181. package/dist/assets/swimlanesDiagram-TC7HE7FX-D-U4GtLQ.js +0 -8
  182. package/dist-server/server/claim-idle.js +0 -33
  183. package/dist-server/server/cloud-overflow.js +0 -167
  184. package/dist-server/server/drivers/chat-boat-tools.js +0 -90
  185. package/dist-server/server/drivers/grokagent.js +0 -4
  186. package/dist-server/server/openmaus-status-capsule.js +0 -329
  187. package/dist-server/server/tightening-requests.js +0 -435
  188. package/dist-server/shared/tightening-request.js +0 -87
@@ -10,80 +10,6 @@ export const emptyObservationMetrics = () => ({
10
10
  verificationSuccesses: 0,
11
11
  verificationFailures: 0,
12
12
  });
13
- export function normalizeCrop(raw, maxWidth, maxHeight) {
14
- if (!raw || typeof raw !== "object")
15
- return null;
16
- const value = raw;
17
- const x = Math.round(Number(value.x));
18
- const y = Math.round(Number(value.y));
19
- const width = Math.round(Number(value.width));
20
- const height = Math.round(Number(value.height));
21
- if (![x, y, width, height, maxWidth, maxHeight].every(Number.isFinite) ||
22
- maxWidth <= 0 ||
23
- maxHeight <= 0 ||
24
- x < 0 ||
25
- y < 0 ||
26
- width < 32 ||
27
- height < 32) {
28
- return null;
29
- }
30
- if (x + width > maxWidth || y + height > maxHeight)
31
- return null;
32
- return { x, y, width, height };
33
- }
34
- /** Canonical value for internal navigation checks. Credentials are never
35
- * needed for equality and are removed here; query and fragment remain so
36
- * two distinct application states cannot verify as the same destination. */
37
- export function normalizeBrowserUrl(value) {
38
- if (typeof value !== "string" || !value || value.length > 8_192)
39
- return null;
40
- try {
41
- const url = new URL(value);
42
- if (!/^https?:$/.test(url.protocol))
43
- return null;
44
- url.username = "";
45
- url.password = "";
46
- return url.toString();
47
- }
48
- catch {
49
- return null;
50
- }
51
- }
52
- /** Removes credentials, query, and fragment before browser state reaches a model or log. */
53
- export function safeBrowserUrl(value) {
54
- const normalized = normalizeBrowserUrl(value);
55
- if (!normalized)
56
- return null;
57
- const url = new URL(normalized);
58
- url.search = "";
59
- url.hash = "";
60
- const safe = url.toString();
61
- return safe.length <= 2_048 ? safe : null;
62
- }
63
- /** Parses Chrome's /json/list response into a small, safe structured observation. */
64
- export function parseBrowserTargets(raw) {
65
- if (raw.length > 1_000_000)
66
- return [];
67
- try {
68
- const parsed = JSON.parse(raw);
69
- if (!Array.isArray(parsed))
70
- return [];
71
- return parsed.slice(0, 20).flatMap((item) => {
72
- if (!item || typeof item !== "object")
73
- return [];
74
- const value = item;
75
- const comparisonUrl = normalizeBrowserUrl(value.url);
76
- const url = safeBrowserUrl(value.url);
77
- if (value.type !== "page" || !url || !comparisonUrl || typeof value.id !== "string")
78
- return [];
79
- const title = typeof value.title === "string" ? value.title.replace(/\s+/g, " ").trim().slice(0, 200) : "";
80
- return [{ id: value.id.slice(0, 100), title, url, comparisonUrl }];
81
- });
82
- }
83
- catch {
84
- return [];
85
- }
86
- }
87
13
  /**
88
14
  * Keeps observations cheap without claiming the screen is immutable. Every
89
15
  * requested observation still captures fresh pixels (pages can change without
@@ -1,7 +1,7 @@
1
1
  // Config + data dirs. One file, ~/.openmausbot/config.json, env fallbacks:
2
2
  // { "xai": {"key":"xai-…"}, "composio": {"apiKey":"ak_…"}, "box": {"token":"…"},
3
3
  // "instances": { "<instanceId>": {"driver":"grok", …} } }
4
- import { readFileSync, mkdirSync, existsSync, renameSync } from "node:fs";
4
+ import { readFileSync, mkdirSync, existsSync, renameSync, statSync } from "node:fs";
5
5
  import { homedir } from "node:os";
6
6
  import { join } from "node:path";
7
7
  import { z } from "zod";
@@ -13,7 +13,6 @@ import { isModelVariant } from "./contracts.js";
13
13
  import { PROVIDER_ICON_PRESETS, providerIconError } from "../shared/provider-icon.js";
14
14
  import { isRemoteMcpServer, parseStoredMcpServer } from "./mcp-registry.js";
15
15
  import { parseJson, schemaIssue } from "./schema.js";
16
- import { CLOUD_SEAT_IDLE_STOP_MS } from "./cloud-overflow.js";
17
16
  const optionalText = z.string().optional();
18
17
  const SSH_ALIAS = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/;
19
18
  const LEGACY_BROWSER_PROFILE_ID = /^[A-Za-z0-9_-]{1,40}$/;
@@ -292,17 +291,6 @@ const featureConfigSchema = z.object({
292
291
  * conversations that share words with the message ride into the turn.
293
292
  * Read-only; on unless explicitly switched off — see autoRecallEnabled. */
294
293
  autoRecall: z.boolean().optional(),
295
- /** Idle release for computer claims (#1653): a desktop seat that stays
296
- * screen-quiet for 90 seconds is released to waiting turns while its
297
- * holder's turn still lives; the previous holder re-claims directly
298
- * for 10 minutes and yields to an occupied seat. Off until baked; see
299
- * computerClaimIdleReleaseEnabled for how to enable it by hand. */
300
- computerClaimIdleRelease: z.boolean().optional(),
301
- /** Consented cloud overflow for local computer waits (#1655): a wait
302
- * may offer a per-second-billed cloud seat, but only through an explicit
303
- * consent card or a configured allowlist — never a silent Auto default.
304
- * Off until baked; see cloudOverflowEnabled for how to enable it by hand. */
305
- cloudOverflow: z.boolean().optional(),
306
294
  /** Opt-in shared skills library: one store at the data dir that bots
307
295
  * read by assignment instead of per-workspace copies. Off until
308
296
  * explicitly enabled — see skillsLibraryEnabled. */
@@ -436,8 +424,12 @@ const appConfigSchema = z.object({
436
424
  * provider is Boat now and the key is kept for compatibility. */
437
425
  box: z.object({ token: optionalText }).optional(),
438
426
  vps: vpsConfigSchema.optional(),
439
- /** Optional OpenCode key; persisted write-only and passed only to its child. */
440
- opencodeGo: z.object({ apiKey: optionalText }).optional(),
427
+ /** Optional OpenCode key; persisted write-only and passed only to its child.
428
+ * `providerKeys`: keys for OpenCode's other providers (Venice, Groq…) by the
429
+ * environment name OpenCode reads, also write-only and only for its child.
430
+ * Settings changes go through mergeOpenCodeProviderKeys; the stored copy
431
+ * is read loosely (storedAppConfigSchema). */
432
+ opencodeGo: z.object({ apiKey: optionalText, providerKeys: z.record(z.string(), z.string()).optional() }).optional(),
441
433
  /** Voice settings and the selected voice id. `provider` picks the
442
434
  * engine: "elevenlabs" (default; needs `key`), "fish" (needs its own
443
435
  * `fishKey`; `fishModel` picks its speech model), "system" (the Mac's
@@ -526,15 +518,6 @@ const appConfigSchema = z.object({
526
518
  /** The authorization decision log (server/decision-log.ts): days of month
527
519
  * files kept, at least; OMB_DECISION_RETENTION_DAYS wins when set. */
528
520
  decisions: z.object({ retentionDays: z.number().int().min(1).max(3650).optional() }).strict().optional(),
529
- /** #1655 cloud-overflow settings. perSecondCostUsd is the operator's own
530
- * verified rate: with no price configured the feature stays inert rather
531
- * than show an invented one. allowlistedThreads carries standing consent
532
- * for exact thread ids. */
533
- cloudOverflow: z.object({
534
- perSecondCostUsd: z.number().positive().max(10).optional(),
535
- idleStopMs: z.number().int().positive().max(24 * 60 * 60_000).optional(),
536
- allowlistedThreads: z.array(z.string().min(1)).max(1000).optional(),
537
- }).strict().optional(),
538
521
  localVm: localVmConfigSchema.optional(),
539
522
  features: featureConfigSchema.optional(),
540
523
  onboarding: onboardingConfigSchema.optional(),
@@ -550,6 +533,15 @@ const appConfigSchema = z.object({
550
533
  });
551
534
  const storedAppConfigSchema = appConfigSchema.extend({
552
535
  browserProfiles: storedBrowserProfilesSchema.optional(),
536
+ /** Read loosely, like mcpServers: a hand-edited provider key that isn't
537
+ * text is dropped here, and a bad name or key later
538
+ * (openCodeProviderKeys), never the whole file. */
539
+ opencodeGo: z.object({
540
+ apiKey: optionalText,
541
+ providerKeys: z.record(z.string(), z.unknown()).catch({})
542
+ .transform((keys) => Object.fromEntries(Object.entries(keys).filter((entry) => typeof entry[1] === "string")))
543
+ .optional(),
544
+ }).optional(),
553
545
  });
554
546
  const appConfigPatchSchema = appConfigSchema.omit({ instances: true, mcpServers: true, cliStartup: true, customDomain: true })
555
547
  .extend({ threads: threadsPatchSchema.optional(), newBots: newBotsPatchSchema.optional() });
@@ -749,43 +741,6 @@ export function claudeUserMcpEnabled(cfg) {
749
741
  export function llmThreadTitlesEnabled(cfg) {
750
742
  return cfg.features?.llmThreadTitles === true;
751
743
  }
752
- /** Idle release for computer claims (#1653): a whole-turn desktop hold
753
- * ends after a screen-quiet window (default 90 seconds, screen-poller
754
- * frames excluded) instead of at turn settle, and the previous holder
755
- * re-claims directly inside a reclaim window (default 10 minutes) while
756
- * yielding to a seat another turn already holds. Off unless an explicit
757
- * `true` — bake it as a maintainer-only flag first, exactly like
758
- * sharedComputers: enable by hand in ~/.openmausbot/config.json
759
- * (`{"features": {"computerClaimIdleRelease": true}}`) and restart. */
760
- export function computerClaimIdleReleaseEnabled(cfg) {
761
- return cfg.features?.computerClaimIdleRelease === true;
762
- }
763
- /** Consented cloud overflow for local computer waits (#1655): while a turn
764
- * waits for a local desktop, it may offer overflow to a per-second-billed
765
- * cloud seat — never silently: the card names the cost, and only an
766
- * explicit per-conversation consent or a configured allowlist thread may
767
- * start the machine, which then hard-stops after an idle window. Off
768
- * unless an explicit `true`, like computerClaimIdleRelease: enable by
769
- * hand in ~/.openmausbot/config.json
770
- * (`{"features": {"cloudOverflow": true}, "cloudOverflow": {"perSecondCostUsd": 0.0004}}`)
771
- * and restart. */
772
- export function cloudOverflowEnabled(cfg) {
773
- return cfg.features?.cloudOverflow === true;
774
- }
775
- /** The per-second rate shown on the card, or null when the operator has
776
- * not set one: the offer fails closed rather than guess a price. */
777
- export function cloudOverflowPerSecondCostUsd(cfg) {
778
- const cost = cfg.cloudOverflow?.perSecondCostUsd;
779
- return typeof cost === "number" && Number.isFinite(cost) && cost > 0 ? cost : null;
780
- }
781
- /** How long a cloud seat may stay screen-quiet before it is stopped. */
782
- export function cloudOverflowIdleStopMs(cfg) {
783
- return cfg.cloudOverflow?.idleStopMs ?? CLOUD_SEAT_IDLE_STOP_MS;
784
- }
785
- /** Threads with standing consent, by exact thread id. */
786
- export function cloudOverflowAllowlistedThreads(cfg) {
787
- return new Set(cfg.cloudOverflow?.allowlistedThreads ?? []);
788
- }
789
744
  /** Opt-in shared skills library (skills lane S1): one store at the data dir
790
745
  * that bots reference by assignment instead of per-workspace copies. Off
791
746
  * unless enabled by hand in ~/.openmausbot/config.json
@@ -815,7 +770,6 @@ export const FLEET_NEUTRAL_KEYS = new Set([
815
770
  "memory",
816
771
  "localVm",
817
772
  "features",
818
- "cloudOverflow",
819
773
  "browserProfiles",
820
774
  "onboarding",
821
775
  ]);
@@ -986,6 +940,34 @@ export function loadConfig() {
986
940
  }
987
941
  return cfg;
988
942
  }
943
+ /** `derive(loadConfig())`, worked out again only when config.json changes:
944
+ * a new file (every save renames one into place), a new size, a new
945
+ * modification time, or new permissions or owner. For values read on every
946
+ * request or stream frame, where parsing the file each time costs more than
947
+ * the work itself. A file that is missing, saved in the last two seconds, or
948
+ * that loadConfig() could not use (unreadable, a failed read, invalid JSON)
949
+ * is read on every call: a second write inside one clock tick is never
950
+ * missed, and a file that is fixed is used again on the next call. */
951
+ export function cacheUntilConfigChanges(derive) {
952
+ let cached = null;
953
+ return () => {
954
+ let stamp = null;
955
+ try {
956
+ const file = statSync(join(DATA_DIR, "config.json"), { bigint: true });
957
+ if (Date.now() - Number(file.mtimeMs) >= 2_000)
958
+ stamp = `${file.ino}:${file.size}:${file.mtimeNs}:${file.ctimeNs}`;
959
+ }
960
+ catch {
961
+ // Missing or out of reach: read it the way loadConfig() always has.
962
+ }
963
+ if (stamp !== null && cached?.stamp === stamp)
964
+ return cached.value;
965
+ const value = derive(loadConfig());
966
+ // A warning means loadConfig() ignored the file and ran on defaults.
967
+ cached = stamp === null || lastIgnoredConfigWarning !== "" ? null : { stamp, value };
968
+ return value;
969
+ };
970
+ }
989
971
  /** After saveConfig() writes a credential, the running process's env must
990
972
  * follow the newest value — loadConfig() prefers env, so the secret injected
991
973
  * at boot would otherwise shadow the save until relaunch: the UI would show
@@ -1123,6 +1105,75 @@ export const PROVIDER_CREDENTIAL_ENV = [
1123
1105
  "CURSOR_API_KEY",
1124
1106
  "CURSOR_AUTH_TOKEN",
1125
1107
  ];
1108
+ /** Provider keys OpenCode reads from its environment, as it does in a
1109
+ * terminal: with ANTHROPIC_API_KEY set, `opencode` lists Anthropic's models.
1110
+ * Keys OpenMaus saves for another engine (xAI, Mistral, the workspace
1111
+ * Anthropic key) are workspace credentials under other names and never
1112
+ * ride along. */
1113
+ export const OPENCODE_PROVIDER_ENV = [
1114
+ "ANTHROPIC_API_KEY",
1115
+ "OPENAI_API_KEY",
1116
+ "GEMINI_API_KEY",
1117
+ "GOOGLE_API_KEY",
1118
+ "KIMI_API_KEY",
1119
+ "MOONSHOT_API_KEY",
1120
+ "MINIMAX_API_KEY",
1121
+ ];
1122
+ /** Keys the owner saves in Settings for OpenCode's other providers (Venice,
1123
+ * Groq, DeepSeek…), each under the environment name OpenCode reads. A Cloud
1124
+ * has no terminal to export them in. They go to OpenCode's process only,
1125
+ * never to this server's own environment. */
1126
+ export const OPENCODE_PROVIDER_KEY_LIMIT = 20;
1127
+ const OPENCODE_PROVIDER_KEY_NAME = /^[A-Z][A-Z0-9_]{0,59}_(?:API_KEY|KEY|TOKEN)$/;
1128
+ /** Printable, no spaces: no provider key has any, and a pasted sentence is not a key. */
1129
+ const OPENCODE_PROVIDER_KEY_VALUE = /^[\x21-\x7e]{1,4096}$/;
1130
+ /** Why OpenCode can't be given a key under this name, or undefined. A name
1131
+ * OpenMaus keeps for itself or another engine would never reach OpenCode:
1132
+ * every engine keeps only the credential names it reads (credentialEnv). */
1133
+ function openCodeProviderKeyNameRefusal(name) {
1134
+ if (!OPENCODE_PROVIDER_KEY_NAME.test(name)) {
1135
+ return "Use the name the provider reads its key from: capitals, ending in _API_KEY, _KEY or _TOKEN, such as VENICE_API_KEY.";
1136
+ }
1137
+ if (name === "OPENCODE_API_KEY")
1138
+ return "Save the OpenCode key in the OpenCode API key box instead.";
1139
+ const reserved = name.startsWith("OPENCODE_") || name.startsWith("OMB_")
1140
+ || WORKSPACE_CREDENTIAL_ENV.includes(name)
1141
+ || (PROVIDER_CREDENTIAL_ENV.includes(name) && !OPENCODE_PROVIDER_ENV.includes(name));
1142
+ return reserved ? `OpenMaus keeps ${name} for itself, so OpenCode can't be given it here. Put this key in opencode.json instead.` : undefined;
1143
+ }
1144
+ /** The saved OpenCode provider keys that can be used, by name. A hand-edited
1145
+ * entry OpenCode could not be given, or whose value is not a key, is
1146
+ * skipped; so is anything past the limit. */
1147
+ export function openCodeProviderKeys(cfg) {
1148
+ const saved = cfg.opencodeGo?.providerKeys ?? {};
1149
+ const usable = Object.keys(saved).sort()
1150
+ .filter((name) => !openCodeProviderKeyNameRefusal(name) && OPENCODE_PROVIDER_KEY_VALUE.test(saved[name] ?? ""))
1151
+ .slice(0, OPENCODE_PROVIDER_KEY_LIMIT);
1152
+ return Object.fromEntries(usable.map((name) => [name, saved[name]]));
1153
+ }
1154
+ /** A Settings change to the saved OpenCode provider keys: a key saves or
1155
+ * replaces its name, "" removes it, and every other saved key stays. Returns
1156
+ * the whole new set to store, or why it was refused. */
1157
+ export function mergeOpenCodeProviderKeys(cfg, change) {
1158
+ const keys = new Map(Object.entries(openCodeProviderKeys(cfg)));
1159
+ for (const [name, raw] of Object.entries(change)) {
1160
+ const value = raw.trim();
1161
+ if (!value) {
1162
+ keys.delete(name);
1163
+ continue;
1164
+ }
1165
+ const refusal = openCodeProviderKeyNameRefusal(name);
1166
+ if (refusal)
1167
+ return { ok: false, error: refusal };
1168
+ if (!OPENCODE_PROVIDER_KEY_VALUE.test(value))
1169
+ return { ok: false, error: "That doesn't look like a key. Paste only the key, with no spaces." };
1170
+ keys.set(name, value);
1171
+ }
1172
+ if (keys.size > OPENCODE_PROVIDER_KEY_LIMIT) {
1173
+ return { ok: false, error: `OpenCode can hold up to ${OPENCODE_PROVIDER_KEY_LIMIT} provider keys. Remove one you no longer use, then add this one.` };
1174
+ }
1175
+ return { ok: true, keys: Object.fromEntries([...keys].sort(([a], [b]) => (a < b ? -1 : 1))) };
1176
+ }
1126
1177
  const configSaveListeners = new Set();
1127
1178
  /** Told, synchronously, what each saveConfig wrote: the file before and
1128
1179
  * after. The admin activity log (server/admin-activity.ts) records the
@@ -1152,7 +1203,7 @@ export function saveConfig(patch, options = {}) {
1152
1203
  const storedProfiles = storedBrowserProfilesSchema.safeParse(disk.browserProfiles);
1153
1204
  if (storedProfiles.success)
1154
1205
  disk.browserProfiles = storedProfiles.data;
1155
- 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", "cloudOverflow", "budgets", "billing", "decisions", "onboarding", "browserEngine", "newBots"]) {
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"]) {
1156
1207
  const section = checkedPatch[key];
1157
1208
  if (!section)
1158
1209
  continue;
@@ -1439,8 +1490,13 @@ function injectedEnvironment(cfg, instanceId, driver) {
1439
1490
  // to Cloud Pro's included token, which never enters an environment map.
1440
1491
  if (driver === "boxAgent" && cfg.box?.token)
1441
1492
  environment.set("BOX_TOKEN", cfg.box.token);
1442
- if (driver === "opencodeGo" && cfg.opencodeGo?.apiKey)
1443
- environment.set("OPENCODE_API_KEY", cfg.opencodeGo.apiKey);
1493
+ if (driver === "opencodeGo") {
1494
+ // Keys for OpenCode's other providers, under the names OpenCode reads.
1495
+ for (const [name, key] of Object.entries(openCodeProviderKeys(cfg)))
1496
+ environment.set(name, key);
1497
+ if (cfg.opencodeGo?.apiKey)
1498
+ environment.set("OPENCODE_API_KEY", cfg.opencodeGo.apiKey);
1499
+ }
1444
1500
  return environment;
1445
1501
  }
1446
1502
  /** One instance per pasted provider key, each running only on its own key.
@@ -46,9 +46,7 @@ const RUNTIMES = ["docker", "podman", "container"];
46
46
  const INTERNAL_VIEWER_PORT = 6901;
47
47
  const HOST_VIEWER_PORT = 6080;
48
48
  const MEMORY_BYTES = 4 * 1024 * 1024 * 1024;
49
- const NANO_CPUS = 2_000_000_000;
50
49
  const PIDS_LIMIT = 512;
51
- const SHM_BYTES = 512 * 1024 * 1024;
52
50
  export const SHARED_LOCAL_VM_TARGET = {
53
51
  key: "shared",
54
52
  containerName: CONTAINER,
@@ -374,11 +372,19 @@ function containerOwnershipLabelsMatch(labels, target) {
374
372
  function normalizeImageId(id) {
375
373
  return id?.trim().replace(/^sha256:/, "") || null;
376
374
  }
377
- function inspectedImage(stdout) {
375
+ function inspectedImage(stdout, runtime) {
378
376
  const parsed = JSON.parse(stdout);
379
377
  const image = parsed[0];
378
+ // Apple container runs on Apple Silicon and puts image labels inside each
379
+ // platform variant. Never accept another platform's labels or guess between
380
+ // multiple matching variants. Docker/Podman keep their existing inspect paths.
381
+ const variants = runtime === "container" && Array.isArray(image?.variants)
382
+ ? image.variants.filter(variant => variant?.platform?.os === "linux" && variant.platform.architecture === "arm64")
383
+ : [];
380
384
  return {
381
- labels: image?.Config?.Labels ?? image?.config?.Labels ?? image?.config?.labels ?? image?.configuration?.labels,
385
+ labels: runtime === "container"
386
+ ? (variants.length === 1 ? variants[0]?.config?.config?.Labels : undefined)
387
+ : image?.Config?.Labels ?? image?.config?.Labels ?? image?.config?.labels ?? image?.configuration?.labels,
382
388
  id: normalizeImageId(image?.Id ?? image?.id ?? image?.configuration?.descriptor?.digest),
383
389
  };
384
390
  }
@@ -432,7 +438,7 @@ export async function containerComputerStatus(runner = sh, platform = process.pl
432
438
  }
433
439
  try {
434
440
  const { stdout } = await runner(status.runtime, ["image", "inspect", IMAGE]);
435
- const image = inspectedImage(stdout);
441
+ const image = inspectedImage(stdout, status.runtime);
436
442
  status.image = imageLabelsMatch(image.labels);
437
443
  status.image_id = image.id;
438
444
  }
@@ -604,14 +610,10 @@ function appleWorkspaceMountIsSafe(mounts, platform, expectedWorkspace) {
604
610
  mounts[0]?.destination === VM_WORKSPACE_GUEST &&
605
611
  !options.some((option) => option === "ro" || option === "readonly"));
606
612
  }
607
- /** One hardening contract for both managed containers (Local VM here, the
608
- * BYO-VPS backend in vps-computer.ts): exact resource limits, no privilege,
609
- * no host namespaces or devices, no disabled security profiles. The only
610
- * runtime-specific capability exception is Podman's Firefox sandbox chroot.
611
- * Callers also differ on restart policy — the VPS
612
- * container must survive a reboot nobody is watching ("unless-stopped"),
613
- * while Local VM starts remain controlled by OMB's idle policy and turn
614
- * lifecycle rather than a daemon restart policy. */
613
+ /** Shared isolation contract for Local VM and BYO-VPS containers. Resource
614
+ * budgets are creation defaults, not an isolation requirement. Podman alone
615
+ * needs chroot for Firefox's sandbox. Local VM starts stay controlled by
616
+ * OMB's idle policy; VPS restart policy belongs to the server operator. */
615
617
  export function dockerSecurityIsHardened(config, options = {}) {
616
618
  if (!config)
617
619
  return false;
@@ -621,26 +623,20 @@ export function dockerSecurityIsHardened(config, options = {}) {
621
623
  .sort();
622
624
  const unsafeSecurityOption = (config.SecurityOpt ?? []).some((option) => /(?:^|=)(?:unconfined|disable)$/i.test(option));
623
625
  const restartPolicy = config.RestartPolicy?.Name;
624
- const restartPolicyOk = options.restartPolicy === "unless-stopped"
626
+ const restartPolicyOk = options.restartPolicy === "any" || (options.restartPolicy === "unless-stopped"
625
627
  ? restartPolicy === "unless-stopped"
626
- : restartPolicy === undefined || restartPolicy === "" || restartPolicy === "no";
627
- return (config.Memory === MEMORY_BYTES &&
628
- (config.MemorySwap ?? 0) === MEMORY_BYTES &&
629
- (config.NanoCpus ?? 0) === NANO_CPUS &&
630
- config.PidsLimit === PIDS_LIMIT &&
631
- capDrop.includes("all") &&
628
+ : restartPolicy === undefined || restartPolicy === "" || restartPolicy === "no");
629
+ return (capDrop.includes("all") &&
632
630
  capAdd.join(",") === (options.podmanBrowserSandbox ? "setgid,setuid,sys_chroot" : "setgid,setuid") &&
633
631
  config.Privileged === false &&
634
632
  !config.PidMode &&
635
633
  config.IpcMode === "private" &&
636
634
  !config.UTSMode &&
637
- config.ShmSize === SHM_BYTES &&
638
635
  (!config.Devices || config.Devices.length === 0) &&
639
636
  (!config.DeviceRequests || config.DeviceRequests.length === 0) &&
640
637
  !unsafeSecurityOption &&
641
638
  !config.UsernsMode &&
642
639
  config.CgroupnsMode === "private" &&
643
- config.OomKillDisable !== true &&
644
640
  config.AutoRemove !== true &&
645
641
  restartPolicyOk);
646
642
  }
@@ -711,8 +707,11 @@ async function ensureVmWorkspace(platform, target) {
711
707
  if (platform !== "win32")
712
708
  await chmod(target.workspaceDir, 0o700);
713
709
  }
710
+ function baseImagePullArgs(runtime) {
711
+ return runtime === "container" ? ["image", "pull", BASE_IMAGE] : ["pull", BASE_IMAGE];
712
+ }
714
713
  async function prepareManagedImage(runtime, runner) {
715
- await runner(runtime, ["pull", BASE_IMAGE], 10 * 60_000);
714
+ await runner(runtime, baseImagePullArgs(runtime), 10 * 60_000);
716
715
  const context = await mkdtemp(join(tmpdir(), "openmausbot-cua-image-"));
717
716
  try {
718
717
  await writeFile(join(context, "Dockerfile"), managedImageDockerfile(), { mode: 0o600 });
@@ -950,7 +949,7 @@ export function setupCommands(runtime, platform = process.platform, target = SHA
950
949
  runtimeStart,
951
950
  // This is the inspectable base download. The normal Prepare button also
952
951
  // builds the checksum-pinned 0.20.0 derivative automatically.
953
- pull: command(["pull", BASE_IMAGE]),
952
+ pull: command(baseImagePullArgs(runtime)),
954
953
  run: runtime === "container" && target.key !== SHARED_LOCAL_VM_TARGET.key
955
954
  ? null
956
955
  : command(containerRunArgs(runtime, "CHANGE_ME", target)),
@@ -90,16 +90,10 @@ export function createControlClient(options) {
90
90
  },
91
91
  };
92
92
  }
93
- /** The one sentence every refused action gets. Deliberately does not vary
94
- * per tool: the model needs the same three facts every time — nothing
95
- * happened, don't retry blindly, and how to wait properly. */
96
- export const CONTROL_REFUSAL = "A person has taken control of this computer, so this call was NOT performed. " +
97
- "Do not retry it — the screen is changing under their hands. " +
98
- "Call computer_request_help (no reason needed) to wait for them to finish, " +
99
- "then take a fresh screenshot before your next action.";
100
- /** The bridge-gated computers (Local VM, VPS) speak Cua Driver's own tool
101
- * surface, which has no wait tool to point at — so the guidance is to
102
- * pause, not to call anything. */
93
+ /** The one sentence every refused computer action gets, on every computer.
94
+ * Deliberately does not vary per tool: the model needs the same three facts
95
+ * every time — nothing happened, don't retry blindly, and wait for the
96
+ * person. */
103
97
  export const CONTROL_REFUSAL_PLAIN = "A person has taken control of this computer, so this call was NOT performed. " +
104
98
  "Do not retry it — the screen is changing under their hands. " +
105
99
  "Pause this task, tell the person you are waiting for them to hand control back, " +
@@ -1,12 +1,9 @@
1
+ import { isLoopback } from "../provider-key-check.js";
1
2
  export const JEV_DEFAULT_BASE_URL = "https://api.typesafe.ai";
2
3
  export const JEV_MODEL = "jev-latest";
3
4
  export const JEV_MAX_OPTIONS = 255;
4
5
  export const JEV_MIN_LEVELS = 2;
5
6
  export const JEV_MAX_LEVELS = 10;
6
- function isLoopback(hostname) {
7
- const host = hostname.replace(/^\[|\]$/g, "");
8
- return host === "localhost" || host === "127.0.0.1" || host === "::1" || host.startsWith("127.");
9
- }
10
7
  /** The endpoint for a base URL, or null when the key must not be sent there:
11
8
  * https anywhere, plain http only to this machine (a local Jev-compatible
12
9
  * server or a test double). */
@@ -14,6 +14,7 @@ import { createHash } from "node:crypto";
14
14
  import { appendFile, mkdir } from "node:fs/promises";
15
15
  import { join } from "node:path";
16
16
  import { boundRetentionDays, pruneMonthFiles } from "../decision-log.js";
17
+ import { monthKey } from "../usage-ledger.js";
17
18
  export const DECIDER_LOG_DIR = "decider-log";
18
19
  const PRUNE_EVERY_MS = 60 * 60_000;
19
20
  const writeQueues = new Map();
@@ -28,9 +29,6 @@ export function stateHash(state) {
28
29
  }
29
30
  return createHash("sha256").update(text).digest("hex").slice(0, 16);
30
31
  }
31
- function monthKey(at) {
32
- return `${at.getUTCFullYear()}-${String(at.getUTCMonth() + 1).padStart(2, "0")}`;
33
- }
34
32
  export function deciderLogFileFor(dataDir, at) {
35
33
  return join(dataDir, DECIDER_LOG_DIR, `${monthKey(at)}.ndjson`);
36
34
  }
@@ -32,7 +32,7 @@ import { readdirSync, readFileSync } from "node:fs";
32
32
  import { appendFile, mkdir, readdir, stat, unlink } from "node:fs/promises";
33
33
  import { basename, join } from "node:path";
34
34
  import { redactSecrets } from "./redact.js";
35
- import { csvCell } from "./usage-ledger.js";
35
+ import { csvCell, monthKey } from "./usage-ledger.js";
36
36
  const DIR = "decisions";
37
37
  const LEGACY_FILE = "decisions.ndjson";
38
38
  const MONTH_FILE = /^(\d{4})-(\d{2})\.ndjson$/;
@@ -70,9 +70,6 @@ const actorScope = new AsyncLocalStorage();
70
70
  export function withDecisionActor(actor, work, via) {
71
71
  return actorScope.run(via ? { actor, via } : { actor }, work);
72
72
  }
73
- function monthKey(at) {
74
- return `${at.getUTCFullYear()}-${String(at.getUTCMonth() + 1).padStart(2, "0")}`;
75
- }
76
73
  export function decisionFileFor(dataDir, at) {
77
74
  return join(dataDir, DIR, `${monthKey(at)}.ndjson`);
78
75
  }
@@ -15,6 +15,7 @@ import { join } from "node:path";
15
15
  import { writeFileAtomic } from "./atomic.js";
16
16
  import { getOrCreateChannel, mirrorExchange } from "./comms-visibility.js";
17
17
  import { DATA_DIR } from "./config.js";
18
+ import { failedTurnTool } from "../shared/failed-turn.js";
18
19
  import { newId } from "./contracts.js";
19
20
  import { peerApprovalFailure, requestPeerApproval } from "./peer-approval.js";
20
21
  import { canAccessTeam, peerAllowed } from "./peer-roster.js";
@@ -383,7 +384,7 @@ onSettled) {
383
384
  appendDeliveryMessage(bus, threadId, item, {
384
385
  role: "bot",
385
386
  kind: "activity",
386
- tool: { name: `error: delegation failed — ${why.slice(0, 120)}`, ok: false },
387
+ tool: failedTurnTool(`delegation failed — ${why}`),
387
388
  });
388
389
  }
389
390
  catch (reportError) {
@@ -607,7 +608,7 @@ async function processOne(bus, approvalBus, from, sourceThreadId, item, runTarge
607
608
  appendDeliveryMessage(bus, sourceThreadId, item, {
608
609
  role: "bot",
609
610
  kind: "activity",
610
- tool: { name: `error: delegation to ${item.toBotId} failed — no such bot`, ok: false },
611
+ tool: failedTurnTool(`delegation to ${item.toBotId} failed — no such bot`),
611
612
  });
612
613
  return "settled";
613
614
  }
@@ -91,10 +91,12 @@ function acpRpcError(value, method) {
91
91
  });
92
92
  }
93
93
  const envOr = (key, fallback) => Number(process.env[key] ?? fallback);
94
- const INIT_TIMEOUT = envOr("OPENMAUS_ACP_INIT_TIMEOUT_MS", 300_000);
95
- const SESSION_CONFIG_TIMEOUT = envOr("OPENMAUS_ACP_SESSION_CONFIG_TIMEOUT_MS", 300_000); // configureSession's per-request default
96
- const NEW_SESSION_TIMEOUT = envOr("OPENMAUS_ACP_NEW_SESSION_TIMEOUT_MS", 300_000);
97
- const LOAD_SESSION_TIMEOUT = envOr("OPENMAUS_ACP_LOAD_SESSION_TIMEOUT_MS", 120_000); // history replay on a long thread is slow
94
+ const INIT_TIMEOUT = 300_000;
95
+ const SESSION_CONFIG_TIMEOUT = 300_000; // configureSession's per-request default
96
+ const NEW_SESSION_TIMEOUT = 300_000;
97
+ const LOAD_SESSION_TIMEOUT = 120_000; // history replay on a long thread is slow
98
+ /** How long a quiet agent shows no sign of life before it is called stuck. */
99
+ const ACP_STUCK_AFTER_MS = 180_000;
98
100
  /** ACP agents may compact their own history without telling the client;
99
101
  * re-send the full prompt after this many bare turns as a backstop to
100
102
  * compaction detection. */
@@ -614,7 +616,7 @@ export function createAcpDriver(support) {
614
616
  // "no sign of life" is called only once that has lasted minutes.
615
617
  if (next.kind === "no-signs") {
616
618
  noSignsSince ??= now;
617
- if (now - noSignsSince < envOr("OMB_ACP_STUCK_AFTER_MS", 180_000))
619
+ if (now - noSignsSince < ACP_STUCK_AFTER_MS)
618
620
  next = { kind: "between-requests" };
619
621
  }
620
622
  else {
@@ -1262,9 +1264,10 @@ export function createAcpDriver(support) {
1262
1264
  // session, so the model is not a separate axis; fullAuto covers the
1263
1265
  // transformEnv-policy supports (opencode). mcpServers are session
1264
1266
  // establishment inputs — they ride session/new and session/load over
1265
- // the wire — and the harness mints fresh integration bearer tokens
1266
- // every turn. Most agents apply changes on live load; those that cache
1267
- // the old MCP clients must resume on a fresh process (see sessionKey).
1267
+ // the wire — and change when a turn's integrations or their grants do
1268
+ // (a thread keeps its integration credentials across turns). Most
1269
+ // agents apply changes on live load; those that cache the old MCP
1270
+ // clients must resume on a fresh process (see sessionKey).
1268
1271
  // The env the spawned child actually receives is part of the
1269
1272
  // contract too, and arrives hashed as envFingerprint for the same
1270
1273
  // reason.