openmausbot 0.1.89 → 0.1.91

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 (168) hide show
  1. package/dist/assets/{abnfDiagram-O67JEVCF-BwaMaZEm.js → abnfDiagram-O67JEVCF-yP-w8pRq.js} +1 -1
  2. package/dist/assets/{arc-Be-8GKxd.js → arc-DsivfZEh.js} +1 -1
  3. package/dist/assets/{architectureDiagram-NJMV4G6O-Dp_U2Kv1.js → architectureDiagram-NJMV4G6O-CILw7lzT.js} +1 -1
  4. package/dist/assets/{blockDiagram-BEXU5L5S-DQjUjYUk.js → blockDiagram-BEXU5L5S-DEQUV3HF.js} +1 -1
  5. package/dist/assets/{c4Diagram-YGBWAQC7-Bd3aE4Kc.js → c4Diagram-YGBWAQC7-lSXCZoKA.js} +1 -1
  6. package/dist/assets/channel-DrfGMu7X.js +1 -0
  7. package/dist/assets/{chunk-2Q5K7J3B-BijjTBeJ.js → chunk-2Q5K7J3B-QCD5bgWP.js} +1 -1
  8. package/dist/assets/{chunk-3FUC2YCW-CMGdwvzW.js → chunk-3FUC2YCW-C5-iZJuL.js} +2 -2
  9. package/dist/assets/{chunk-5VM5RSS4-0G09BfsZ.js → chunk-5VM5RSS4-cxsawMpY.js} +1 -1
  10. package/dist/assets/{chunk-GTNCS2PH-FOp3vSHe.js → chunk-GTNCS2PH-DV8tV8H7.js} +1 -1
  11. package/dist/assets/{chunk-GWA4HPMP-Dv-20VIn.js → chunk-GWA4HPMP-CJFZyfbW.js} +1 -1
  12. package/dist/assets/{chunk-JWPE2WC7-CRgrdFcz.js → chunk-JWPE2WC7-BigbMXco.js} +1 -1
  13. package/dist/assets/{chunk-NETBCI7D-3hP4f7QB.js → chunk-NETBCI7D-oVxTfpDy.js} +1 -1
  14. package/dist/assets/{chunk-WEXAMYUT-CG98IjlG.js → chunk-WEXAMYUT-CH4BiroW.js} +1 -1
  15. package/dist/assets/{chunk-XXDRQBXY-BrSTC9xG.js → chunk-XXDRQBXY-KEDa6gax.js} +1 -1
  16. package/dist/assets/{classDiagram-v2-NBCMYWYE-rpQQQDFX.js → classDiagram-v2-NBCMYWYE-BiPjfqVv.js} +1 -1
  17. package/dist/assets/{cose-bilkent-JH36ORCC-G6RSafHA.js → cose-bilkent-JH36ORCC-Caxew3OY.js} +1 -1
  18. package/dist/assets/{cynefin-OW5HDTMX-CXjv-IM2.js → cynefin-OW5HDTMX-B_Pui369.js} +1 -1
  19. package/dist/assets/{cynefinDiagram-VND7K2PF-Dw07MR9f.js → cynefinDiagram-VND7K2PF-X64ydIhP.js} +1 -1
  20. package/dist/assets/{dagre-6A5THRUB-BwPnlTg2.js → dagre-6A5THRUB-TqYCI1E1.js} +1 -1
  21. package/dist/assets/{diagram-22UHCM2B-Cm6xcMiz.js → diagram-22UHCM2B-TeOt3rAQ.js} +1 -1
  22. package/dist/assets/{diagram-3UASUU5V-CDpogPbE.js → diagram-3UASUU5V-D70BLhDA.js} +1 -1
  23. package/dist/assets/{diagram-ATOU4E4O-BV8z3Pnc.js → diagram-ATOU4E4O-BbQDLwUC.js} +1 -1
  24. package/dist/assets/{diagram-CDSNMT55-CKCQhOOC.js → diagram-CDSNMT55-ChNJZ1u-.js} +1 -1
  25. package/dist/assets/{diagram-MLGK6HIB-FGvJg69C.js → diagram-MLGK6HIB-DMBl2APa.js} +1 -1
  26. package/dist/assets/{diagram-MPIPVDR6-CwATvgnS.js → diagram-MPIPVDR6-CEqDchOp.js} +1 -1
  27. package/dist/assets/{ebnfDiagram-ZINNZB2B-CH_c3SSC.js → ebnfDiagram-ZINNZB2B-BVDKRp_-.js} +1 -1
  28. package/dist/assets/{elk-276RUBZZ-3guZi71R.js → elk-276RUBZZ-CRkofO9B.js} +1 -1
  29. package/dist/assets/{erDiagram-OPXOYQCR-BMHphR2n.js → erDiagram-OPXOYQCR-DruOMh5e.js} +1 -1
  30. package/dist/assets/{flowDiagram-KWPJA3E3-Vm5DkW5x.js → flowDiagram-KWPJA3E3-8GMQeB7t.js} +1 -1
  31. package/dist/assets/{ganttDiagram-FUAMR5RP-DO-1ct07.js → ganttDiagram-FUAMR5RP-ORE9jcsM.js} +1 -1
  32. package/dist/assets/{gitGraphDiagram-X574FWY7-nxeksEIN.js → gitGraphDiagram-X574FWY7-BmsedsWh.js} +1 -1
  33. package/dist/assets/index-BlnCEcU6.js +588 -0
  34. package/dist/assets/{index-Oxk9x45i.js → index-DKNEM8Q6.js} +1 -1
  35. package/dist/assets/index-DVxjOvID.css +1 -0
  36. package/dist/assets/{infoDiagram-VRGFBTTK-Dkcvr0p7.js → infoDiagram-VRGFBTTK-E_p7AooJ.js} +1 -1
  37. package/dist/assets/{ishikawaDiagram-OU5B5YK6-BwVibkoO.js → ishikawaDiagram-OU5B5YK6-Y-B2mKda.js} +1 -1
  38. package/dist/assets/{journeyDiagram-ZHPQQLJL-ByiBZqgG.js → journeyDiagram-ZHPQQLJL-eA8RELcK.js} +1 -1
  39. package/dist/assets/{kanban-definition-PNTS6WVX-qqjgXBF3.js → kanban-definition-PNTS6WVX-DqIL2e8e.js} +1 -1
  40. package/dist/assets/{linear-CtJnKe6A.js → linear-yC_29R6e.js} +1 -1
  41. package/dist/assets/{mermaid.core-BEiDklrY.js → mermaid.core-VhEMkSND.js} +5 -5
  42. package/dist/assets/{mindmap-definition-NLK3R4M7-DfIYHia9.js → mindmap-definition-NLK3R4M7-oJ7kyQZy.js} +1 -1
  43. package/dist/assets/{pegDiagram-GJSIUBJH-CYyRqa1a.js → pegDiagram-GJSIUBJH-BmfYcY0O.js} +1 -1
  44. package/dist/assets/{pieDiagram-5QR66LMP-hOToy_zH.js → pieDiagram-5QR66LMP-DKvgP7w9.js} +1 -1
  45. package/dist/assets/{quadrantDiagram-O4NWA36T-DKAsNN9I.js → quadrantDiagram-O4NWA36T-BEKL3W0L.js} +1 -1
  46. package/dist/assets/{railroadDiagram-XR7U4H2S-BSA-d8JU.js → railroadDiagram-XR7U4H2S-BEsiqR5s.js} +1 -1
  47. package/dist/assets/{requirementDiagram-PLB6GJNP-CSmlvb-p.js → requirementDiagram-PLB6GJNP-DWBcWvAP.js} +1 -1
  48. package/dist/assets/{sankeyDiagram-IPEJSGJF-CD5F6fnG.js → sankeyDiagram-IPEJSGJF-rpZ2_O4s.js} +1 -1
  49. package/dist/assets/{sequenceDiagram-PO4LG4MO-JUq5sx6d.js → sequenceDiagram-PO4LG4MO-B99HQdJR.js} +1 -1
  50. package/dist/assets/{sizeCapture-INFHLROL-BjIS-Stk.js → sizeCapture-INFHLROL-BjN0qT5T.js} +1 -1
  51. package/dist/assets/{stateDiagram-v2-GCMORJYK-DklkUxOg.js → stateDiagram-v2-GCMORJYK-BbsVgGsC.js} +1 -1
  52. package/dist/assets/{swimlanes-2SLR337P-CsjIU6be.js → swimlanes-2SLR337P-DG3gKkeR.js} +1 -1
  53. package/dist/assets/swimlanesDiagram-TC7HE7FX-BCZhxl7e.js +8 -0
  54. package/dist/assets/{timeline-definition-EJHVYXUP-BkFqK7AB.js → timeline-definition-EJHVYXUP-BTHOdtF_.js} +1 -1
  55. package/dist/assets/{usecaseDiagram-POWQR4AR-70gJB1EU.js → usecaseDiagram-POWQR4AR-acLSLFwc.js} +1 -1
  56. package/dist/assets/{vennDiagram-UO4OBE2U-83Tm62dN.js → vennDiagram-UO4OBE2U-C5JzNX9G.js} +1 -1
  57. package/dist/assets/{wardleyDiagram-VNRHLVJA-tTmseoX9.js → wardleyDiagram-VNRHLVJA-DL1-PrR_.js} +1 -1
  58. package/dist/assets/{xychartDiagram-PMCCYNJV-R1LBA55a.js → xychartDiagram-PMCCYNJV-Bvr1477p.js} +1 -1
  59. package/dist/index.html +2 -2
  60. package/dist-server/cloud-home-start.js +14810 -0
  61. package/dist-server/container-mcp.js +136 -36
  62. package/dist-server/drivers/agents-proxy.js +31 -10
  63. package/dist-server/index.js +48251 -44124
  64. package/dist-server/openmausbot.js +628 -292
  65. package/dist-server/pair-cli.js +628 -292
  66. package/dist-server/permission-proxy.js +1 -1
  67. package/dist-server/server/admission.js +46 -0
  68. package/dist-server/server/{box-create-idempotency.js → boat-create-idempotency.js} +32 -31
  69. package/dist-server/server/{box-delete-journal.js → boat-delete-journal.js} +26 -25
  70. package/dist-server/server/{box.js → boat.js} +381 -334
  71. package/dist-server/server/bot-overview.js +7 -4
  72. package/dist-server/server/bot-profile.js +7 -1
  73. package/dist-server/server/channel-queue.js +58 -18
  74. package/dist-server/server/checkpoints.js +98 -52
  75. package/dist-server/server/claim-idle.js +33 -0
  76. package/dist-server/server/cloud-backend.js +5 -5
  77. package/dist-server/server/cloud-home-start.js +121 -0
  78. package/dist-server/server/cloud-home.js +241 -0
  79. package/dist-server/server/cloud-lending.js +103 -0
  80. package/dist-server/server/cloud-move-http.js +268 -0
  81. package/dist-server/server/cloud-move.js +418 -0
  82. package/dist-server/server/cloud-overflow.js +167 -0
  83. package/dist-server/server/composio.js +5 -0
  84. package/dist-server/server/computer-observation.js +2 -2
  85. package/dist-server/server/config.js +156 -9
  86. package/dist-server/server/container-computer.js +13 -0
  87. package/dist-server/server/control-client.js +2 -2
  88. package/dist-server/server/decider/index.js +238 -0
  89. package/dist-server/server/decider/jev.js +183 -0
  90. package/dist-server/server/decider/log.js +62 -0
  91. package/dist-server/server/decider/relay.js +52 -0
  92. package/dist-server/server/decider/room-routing.js +88 -0
  93. package/dist-server/server/decider/types.js +5 -0
  94. package/dist-server/server/delegations.js +11 -3
  95. package/dist-server/server/drivers/acp/core.js +5 -4
  96. package/dist-server/server/drivers/agents-call.js +5 -1
  97. package/dist-server/server/drivers/agents-catalog.js +25 -3
  98. package/dist-server/server/drivers/{boxagent.js → boatagent.js} +30 -21
  99. package/dist-server/server/drivers/builtIn.js +2 -2
  100. package/dist-server/server/drivers/{chat-box-tools.js → chat-boat-tools.js} +3 -3
  101. package/dist-server/server/drivers/chat-mcp-tools.js +2 -2
  102. package/dist-server/server/drivers/claude.js +178 -13
  103. package/dist-server/server/drivers/native.js +1 -1
  104. package/dist-server/server/drivers/openai-chat.js +15 -4
  105. package/dist-server/server/drivers/pi.js +3 -3
  106. package/dist-server/server/group-goal-run.js +6 -4
  107. package/dist-server/server/harness-capabilities.js +2 -0
  108. package/dist-server/server/hosted-models.js +6 -1
  109. package/dist-server/server/included-services.js +70 -0
  110. package/dist-server/server/index.js +1462 -576
  111. package/dist-server/server/local-vm-inventory.js +11 -1
  112. package/dist-server/server/local-vm-seat-pool.js +141 -0
  113. package/dist-server/server/managed-policy.js +4 -1
  114. package/dist-server/server/memory-capture.js +194 -0
  115. package/dist-server/server/memory-entries.js +76 -0
  116. package/dist-server/server/memory-organize.js +102 -0
  117. package/dist-server/server/memory-tidy.js +144 -0
  118. package/dist-server/server/memory-topics.js +169 -0
  119. package/dist-server/server/memory-upkeep.js +452 -0
  120. package/dist-server/server/message-db.js +49 -5
  121. package/dist-server/server/model-context-window.js +4 -4
  122. package/dist-server/server/package-export.js +17 -10
  123. package/dist-server/server/package-import.js +6 -3
  124. package/dist-server/server/package-parts.js +3 -1
  125. package/dist-server/server/permission-proxy.js +3 -3
  126. package/dist-server/server/profile-learned.js +98 -0
  127. package/dist-server/server/profile-versions.js +45 -11
  128. package/dist-server/server/recall.js +172 -0
  129. package/dist-server/server/redact.js +1 -1
  130. package/dist-server/server/request-auth.js +2 -0
  131. package/dist-server/server/room-cwd.js +1 -1
  132. package/dist-server/server/room-handoffs.js +61 -6
  133. package/dist-server/server/routes/bot-memory.js +215 -0
  134. package/dist-server/server/routes/decider.js +19 -0
  135. package/dist-server/server/routine-requests.js +27 -16
  136. package/dist-server/server/routines.js +10 -6
  137. package/dist-server/server/shared-computers.js +116 -30
  138. package/dist-server/server/steer-queue.js +41 -10
  139. package/dist-server/server/store.js +42 -3
  140. package/dist-server/server/surface.js +14 -1
  141. package/dist-server/server/system-prompt.js +16 -5
  142. package/dist-server/server/team-backup.js +6 -2
  143. package/dist-server/server/team-computers.js +2 -2
  144. package/dist-server/server/team-manifest.js +11 -3
  145. package/dist-server/server/tightening-requests.js +34 -4
  146. package/dist-server/server/tool-summary.js +2 -2
  147. package/dist-server/server/tts/elevenlabs.js +12 -7
  148. package/dist-server/server/tts/index.js +17 -10
  149. package/dist-server/server/turn-resources.js +87 -11
  150. package/dist-server/server/vps-computer.js +1 -1
  151. package/dist-server/server/workspace-backup-policy.js +22 -3
  152. package/dist-server/server/workspace-backup.js +25 -8
  153. package/dist-server/server/workspace.js +57 -22
  154. package/dist-server/shared/ask-question.js +7 -5
  155. package/dist-server/shared/bot-avatar.js +19 -0
  156. package/dist-server/shared/cloud-home.js +4 -0
  157. package/dist-server/shared/credential-request.js +3 -3
  158. package/dist-server/shared/package-format.js +30 -4
  159. package/dist-server/shared/team-backup.js +20 -3
  160. package/dist-server/shared/tool-surface.js +1 -1
  161. package/dist-server/src/lib/composer-attachments.js +9 -0
  162. package/dist-server/vps-container-mcp.js +136 -36
  163. package/dist-server/workspace-backup.worker.js +165 -35
  164. package/package.json +1 -1
  165. package/dist/assets/channel-DqY_gMLg.js +0 -1
  166. package/dist/assets/index-BIMdIbaw.css +0 -1
  167. package/dist/assets/index-BVPdbq2r.js +0 -583
  168. package/dist/assets/swimlanesDiagram-TC7HE7FX-Bk14M0_B.js +0 -8
@@ -0,0 +1,183 @@
1
+ export const JEV_DEFAULT_BASE_URL = "https://api.typesafe.ai";
2
+ export const JEV_MODEL = "jev-latest";
3
+ export const JEV_MAX_OPTIONS = 255;
4
+ export const JEV_MIN_LEVELS = 2;
5
+ 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
+ /** The endpoint for a base URL, or null when the key must not be sent there:
11
+ * https anywhere, plain http only to this machine (a local Jev-compatible
12
+ * server or a test double). */
13
+ export function jevEndpoint(baseUrl) {
14
+ const root = (baseUrl?.trim() || JEV_DEFAULT_BASE_URL).replace(/\/+$/, "");
15
+ let url;
16
+ try {
17
+ url = new URL(`${root}/v1/systemone`);
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ if (url.username || url.password)
23
+ return null;
24
+ if (url.protocol === "https:")
25
+ return url;
26
+ return url.protocol === "http:" && isLoopback(url.hostname) ? url : null;
27
+ }
28
+ /** Questions within the API's limits: 2–255 options, 2–10 levels. */
29
+ export function questionsFit(questions) {
30
+ const entries = Object.entries(questions);
31
+ if (!entries.length)
32
+ return false;
33
+ return entries.every(([, question]) => {
34
+ if (question.type === "choice") {
35
+ const count = Object.keys(question.options).length;
36
+ return count >= 2 && count <= JEV_MAX_OPTIONS;
37
+ }
38
+ if (question.type === "score")
39
+ return question.levels.length >= JEV_MIN_LEVELS && question.levels.length <= JEV_MAX_LEVELS;
40
+ return true;
41
+ });
42
+ }
43
+ export function jevRequestBody(state, questions) {
44
+ const wire = {};
45
+ for (const [id, question] of Object.entries(questions)) {
46
+ if (question.type === "choice") {
47
+ wire[id] = { type: "choice", instructions: question.instructions, criteria: { ...question.options } };
48
+ }
49
+ else if (question.type === "score") {
50
+ wire[id] = { type: "score", instructions: question.instructions, criteria: [...question.levels] };
51
+ }
52
+ else {
53
+ const criteria = question.criteria?.yes || question.criteria?.no
54
+ ? { ...(question.criteria.yes ? { true: question.criteria.yes } : {}), ...(question.criteria.no ? { false: question.criteria.no } : {}) }
55
+ : undefined;
56
+ wire[id] = { type: "noul", instructions: question.instructions, ...(criteria ? { criteria } : {}) };
57
+ }
58
+ }
59
+ return { state, model: JEV_MODEL, questions: wire };
60
+ }
61
+ const isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
62
+ const isProbability = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
63
+ /** Jev rounds to two places, so a map can sum to 0.98 or 1.02. */
64
+ const SUM_TOLERANCE = 0.1;
65
+ const EPSILON = 1e-9;
66
+ function parseChoice(raw, question) {
67
+ const offered = new Set(Object.keys(question.options));
68
+ const choice = raw.choice;
69
+ if (typeof choice !== "string" || !offered.has(choice))
70
+ return null;
71
+ if (!isRecord(raw.probabilities))
72
+ return null;
73
+ const probabilities = {};
74
+ let sum = 0;
75
+ for (const [key, value] of Object.entries(raw.probabilities)) {
76
+ if (!offered.has(key) || !isProbability(value))
77
+ return null;
78
+ probabilities[key] = value;
79
+ sum += value;
80
+ }
81
+ if (!(Math.abs(sum - 1) <= SUM_TOLERANCE))
82
+ return null;
83
+ const pTop = probabilities[choice];
84
+ if (pTop === undefined)
85
+ return null;
86
+ const others = Object.entries(probabilities).filter(([key]) => key !== choice).map(([, value]) => value);
87
+ const runnerUp = others.length ? Math.max(...others) : 0;
88
+ // Ties break to the first key on the vendor side; a choice that is not
89
+ // the most likely option means the answer is not what it claims to be.
90
+ if (runnerUp > pTop + EPSILON)
91
+ return null;
92
+ return { type: "choice", choice, pTop, margin: pTop - runnerUp, probabilities };
93
+ }
94
+ function parseScore(raw, question) {
95
+ const levels = question.levels.length;
96
+ const score = raw.score;
97
+ if (typeof score !== "number" || !Number.isFinite(score) || score < -EPSILON || score > levels - 1 + EPSILON)
98
+ return null;
99
+ if (!isRecord(raw.probabilities))
100
+ return null;
101
+ const probabilities = Array.from({ length: levels }, () => 0);
102
+ for (const [key, value] of Object.entries(raw.probabilities)) {
103
+ const index = Number(key);
104
+ if (!Number.isInteger(index) || index < 0 || index >= levels || String(index) !== key || !isProbability(value))
105
+ return null;
106
+ probabilities[index] = value;
107
+ }
108
+ const sum = probabilities.reduce((total, value) => total + value, 0);
109
+ if (!(Math.abs(sum - 1) <= SUM_TOLERANCE))
110
+ return null;
111
+ const level = probabilities.indexOf(Math.max(...probabilities));
112
+ return { type: "score", score, level, probabilities };
113
+ }
114
+ function parseYesNo(raw) {
115
+ return isProbability(raw.noul) ? { type: "yesno", p: raw.noul } : null;
116
+ }
117
+ const WIRE_TYPE = { choice: "choice", score: "score", yesno: "noul" };
118
+ /** Strictly decode one response body against the questions that were asked. */
119
+ export function parseJevResponse(body, questions) {
120
+ if (!isRecord(body) || !isRecord(body.answers))
121
+ return { ok: false };
122
+ const answers = {};
123
+ for (const [id, question] of Object.entries(questions)) {
124
+ const raw = body.answers[id];
125
+ if (!isRecord(raw))
126
+ return { ok: false };
127
+ if (raw.type !== undefined && raw.type !== WIRE_TYPE[question.type])
128
+ return { ok: false };
129
+ const parsed = question.type === "choice" ? parseChoice(raw, question)
130
+ : question.type === "score" ? parseScore(raw, question)
131
+ : parseYesNo(raw);
132
+ if (!parsed)
133
+ return { ok: false };
134
+ answers[id] = parsed;
135
+ }
136
+ const usage = isRecord(body.usage) ? body.usage.input_tokens : undefined;
137
+ const inputTokens = typeof usage === "number" && Number.isFinite(usage) && usage >= 0 ? usage : undefined;
138
+ const model = typeof body.model === "string" && body.model.length <= 80 ? body.model : undefined;
139
+ return { ok: true, answers, ...(inputTokens !== undefined ? { inputTokens } : {}), ...(model ? { model } : {}) };
140
+ }
141
+ function refusal(status) {
142
+ if (status === 401 || status === 403)
143
+ return { ok: false, reason: "rejected", status };
144
+ if (status === 429)
145
+ return { ok: false, reason: "rate_limited", status };
146
+ if (status === 529 || status === 503)
147
+ return { ok: false, reason: "overloaded", status };
148
+ return { ok: false, reason: "http_error", status };
149
+ }
150
+ async function decide(request) {
151
+ const endpoint = jevEndpoint(request.baseUrl);
152
+ if (!endpoint || !questionsFit(request.questions))
153
+ return { ok: false, reason: "misconfigured" };
154
+ let response;
155
+ try {
156
+ response = await request.fetch(endpoint, {
157
+ method: "POST",
158
+ headers: { authorization: `Bearer ${request.key}`, "content-type": "application/json", accept: "application/json" },
159
+ body: JSON.stringify(jevRequestBody(request.state, request.questions)),
160
+ signal: request.signal,
161
+ // Never replay the key to wherever a redirect points.
162
+ redirect: "error",
163
+ });
164
+ }
165
+ catch {
166
+ // The caller tells a timeout or a Stop from a dead network by its own signals.
167
+ return { ok: false, reason: "unreachable" };
168
+ }
169
+ if (!response.ok) {
170
+ await response.body?.cancel().catch(() => undefined);
171
+ return refusal(response.status);
172
+ }
173
+ let body;
174
+ try {
175
+ body = await response.json();
176
+ }
177
+ catch {
178
+ return { ok: false, reason: "malformed" };
179
+ }
180
+ const parsed = parseJevResponse(body, request.questions);
181
+ return parsed.ok ? parsed : { ok: false, reason: "malformed" };
182
+ }
183
+ export const jevBackend = { id: "jev", decide };
@@ -0,0 +1,62 @@
1
+ // The decision model's own log: one row per call that reached a backend,
2
+ // one file per UTC month under <data>/decider-log/YYYY-MM.ndjson.
3
+ //
4
+ // Not the authorization decision log (server/decision-log.ts, <data>/
5
+ // decisions/), which records approvals. This one records what a fast
6
+ // classifier picked and how sure it was, so its accuracy and cost can be
7
+ // measured later. A row never holds the message text, the options' text or
8
+ // the key: only the seam, the chosen option key (a bot id for room routing),
9
+ // the probabilities and a short hash of the state.
10
+ //
11
+ // Same discipline as the other logs: 0600, fire-and-forget, serialized per
12
+ // directory, and months older than the shared retention window pruned.
13
+ import { createHash } from "node:crypto";
14
+ import { appendFile, mkdir } from "node:fs/promises";
15
+ import { join } from "node:path";
16
+ import { boundRetentionDays, pruneMonthFiles } from "../decision-log.js";
17
+ export const DECIDER_LOG_DIR = "decider-log";
18
+ const PRUNE_EVERY_MS = 60 * 60_000;
19
+ const writeQueues = new Map();
20
+ const lastPrune = new Map();
21
+ export function stateHash(state) {
22
+ let text;
23
+ try {
24
+ text = JSON.stringify(state) ?? "";
25
+ }
26
+ catch {
27
+ text = "";
28
+ }
29
+ return createHash("sha256").update(text).digest("hex").slice(0, 16);
30
+ }
31
+ function monthKey(at) {
32
+ return `${at.getUTCFullYear()}-${String(at.getUTCMonth() + 1).padStart(2, "0")}`;
33
+ }
34
+ export function deciderLogFileFor(dataDir, at) {
35
+ return join(dataDir, DECIDER_LOG_DIR, `${monthKey(at)}.ndjson`);
36
+ }
37
+ async function write(dataDir, row) {
38
+ const at = new Date(row.at);
39
+ await mkdir(join(dataDir, DECIDER_LOG_DIR), { recursive: true, mode: 0o700 });
40
+ await appendFile(deciderLogFileFor(dataDir, at), JSON.stringify(row) + "\n", { mode: 0o600 });
41
+ if (at.getTime() - (lastPrune.get(dataDir) ?? 0) >= PRUNE_EVERY_MS) {
42
+ lastPrune.set(dataDir, at.getTime());
43
+ await pruneMonthFiles(join(dataDir, DECIDER_LOG_DIR), boundRetentionDays(), at);
44
+ }
45
+ }
46
+ /** Append one row. Never throws and never waits: a full disk must not turn
47
+ * into a slower or failed room turn. */
48
+ export function appendDeciderLog(dataDir, row) {
49
+ const previous = writeQueues.get(dataDir) ?? Promise.resolve();
50
+ const queued = previous.then(() => write(dataDir, row)).catch(() => {
51
+ /* logging must never take down a decision */
52
+ });
53
+ writeQueues.set(dataDir, queued);
54
+ void queued.finally(() => {
55
+ if (writeQueues.get(dataDir) === queued)
56
+ writeQueues.delete(dataDir);
57
+ });
58
+ }
59
+ /** Test seam: wait until every queued row for this directory is on disk. */
60
+ export async function flushDeciderLog(dataDir) {
61
+ await writeQueues.get(dataDir);
62
+ }
@@ -0,0 +1,52 @@
1
+ // What Cloud Pro's decision relay accepts through the included token
2
+ // (docs/cloud-pro.md, "Included Boat computers, voice and decisions"; the
3
+ // Admin checks the same): room routing's one request and the Settings key
4
+ // check, within its size caps. Anything else (another job, another question,
5
+ // a larger state) is never sent through the included token: it goes only
6
+ // with the person's own key, until the relay accepts it too.
7
+ import { jevRequestBody } from "./jev.js";
8
+ import { ROOM_ROUTING_INSTRUCTIONS, ROOM_ROUTING_STATE_KEYS } from "./room-routing.js";
9
+ /** The Settings key check's fixed request. */
10
+ export const KEY_CHECK_STATE = { purpose: "OpenMausBot is checking that a decision-model key works." };
11
+ export const KEY_CHECK_QUESTION = "Is this a connection check?";
12
+ /** The relay's caps: the whole request body, and the state as JSON. Both
13
+ * measured in UTF-8 bytes, which is never less than characters. */
14
+ export const RELAY_MAX_BODY_BYTES = 64 * 1024;
15
+ export const RELAY_MAX_STATE_BYTES = 24_000;
16
+ /** Where a request may use the included token. A job added later is not here
17
+ * until the relay accepts it. */
18
+ const RELAY_SEAMS = new Set(["roomRouting", "keyCheck"]);
19
+ const ROOM_STATE_KEYS = new Set(ROOM_ROUTING_STATE_KEYS);
20
+ const ROOM_STATE_REQUIRED = ROOM_ROUTING_STATE_KEYS.filter((key) => key !== "recent_messages");
21
+ export function relaySeam(seam) {
22
+ return RELAY_SEAMS.has(seam);
23
+ }
24
+ const isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
25
+ function shapeFits(seam, state, questions) {
26
+ const ids = Object.keys(questions);
27
+ const question = questions.answer;
28
+ if (ids.length !== 1 || !question)
29
+ return false;
30
+ if (seam === "keyCheck") {
31
+ return question.type === "yesno" && question.instructions === KEY_CHECK_QUESTION && !question.criteria &&
32
+ JSON.stringify(state) === JSON.stringify(KEY_CHECK_STATE);
33
+ }
34
+ if (seam === "roomRouting") {
35
+ return question.type === "choice" && question.instructions === ROOM_ROUTING_INSTRUCTIONS && isRecord(state) &&
36
+ Object.keys(state).every((key) => ROOM_STATE_KEYS.has(key)) && ROOM_STATE_REQUIRED.every((key) => key in state);
37
+ }
38
+ // Any other seam: the relay takes nothing from it.
39
+ return false;
40
+ }
41
+ /** Whether this exact request may go through the included token. */
42
+ export function relayAccepts(seam, state, questions) {
43
+ try {
44
+ if (!shapeFits(seam, state, questions))
45
+ return false;
46
+ return Buffer.byteLength(JSON.stringify(state)) <= RELAY_MAX_STATE_BYTES &&
47
+ Buffer.byteLength(JSON.stringify(jevRequestBody(state, questions))) <= RELAY_MAX_BODY_BYTES;
48
+ }
49
+ catch {
50
+ return false;
51
+ }
52
+ }
@@ -0,0 +1,88 @@
1
+ /** Turn starts take seconds anyway, and people far from the US West Coast
2
+ * see 400–700 ms round trips; 1.5 s keeps a slow answer usable. */
3
+ export const ROOM_ROUTING_TIMEOUT_MS = 1_500;
4
+ /** Bench calibration: answers at >= 0.6 were right 92–99% of the time. */
5
+ export const ROOM_ROUTING_MIN_PROBABILITY = 0.6;
6
+ export const EVERYONE_OPTION = "__everyone__";
7
+ const EVERYONE_MEANING = "Several members: the message explicitly needs answers or work from more than one member of the room, for example it asks everyone or asks each member for their part.";
8
+ /** Fixed: Cloud Pro's decision relay accepts room routing only with these
9
+ * exact instructions (relay.ts). */
10
+ export const ROOM_ROUTING_INSTRUCTIONS = "Which bot in this room should answer `new_message`? Choose __everyone__ only when the message needs several members to answer.";
11
+ /** Every key the state may carry; `recent_messages` is left out when there
12
+ * are none. Cloud Pro's relay accepts no other. */
13
+ export const ROOM_ROUTING_STATE_KEYS = ["room", "humans_in_room", "bots_in_room", "recent_messages", "new_message"];
14
+ const NAME_MAX = 80;
15
+ const TITLE_MAX = 120;
16
+ const DESCRIPTION_MAX = 400;
17
+ const LINE_MAX = 500;
18
+ /** The room context window bounds how many lines; this bounds their size,
19
+ * because large irrelevant state makes the classifier worse, not better. */
20
+ const RECENT_BUDGET_CHARS = 6_000;
21
+ function clip(value, max) {
22
+ const flat = value.replace(/\s+/g, " ").trim();
23
+ return flat.length > max ? `${flat.slice(0, max - 1)}…` : flat;
24
+ }
25
+ /** What one bot is, as an option: "Maya, Product Designer. Owns UI…". */
26
+ export function memberOption(member) {
27
+ const name = clip(member.name, NAME_MAX) || "Unnamed bot";
28
+ const title = member.title ? clip(member.title, TITLE_MAX) : "";
29
+ const description = member.description ? clip(member.description, DESCRIPTION_MAX) : "";
30
+ return `${name}${title ? `, ${title}` : ""} bot.${description ? ` ${description}` : ""}`;
31
+ }
32
+ /** The newest lines that fit the budget, oldest first, each clipped. */
33
+ function recentWithinBudget(recent) {
34
+ const kept = [];
35
+ let used = 0;
36
+ for (let index = recent.length - 1; index >= 0; index--) {
37
+ const line = recent[index];
38
+ const text = clip(line.text, LINE_MAX);
39
+ if (!text)
40
+ continue;
41
+ const size = text.length + line.from.length;
42
+ if (used + size > RECENT_BUDGET_CHARS)
43
+ break;
44
+ used += size;
45
+ kept.unshift({ from: clip(line.from, NAME_MAX), text });
46
+ }
47
+ return kept;
48
+ }
49
+ export function roomRoutingRequest(input) {
50
+ const options = {};
51
+ for (const member of input.members)
52
+ options[member.id] = memberOption(member);
53
+ options[EVERYONE_OPTION] = EVERYONE_MEANING;
54
+ const recent = recentWithinBudget(input.recent);
55
+ const state = {
56
+ room: clip(input.room, NAME_MAX),
57
+ humans_in_room: input.humans.map((human) => clip(human, NAME_MAX)),
58
+ bots_in_room: input.members.map((member) => clip(member.name, NAME_MAX)),
59
+ ...(recent.length ? { recent_messages: recent } : {}),
60
+ new_message: { from: clip(input.message.from, NAME_MAX), text: input.message.text.trim().slice(0, 8_000) },
61
+ };
62
+ return { state, question: { instructions: ROOM_ROUTING_INSTRUCTIONS, options } };
63
+ }
64
+ /** Ask once and turn the answer into a route. Never throws. */
65
+ export async function decideRoomResponder(decider, input, options = {}) {
66
+ try {
67
+ if (input.members.length < 2)
68
+ return { kind: "fallback", reason: "no_choice" };
69
+ const { state, question } = roomRoutingRequest(input);
70
+ const result = await decider.choose("roomRouting", state, question, {
71
+ timeoutMs: options.timeoutMs ?? ROOM_ROUTING_TIMEOUT_MS,
72
+ signal: options.signal,
73
+ });
74
+ if (!result.ok)
75
+ return { kind: "fallback", reason: result.reason };
76
+ const { choice, pTop } = result.answers;
77
+ if (pTop < ROOM_ROUTING_MIN_PROBABILITY)
78
+ return { kind: "fallback", reason: "low_confidence" };
79
+ if (choice === EVERYONE_OPTION)
80
+ return { kind: "everyone", probability: pTop };
81
+ if (!input.members.some((member) => member.id === choice))
82
+ return { kind: "fallback", reason: "malformed" };
83
+ return { kind: "member", botId: choice, probability: pTop };
84
+ }
85
+ catch {
86
+ return { kind: "fallback", reason: "malformed" };
87
+ }
88
+ }
@@ -0,0 +1,5 @@
1
+ // The decision model's vocabulary, shared by every backend and caller. A
2
+ // question is one of three typed shapes; an answer comes back typed to match.
3
+ // Nothing here names a vendor: `jev` is one backend, and another (a local
4
+ // Jev-compatible server, an LLM) plugs in behind the same types.
5
+ export const DECIDER_JOBS = ["roomRouting"];
@@ -259,8 +259,10 @@ export function pendingDelegationSnapshot() {
259
259
  })));
260
260
  }
261
261
  /** How many handoffs one turn may queue. Small on purpose: this is the only
262
- * thing standing between a confused bot and a fan-out of real turns. */
263
- const MAX_QUEUED_PER_THREAD = 4;
262
+ * thing standing between a confused bot and a fan-out of real turns. Six,
263
+ * not four: a lead running a weekly check-in over a five-member team is the
264
+ * ordinary case, and hitting the cap there silently dropped one teammate. */
265
+ const MAX_QUEUED_PER_THREAD = 6;
264
266
  /** Validate and enqueue a delegation. Pushes a "Delegated to @B: reason"
265
267
  * chip to the source thread so the user can see what was queued. */
266
268
  export function queueDelegation(bus, from, item, maxDepth, sourceThreadId = from.threadId) {
@@ -838,7 +840,13 @@ export function buildDelegationFailurePrompt(targetName, reason) {
838
840
  "Take over: tell the user what failed in plain terms, then decide the next step — retry with a narrower task, do the work yourself, or propose an alternative. Do not re-delegate the exact same task unchanged.",
839
841
  ].join("\n\n");
840
842
  }
841
- export const DELEGATION_WAKE_MAX_PER_WINDOW = 3;
843
+ // Must not be smaller than MAX_QUEUED_PER_THREAD: every delegate reply is
844
+ // one wake, so a fan-out that the queue cap allows has to be resumable
845
+ // within the window. Three failed a five-member weekly check-in whose
846
+ // replies all landed inside four minutes ("Delegation follow-up limit
847
+ // reached"), while the same routine passed a week earlier only because the
848
+ // replies happened to spread over twenty.
849
+ export const DELEGATION_WAKE_MAX_PER_WINDOW = 6;
842
850
  export const DELEGATION_WAKE_WINDOW_MS = 5 * 60 * 1000;
843
851
  /** Bounded auto-wake budget per source thread. A delegation completion
844
852
  * wakes the source; if that source re-delegates and the new completion
@@ -43,7 +43,7 @@ export function skipSubscriptionAuthForLocalInject(model) {
43
43
  import { newEventId, newId, TurnNotStartedError } from "../../contracts.js";
44
44
  import { augmentedPath } from "../../env-path.js";
45
45
  import { supportsApprovalMode } from "../../../shared/approval-mode.js";
46
- import { parseAskQuestions, parseChoices, questionAnswersByQuestion } from "../../../shared/ask-question.js";
46
+ import { MAX_QUESTION_TEXT, parseAskQuestions, parseChoices, questionAnswersByQuestion } from "../../../shared/ask-question.js";
47
47
  import { appendNative } from "../native.js";
48
48
  import { acpPermissionCommand, permissionLaunchCwd } from "../permission-command.js";
49
49
  import { commandSummary, toolDetailPreview } from "../../tool-summary.js";
@@ -275,7 +275,7 @@ export function createAcpDriver(support) {
275
275
  };
276
276
  const allowedCredentials = new Set(support.credentialEnv ?? []);
277
277
  // two lists, one rule: foreign PROVIDER keys must not flip a CLI's
278
- // billing off its own login, and WORKSPACE credentials (box token,
278
+ // billing off its own login, and WORKSPACE credentials (boat token,
279
279
  // voice key, …) are the harness's secrets — riding along in
280
280
  // `...process.env` is not a grant. A driver keeps only what its
281
281
  // credentialEnv allowlist names.
@@ -439,7 +439,7 @@ export function createAcpDriver(support) {
439
439
  }
440
440
  // The bot's computer, mounted exactly like the Claude driver does:
441
441
  // host and sandbox Cua connections expose Cua Driver's own MCP server.
442
- // (A cloud box is not mounted here at all: a cloud turn runs ON the box.)
442
+ // (A cloud boat is not mounted here at all: a cloud turn runs ON the boat.)
443
443
  if (turn.integrations?.localComputer) {
444
444
  const local = turn.integrations.localComputer;
445
445
  servers.push({
@@ -722,7 +722,8 @@ export function createAcpDriver(support) {
722
722
  }
723
723
  const tool = kind === "execute" ? "shell" : kind === "edit" ? "edit" : kind || "tool";
724
724
  const isShellCommand = !isQuestion && kind === "execute" && !/^mcp(?:__|[.:])/i.test(String(toolCall.title ?? ""));
725
- const summary = String(toolCall.rawInput?.command ?? toolCall.title ?? tool).slice(0, 200);
725
+ const rawSummary = String(toolCall.rawInput?.command ?? toolCall.title ?? tool);
726
+ const summary = rawSummary.slice(0, isQuestion ? MAX_QUESTION_TEXT : 200);
726
727
  // One structured question beside the flat choices: the richer card
727
728
  // renders from it while older clients keep answering through
728
729
  // `choices`. Built once here so the emit and the answer path can
@@ -278,7 +278,7 @@ function routineFields(args) {
278
278
  const runOn = destination(args.run_on ?? args.runOn);
279
279
  const timeoutMinutes = args.timeout_minutes ?? args.timeoutMinutes;
280
280
  if (runOn != null && runOn !== "maus" && runOn !== "cloud") {
281
- return { fields, error: 'Use run_on="maus" for the bot’s current model and configured computer (including VPS), or run_on="box" only for the Box-hosted agent. Legacy "cloud" also means Box.' };
281
+ return { fields, error: 'Use run_on="maus" for the bot’s current model and configured computer (including VPS), or run_on="box" only for the Boat-hosted agent. Legacy "cloud" also means Boat.' };
282
282
  }
283
283
  if (timeoutMinutes != null && (typeof timeoutMinutes !== "number" || !Number.isInteger(timeoutMinutes) || timeoutMinutes < 5 || timeoutMinutes > 240)) {
284
284
  return { fields, error: "timeout_minutes must be a whole number from 5 to 240. Use clear_timeout to remove a limit." };
@@ -982,11 +982,14 @@ export async function callTool(name, args, context) {
982
982
  if (!routineId || !action) {
983
983
  return { text: "propose_routine_action needs a routine_id and supported action.", isError: true };
984
984
  }
985
+ const forBotId = String(args.for_bot_id ?? "").trim();
985
986
  const body = {
986
987
  fromBotId: BOT_ID,
987
988
  fromThreadId: THREAD_ID,
988
989
  action,
989
990
  routineId,
991
+ // JSON.stringify drops the key entirely when no target was named
992
+ ...(forBotId ? { forBotId } : {}),
990
993
  };
991
994
  if (action === "update") {
992
995
  if (!jsonRecord(args.changes)) {
@@ -1098,6 +1101,7 @@ export async function callTool(name, args, context) {
1098
1101
  action: args.action,
1099
1102
  text: args.text,
1100
1103
  oldText: args.old_text,
1104
+ ...(typeof args.until === "string" && args.until.trim() ? { until: args.until.trim() } : {}),
1101
1105
  }),
1102
1106
  });
1103
1107
  if (r.error || r.ok !== true) {
@@ -24,6 +24,7 @@ export function catalogProfileFromEnv(env) {
24
24
  skillAuthoring: env.OMB_SKILL_AUTHORING_ENABLED === "1",
25
25
  sharedComputers: env.OMB_SHARED_COMPUTERS_ENABLED === "1",
26
26
  voiceNotes: env.OMB_VOICE_NOTES === "1",
27
+ cloudHome: env.OMB_CLOUD_HOME === "1",
27
28
  botId: env.OMB_BOT_ID ?? "",
28
29
  };
29
30
  }
@@ -128,8 +129,9 @@ const ROUTINE_FIELDS_SCHEMA = {
128
129
  schedule: ROUTINE_SCHEDULE_SCHEMA,
129
130
  run_on: {
130
131
  type: "string",
132
+ // "box" is Boat's historical run_on destination id (agents wire contract).
131
133
  enum: ["maus", "box"],
132
- description: "Default maus keeps the bot's selected model and configured computer, INCLUDING a self-hosted VPS. Omit this field for normal schedules. box explicitly switches the agent to the Box-hosted runner; it requires Box setup and is not the generic cloud/VPS option. Legacy cloud values from list_routines mean box, not VPS.",
134
+ description: "Default maus keeps the bot's selected model and configured computer, INCLUDING a self-hosted VPS. Omit this field for normal schedules. box explicitly switches the agent to the Boat-hosted runner; it requires Boat setup and is not the generic cloud/VPS option. Legacy cloud values from list_routines mean box, not VPS.",
133
135
  },
134
136
  timeout_minutes: {
135
137
  type: "integer",
@@ -293,7 +295,7 @@ const toolDefinitions = (externalRuntime) => [
293
295
  description: "Choose where this conversation does computer work. Call with no arguments to inspect actual available choices and the current place. For a task needing computer interaction, select the requested place, or auto to choose a suitable configured computer without asking the user to use menus. OpenMausBot reuses an existing computer first; with a configured provider it can start or provision one when needed. Do not provision for ordinary chat or just to inspect availability. A pending result means end this turn immediately: OpenMausBot updates the conversation selector and resumes the original request with that computer's real tools. Do not use the old tools after requesting a switch, repeat the task, or claim the action is done. This cannot change permissions, override Off, or switch a teammate/routine/channel.",
294
296
  inputSchema: { type: "object", additionalProperties: false, properties: {
295
297
  surface: { type: "string", enum: ["auto", "cloud", "vm", "local", "browser"],
296
- description: "auto = suitable configured computer, cloud = remote Box/VPS, vm = isolated Local VM, local = user's own desktop, browser = built-in browser. Omit to list." },
298
+ description: "auto = suitable configured computer, cloud = remote Boat/VPS, vm = isolated Local VM, local = user's own desktop, browser = built-in browser. Omit to list." },
297
299
  } },
298
300
  },
299
301
  {
@@ -523,6 +525,7 @@ const toolDefinitions = (externalRuntime) => [
523
525
  action: { type: "string", enum: ["append", "replace", "remove", "supersede"] },
524
526
  text: { type: "string", minLength: 1, description: "Non-blank new text for append, replace, or supersede: the fact itself, without a date or bullet. Omit for remove; use remove to delete a passage." },
525
527
  old_text: { type: "string", minLength: 1, description: "Exact unique existing passage for replace, supersede, or remove. Omit for append." },
528
+ until: { type: "string", pattern: "^\\d{4}-\\d{2}-\\d{2}$", description: "Optional, for append or supersede: YYYY-MM-DD, the last day a temporary fact holds (an exam this weekend, a trip next week). After that day the entry is hidden from your memory." },
526
529
  },
527
530
  required: ["action"],
528
531
  },
@@ -614,12 +617,16 @@ const toolDefinitions = (externalRuntime) => [
614
617
  },
615
618
  {
616
619
  name: "propose_routine_action",
617
- description: "Prepare a user-requested change to one of this bot's existing routines. Use list_routines first to get the routine id." + PROPOSAL_OUTCOME,
620
+ description: "Prepare a user-requested change to one of this bot's existing routines. Use list_routines first to get the routine id. If the user asks to change ANOTHER bot's routine and that bot is in your section, call list_bots and pass that bot's id as for_bot_id; the routine keeps its owner and every run keeps that bot's engine and permissions." + PROPOSAL_OUTCOME,
618
621
  inputSchema: {
619
622
  type: "object",
620
623
  additionalProperties: false,
621
624
  properties: {
622
625
  routine_id: { type: "string", minLength: 1, description: "Routine id from list_routines." },
626
+ for_bot_id: {
627
+ type: "string",
628
+ description: "Only when the requested change targets ANOTHER bot's routine and that bot is in your section: that bot's id from list_bots. Learn the routine id from that bot's own routines (it can run list_routines). Omit to change one of your own routines.",
629
+ },
623
630
  action: {
624
631
  type: "string",
625
632
  enum: ["update", "pause", "resume", "run_now", "delete"],
@@ -753,8 +760,23 @@ const ROOM_ONLY_TOOLS = new Set(["list_room_targets", "coordinate_bots"]);
753
760
  const ROOM_REPLACED_TOOLS = new Set(["ask_bot", "delegate_bot", "check_delegation", "wait_delegation", "start_thread", "send_to_thread", "wait_thread"]);
754
761
  const EXTERNAL_TOOL_NAMES = new Set(["list_bots", "ask_bot", "delegate_bot", "check_delegation", "wait_delegation"]);
755
762
  const WATCHER_TOOL_NAMES = new Set(["create_options_card"]);
763
+ // A Cloud home never offers this computer or a Local VM, so its bots are not
764
+ // shown them as choices, nor a VM shell they could never have.
765
+ const LOCAL_VM_TOOL_NAMES = new Set(["vm_exec"]);
766
+ const CLOUD_HOME_SURFACE = {
767
+ type: "string", enum: ["auto", "cloud", "browser"],
768
+ description: "auto = suitable configured computer, cloud = remote Boat/VPS, browser = built-in browser. Omit to list. This server runs in the cloud: the user's own computer and a Local VM are not places here.",
769
+ };
756
770
  /** The tools one turn is shown, exactly as tools/list serializes them. */
757
771
  export function availableTools(profile) {
772
+ const tools = catalogTools(profile);
773
+ return profile.cloudHome
774
+ ? tools.filter(tool => !LOCAL_VM_TOOL_NAMES.has(tool.name)).map(tool => tool.name === "select_computer"
775
+ ? { ...tool, inputSchema: { ...tool.inputSchema, properties: { surface: CLOUD_HOME_SURFACE } } }
776
+ : tool)
777
+ : tools;
778
+ }
779
+ function catalogTools(profile) {
758
780
  const TOOLS = toolDefinitions(profile.externalRuntime);
759
781
  const BOT_SCOPED_TOOLS = profile.botId === WATCHER_OPTIONS_CARD_BOT_ID
760
782
  ? TOOLS