openmausbot 0.1.93 → 0.1.95

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 (192) hide show
  1. package/dist/assets/{DesktopViewer-CKNR3SEW.js → DesktopViewer-BwWpc4RK.js} +1 -1
  2. package/dist/assets/{abnfDiagram-O67JEVCF-Ckrm1I1X.js → abnfDiagram-O67JEVCF-jsQkIqTn.js} +1 -1
  3. package/dist/assets/{arc-BBdBuCgB.js → arc-7kei4Hsu.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-CDdA3Hjs.js → architectureDiagram-NJMV4G6O-CRWDFDMq.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-AUrsx1YR.js → blockDiagram-BEXU5L5S-CTKW_IKs.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-BEqQ0Unh.js → c4Diagram-YGBWAQC7-BBTW7-6M.js} +1 -1
  7. package/dist/assets/channel-DS2NbT5x.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-CEEuvd4C.js → chunk-2Q5K7J3B-M_Ox-MkD.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-DyANCiHl.js → chunk-3FUC2YCW-D2YiN-Ud.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-Dx_Slpw9.js → chunk-5VM5RSS4-BmVYJ0f8.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-Dl7h7iiD.js → chunk-GTNCS2PH-BtiLVVnZ.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-BLx07SPb.js → chunk-GWA4HPMP-DFYSb6J6.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-BMxDj3n7.js → chunk-JWPE2WC7-mknKBzgV.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-CN66PT4H.js → chunk-NETBCI7D-DgSYGiOu.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-6HwhsDKw.js → chunk-WEXAMYUT-C7ZvuguH.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-gWnLt7SN.js → chunk-XXDRQBXY-CPqxINpA.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-CMS5l2ea.js → classDiagram-v2-NBCMYWYE-Cd6xfJaC.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-CV7sQwIG.js → cose-bilkent-JH36ORCC-BSXY5qRh.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-CSyl-7vl.js → cynefin-OW5HDTMX-D_lhEMj1.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-BCLKL6az.js → cynefinDiagram-VND7K2PF-Da9FrFwk.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-DiaZs6S1.js → dagre-6A5THRUB-T77jMOXe.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-BeTtmt3I.js → diagram-22UHCM2B-Dkj96X5E.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-CsdBG-QQ.js → diagram-3UASUU5V-CMi9zlD_.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-BczvdbR1.js → diagram-ATOU4E4O-CXK6YUfS.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-DHTTTMhi.js → diagram-CDSNMT55-BE7B1u9Q.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-RTjHyd8z.js → diagram-MLGK6HIB-CCMQphKr.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-BDenHEH_.js → diagram-MPIPVDR6-iHJ_-tGH.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-BSiRSbtL.js → ebnfDiagram-ZINNZB2B-DONsCWQ7.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-BCAElss-.js → elk-276RUBZZ-CU_WOaXk.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-C4_1ZPJe.js → erDiagram-OPXOYQCR-BeYwM04G.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-Bcb4Z75S.js → flowDiagram-KWPJA3E3-Bdw9DqEl.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-BiPOzxzb.js → ganttDiagram-FUAMR5RP-C9iwq3qm.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-B1wtF3ZS.js → gitGraphDiagram-X574FWY7-Dx-VWzUg.js} +1 -1
  34. package/dist/assets/{index-BkYwilkG.js → index-KygHRE3E.js} +1 -1
  35. package/dist/assets/index-NF7XCHAy.css +1 -0
  36. package/dist/assets/index-X_5ydXIE.js +610 -0
  37. package/dist/assets/{infoDiagram-VRGFBTTK-A19No71U.js → infoDiagram-VRGFBTTK-BG7uaEd4.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-BQsqJ_cV.js → ishikawaDiagram-OU5B5YK6-D72WAXnM.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-DAO0pVan.js → journeyDiagram-ZHPQQLJL-DKH1gjYT.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-NepmKEe2.js → kanban-definition-PNTS6WVX-3upm9JUT.js} +1 -1
  41. package/dist/assets/{linear-bq1PTDTW.js → linear-CcR4pc0-.js} +1 -1
  42. package/dist/assets/{mermaid.core-s4LTbQle.js → mermaid.core-C6yU5FSS.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-LtdbgHFH.js → mindmap-definition-NLK3R4M7-DOI2xEuv.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-tdbaCxiK.js → pegDiagram-GJSIUBJH-BVWJSgLd.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-DvkbpW8l.js → pieDiagram-5QR66LMP-jtbYezhq.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-Do4KaH-E.js → quadrantDiagram-O4NWA36T-g0u8mtPX.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-BQWULOAs.js → railroadDiagram-XR7U4H2S-k8689Wny.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-w9kei1FU.js → requirementDiagram-PLB6GJNP-DsJ22PDt.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-D2gRy0e8.js → sankeyDiagram-IPEJSGJF-CneWBRgG.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-a2sO5_Eo.js → sequenceDiagram-PO4LG4MO-DWsFCWap.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-CiPJmFEB.js → sizeCapture-INFHLROL-Bvf0YE3S.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-DAkUUlL7.js → stateDiagram-v2-GCMORJYK-Bxb3Iei8.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-DANf2R8Y.js → swimlanes-2SLR337P-DLkk7W4A.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-C6bZtBXk.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-BKpQmzrT.js → timeline-definition-EJHVYXUP-KR9bIHYC.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-BtI7qLN-.js → usecaseDiagram-POWQR4AR-DfsOrj8t.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-gXJXrAG_.js → vennDiagram-UO4OBE2U-DRSTzG3h.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-GU8Po5-n.js → wardleyDiagram-VNRHLVJA-DtEdhPgT.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-BF4J30g6.js → xychartDiagram-PMCCYNJV-B-XhTxjG.js} +1 -1
  60. package/dist/index.html +2 -2
  61. package/dist-server/cloud-home-start.js +3 -1
  62. package/dist-server/companion/src/routes.js +60 -0
  63. package/dist-server/container-mcp.js +94 -77
  64. package/dist-server/drivers/agents-proxy.js +96 -39
  65. package/dist-server/drivers/pi-mcp-extension.ts +245 -299
  66. package/dist-server/enterprise/server/index.js +3 -1
  67. package/dist-server/{browser-proxy.js → harness-mcp-proxy.js} +35 -19
  68. package/dist-server/index.js +18242 -13645
  69. package/dist-server/local-computer.js +3 -1
  70. package/dist-server/mcp-gate.js +147 -19
  71. package/dist-server/mcp-remote-proxy.js +437 -0
  72. package/dist-server/openmausbot.js +9618 -10058
  73. package/dist-server/pair-cli.js +9618 -10058
  74. package/dist-server/permission-proxy.js +65 -1
  75. package/dist-server/proxy-paths.js +3 -1
  76. package/dist-server/server/account-signin.js +0 -2
  77. package/dist-server/server/activity.js +201 -0
  78. package/dist-server/server/auto-approve.js +39 -11
  79. package/dist-server/server/automatic-recovery.js +1 -1
  80. package/dist-server/server/boat-create-idempotency.js +0 -7
  81. package/dist-server/server/boat-delete-journal.js +0 -4
  82. package/dist-server/server/bot-package.js +1 -19
  83. package/dist-server/server/browser-live.js +14 -5
  84. package/dist-server/server/browser-runtime.js +7 -2
  85. package/dist-server/server/channel-queue.js +4 -1
  86. package/dist-server/server/chief-of-staff.js +2 -3
  87. package/dist-server/server/cli.js +32 -12
  88. package/dist-server/server/cloud-backend.js +0 -4
  89. package/dist-server/server/cloud-computer-tools.js +110 -0
  90. package/dist-server/server/cloud-home.js +0 -1
  91. package/dist-server/server/cloud-lending.js +0 -3
  92. package/dist-server/server/cloud-move-http.js +9 -3
  93. package/dist-server/server/cloud-move.js +18 -0
  94. package/dist-server/server/composio.js +31 -2
  95. package/dist-server/server/computer-control.js +0 -15
  96. package/dist-server/server/computer-observation.js +0 -74
  97. package/dist-server/server/computer-wait.js +33 -13
  98. package/dist-server/server/config.js +73 -11
  99. package/dist-server/server/container-computer.js +7 -19
  100. package/dist-server/server/contracts.js +9 -0
  101. package/dist-server/server/control-client.js +4 -10
  102. package/dist-server/server/decision-log.js +9 -6
  103. package/dist-server/server/delegations.js +47 -22
  104. package/dist-server/server/drivers/acp/core.js +64 -13
  105. package/dist-server/server/drivers/acp/grok.js +208 -3
  106. package/dist-server/server/drivers/agents-call.js +70 -29
  107. package/dist-server/server/drivers/agents-catalog.js +34 -12
  108. package/dist-server/server/drivers/agents-proxy.js +8 -30
  109. package/dist-server/server/drivers/antigravity.js +1 -4
  110. package/dist-server/server/drivers/boatagent.js +25 -4
  111. package/dist-server/server/drivers/cerebras.js +3 -3
  112. package/dist-server/server/drivers/chat-mcp-tools.js +29 -19
  113. package/dist-server/server/drivers/claude.js +33 -19
  114. package/dist-server/server/drivers/codex.js +84 -10
  115. package/dist-server/server/drivers/openai-chat.js +14 -9
  116. package/dist-server/server/drivers/pi.js +48 -4
  117. package/dist-server/server/drivers/retry.js +8 -4
  118. package/dist-server/server/engine-install.js +1 -5
  119. package/dist-server/server/environment.js +3 -0
  120. package/dist-server/server/guarded-requests.js +2 -1
  121. package/dist-server/server/harness/registry.js +0 -1
  122. package/dist-server/server/{browser-proxy.js → harness-mcp-proxy.js} +40 -20
  123. package/dist-server/server/index.js +1929 -507
  124. package/dist-server/server/lending-memory.js +12 -8
  125. package/dist-server/server/live-call-controller.js +868 -0
  126. package/dist-server/server/live-call.js +216 -0
  127. package/dist-server/server/mcp-gate-config.js +43 -12
  128. package/dist-server/server/mcp-gate.js +127 -25
  129. package/dist-server/server/mcp-http.js +46 -8
  130. package/dist-server/server/mcp-remote-proxy.js +108 -0
  131. package/dist-server/server/memory-entries.js +38 -8
  132. package/dist-server/server/memory-organize.js +1 -8
  133. package/dist-server/server/memory-topics.js +4 -4
  134. package/dist-server/server/memory-upkeep.js +13 -15
  135. package/dist-server/server/message-file.js +5 -9
  136. package/dist-server/server/new-bot-defaults.js +3 -0
  137. package/dist-server/server/outbound-counts.js +79 -0
  138. package/dist-server/server/outbound-requests.js +60 -0
  139. package/dist-server/server/package-import.js +1 -1
  140. package/dist-server/server/peer-delivery.js +1 -0
  141. package/dist-server/server/peer-provenance.js +4 -2
  142. package/dist-server/server/permission-proxy.js +15 -1
  143. package/dist-server/server/plan-usage.js +797 -0
  144. package/dist-server/server/proxy-paths.js +3 -1
  145. package/dist-server/server/redact.js +2 -0
  146. package/dist-server/server/request-auth.js +3 -2
  147. package/dist-server/server/room-handoffs.js +5 -13
  148. package/dist-server/server/routes/live.js +98 -0
  149. package/dist-server/server/routine-requests.js +1 -1
  150. package/dist-server/server/routines.js +5 -1
  151. package/dist-server/server/setup-mode.js +0 -2
  152. package/dist-server/server/skill-learn.js +0 -1
  153. package/dist-server/server/skill-library.js +195 -3
  154. package/dist-server/server/skills-library-migration.js +235 -0
  155. package/dist-server/server/skills.js +66 -3
  156. package/dist-server/server/steer-queue.js +13 -1
  157. package/dist-server/server/store.js +41 -5
  158. package/dist-server/server/surface.js +74 -23
  159. package/dist-server/server/system-prompt.js +4 -4
  160. package/dist-server/server/team-backup.js +2 -1
  161. package/dist-server/server/team-memory.js +209 -0
  162. package/dist-server/server/thread-events.js +3 -3
  163. package/dist-server/server/tunnel.js +20 -1
  164. package/dist-server/server/turn-log.js +10 -0
  165. package/dist-server/server/turn-resources.js +124 -0
  166. package/dist-server/server/vps-computer.js +4 -14
  167. package/dist-server/server/workspace.js +85 -47
  168. package/dist-server/shared/activity.js +3 -0
  169. package/dist-server/shared/ask-question.js +13 -0
  170. package/dist-server/shared/bot-avatar.js +8 -0
  171. package/dist-server/shared/call-consent.js +28 -0
  172. package/dist-server/shared/connector-scopes.js +78 -0
  173. package/dist-server/shared/live-approval.js +124 -0
  174. package/dist-server/shared/live-call.js +176 -0
  175. package/dist-server/shared/outbound.js +128 -0
  176. package/dist-server/shared/skill-md.js +10 -0
  177. package/dist-server/shared/team-backup.js +3 -0
  178. package/dist-server/shared/tool-scope-support.js +25 -0
  179. package/dist-server/shared/tool-scope.js +136 -0
  180. package/dist-server/shared/tool-surface.js +3 -2
  181. package/dist-server/src/lib/thread-refs.js +406 -0
  182. package/dist-server/vps-container-mcp.js +94 -77
  183. package/dist-server/workspace-backup.worker.js +89 -100
  184. package/enterprise/server/index.js +3 -1
  185. package/package.json +1 -1
  186. package/dist/assets/channel-BU94VpxE.js +0 -1
  187. package/dist/assets/index-Biji-bfL.css +0 -1
  188. package/dist/assets/index-DZlhoES0.js +0 -597
  189. package/dist/assets/swimlanesDiagram-TC7HE7FX-CdHxG8aM.js +0 -8
  190. package/dist-server/server/drivers/chat-boat-tools.js +0 -90
  191. package/dist-server/server/drivers/grokagent.js +0 -4
  192. package/dist-server/server/openmaus-status-capsule.js +0 -329
@@ -94,7 +94,70 @@ function questionAnswersById(message, questions) {
94
94
  return only && flat ? { [only.id]: flat } : {};
95
95
  }
96
96
 
97
+ // shared/tool-scope.ts
98
+ var SERVER_NAME = /^[a-z][a-z0-9_-]{0,31}$/;
99
+ var CONTROL_CHARACTER = /[\x00-\x1f\x7f]/;
100
+ var MAX_SELECTORS = 256;
101
+ var MAX_SELECTOR_LENGTH = 1024;
102
+ function validName(name) {
103
+ return typeof name === "string" && name.trim().length > 0 && name.length <= MAX_SELECTOR_LENGTH && !CONTROL_CHARACTER.test(name) && !name.includes("*");
104
+ }
105
+ function selectorIdentity(selector) {
106
+ if (selector.length > MAX_SELECTOR_LENGTH || CONTROL_CHARACTER.test(selector)) return;
107
+ if (selector.startsWith("native:")) {
108
+ const name = selector.slice(7);
109
+ if (name === "*" || validName(name)) return { kind: "native", name };
110
+ } else if (selector.startsWith("mcp:")) {
111
+ const separator = selector.indexOf(":", 4);
112
+ if (separator < 0) return;
113
+ const server = selector.slice(4, separator);
114
+ const name = selector.slice(separator + 1);
115
+ if (SERVER_NAME.test(server) && (name === "*" || validName(name))) {
116
+ return { kind: "mcp", server, name };
117
+ }
118
+ }
119
+ }
120
+ function parseToolScope(value) {
121
+ if (value === void 0) return { ok: true, scope: void 0 };
122
+ const invalid = { ok: false, error: "Tool selection must contain only valid allow and deny lists." };
123
+ if (!value || typeof value !== "object" || Array.isArray(value)) return invalid;
124
+ const prototype = Object.getPrototypeOf(value);
125
+ if (prototype !== Object.prototype && prototype !== null) return invalid;
126
+ const scope = {};
127
+ for (const key of Reflect.ownKeys(value)) {
128
+ if (key !== "allow" && key !== "deny") return invalid;
129
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
130
+ if (!descriptor || !("value" in descriptor)) return invalid;
131
+ const list = descriptor.value;
132
+ if (!Array.isArray(list) || list.length > MAX_SELECTORS) return invalid;
133
+ if (!Array.from(list).every((entry) => typeof entry === "string" && selectorIdentity(entry))) return invalid;
134
+ scope[key] = [...new Set(list)];
135
+ }
136
+ return { ok: true, scope };
137
+ }
138
+ function matches(selector, tool) {
139
+ const selected = selectorIdentity(selector);
140
+ return selected?.kind === tool.kind && (selected.kind !== "mcp" || tool.kind === "mcp" && selected.server === tool.server) && (selected.name === "*" || selected.name === tool.name);
141
+ }
142
+ function permits(scope, tool) {
143
+ if (!validName(tool.name) || tool.kind === "mcp" && !SERVER_NAME.test(tool.server)) return false;
144
+ return (scope?.allow === void 0 || scope.allow.some((selector) => matches(selector, tool))) && !scope?.deny?.some((selector) => matches(selector, tool));
145
+ }
146
+ function allowsTool(scope, tool) {
147
+ const parsed = parseToolScope(scope);
148
+ return parsed.ok && permits(parsed.scope, tool);
149
+ }
150
+
97
151
  // server/permission-proxy.ts
152
+ var selection = parseToolScope(process.env.OMB_PERMISSION_TOOL_SCOPE === void 0 ? void 0 : (() => {
153
+ try {
154
+ return JSON.parse(process.env.OMB_PERMISSION_TOOL_SCOPE);
155
+ } catch {
156
+ return null;
157
+ }
158
+ })());
159
+ if (!selection.ok) throw new Error(selection.error);
160
+ var allowQuestion = allowsTool(selection.scope, { kind: "mcp", server: "ogb", name: "ask_user" });
98
161
  var socketPath = process.argv[2] ?? "";
99
162
  var waiting = /* @__PURE__ */ new Map();
100
163
  var conn = connect(socketPath);
@@ -203,11 +266,12 @@ async function handle(msg) {
203
266
  }
204
267
  });
205
268
  }
206
- if (msg.method === "tools/list") return send({ jsonrpc: "2.0", id: msg.id, result: { tools: TOOLS } });
269
+ if (msg.method === "tools/list") return send({ jsonrpc: "2.0", id: msg.id, result: { tools: TOOLS.filter((tool) => tool.name === "approve" || allowQuestion) } });
207
270
  if (msg.method === "tools/call") {
208
271
  const name = msg.params?.name;
209
272
  const args = msg.params?.arguments ?? {};
210
273
  const reply = (text3) => send({ jsonrpc: "2.0", id: msg.id, result: { content: [{ type: "text", text: text3 }] } });
274
+ if (name === "ask_user" && !allowQuestion) return reply("OpenMausBot: this question tool is excluded by the bot's tool selection.");
211
275
  if (name === "approve" && args.tool_name === ASK_USER_QUESTION) {
212
276
  return reply(await answerNativeQuestions(args.input));
213
277
  }
@@ -8,7 +8,8 @@ function resolveProxy(relative) {
8
8
  return existsSync(source) ? source : join(SERVER_ROOT, `${relative}.js`);
9
9
  }
10
10
  var SPAWNED_PROXIES = {
11
- browser: resolveProxy("browser-proxy"),
11
+ // The built-in browser and the cloud computer: argv[2] picks which.
12
+ harnessMcp: resolveProxy("harness-mcp-proxy"),
12
13
  localComputer: resolveProxy("local-computer-proxy"),
13
14
  permission: resolveProxy("permission-proxy"),
14
15
  containerMcp: resolveProxy("container-mcp"),
@@ -17,6 +18,7 @@ var SPAWNED_PROXIES = {
17
18
  dweb: resolveProxy("drivers/dweb-proxy"),
18
19
  connectors: resolveProxy("connector-proxy"),
19
20
  mcpGate: resolveProxy("mcp-gate"),
21
+ mcpRemote: resolveProxy("mcp-remote-proxy"),
20
22
  phone: resolveProxy("drivers/phone-proxy"),
21
23
  hook: resolveProxy("hooks/omb-hook"),
22
24
  // Loaded by the external `pi` process via `-e`, not by this server — but
@@ -11,8 +11,6 @@
11
11
  // not expose the routes.
12
12
  import { resolveCompanionControlPlaneURL } from "../electron/companion-account-service.mjs";
13
13
  import { ControlPlaneError, createControlPlaneClient, normalizeAccountEmail } from "../electron/control-plane-client.mjs";
14
- export const ADMIN_EMAILS_ENV = "OMB_SIGNIN_EMAILS";
15
- export const MEMBER_EMAILS_ENV = "OMB_SIGNIN_MEMBER_EMAILS";
16
14
  /** Commas, spaces or newlines between entries; case does not matter. */
17
15
  export function parseAllowList(value) {
18
16
  return (value ?? "")
@@ -0,0 +1,201 @@
1
+ // A bot's activity log: what it did, in plain words, with the outcome.
2
+ //
3
+ // Nothing here is captured for the log's sake. Two records already on disk
4
+ // between them know everything a receipt needs: the per-thread runtime
5
+ // events (harness/bus.ts) say what ran and whether it finished, and the
6
+ // fleet-wide decision log (decision-log.ts) says what was asked, with which
7
+ // arguments, and who allowed it. This module reads both back and folds
8
+ // them into one row per thing that happened.
9
+ //
10
+ // Connector calls pass through the harness relay, whose approval and scope
11
+ // decisions share the decision log. Read those alongside native tool runs.
12
+ import { join } from "node:path";
13
+ import { readDecisions } from "./decision-log.js";
14
+ import { isRuntimeEvent, readRecentLines } from "./thread-events.js";
15
+ /** Composio toolkit slugs whose display name is not just a capitalised word. */
16
+ const TOOLKIT_NAMES = {
17
+ GMAIL: "Gmail",
18
+ GITHUB: "GitHub",
19
+ GOOGLESHEETS: "Google Sheets",
20
+ GOOGLECALENDAR: "Google Calendar",
21
+ GOOGLEDRIVE: "Google Drive",
22
+ GOOGLEDOCS: "Google Docs",
23
+ HUBSPOT: "HubSpot",
24
+ LINKEDIN: "LinkedIn",
25
+ YOUTUBE: "YouTube",
26
+ TWITTER: "X",
27
+ WHATSAPP: "WhatsApp",
28
+ ONEDRIVE: "OneDrive",
29
+ };
30
+ /** The agent's own built-in tools, phrased as what they did. */
31
+ const BUILTIN_LABELS = {
32
+ Bash: "Ran a command",
33
+ Read: "Read a file",
34
+ Edit: "Edited a file",
35
+ MultiEdit: "Edited files",
36
+ Write: "Wrote a file",
37
+ Glob: "Searched files",
38
+ Grep: "Searched files",
39
+ LS: "Listed a folder",
40
+ WebSearch: "Searched the web",
41
+ WebFetch: "Fetched a page",
42
+ Skill: "Used a skill",
43
+ Task: "Ran a subagent",
44
+ Agent: "Ran a subagent",
45
+ AskUserQuestion: "Asked you a question",
46
+ TodoWrite: "Updated its task list",
47
+ };
48
+ /** "save_issue" → "Save issue", "TaskUpdate" → "Task update",
49
+ * "CREATE_AN_ISSUE" → "Create an issue". */
50
+ function humanize(raw) {
51
+ const spaced = raw
52
+ .replace(/_+/g, " ")
53
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
54
+ .replace(/\s+/g, " ")
55
+ .trim()
56
+ .toLowerCase();
57
+ return spaced ? spaced[0].toUpperCase() + spaced.slice(1) : raw;
58
+ }
59
+ function toolkitName(slug) {
60
+ return TOOLKIT_NAMES[slug] ?? slug[0] + slug.slice(1).toLowerCase();
61
+ }
62
+ /** Composio names its tools TOOLKIT_ACTION_WORDS, all upper case. */
63
+ const COMPOSIO_TOOL = /^([A-Z][A-Z0-9]+)_([A-Z0-9_]+)$/;
64
+ /** Turn a raw tool name into the app it touched and the action, in words. */
65
+ export function describeTool(name) {
66
+ const builtin = BUILTIN_LABELS[name];
67
+ if (builtin)
68
+ return { app: null, label: builtin };
69
+ if (name.startsWith("mcp__")) {
70
+ const [, server = "", ...rest] = name.split("__");
71
+ const action = rest.join("__");
72
+ if (server === "composio")
73
+ return describeTool(action);
74
+ if (server === "computer")
75
+ return { app: "Computer", label: humanize(action) };
76
+ if (server === "agents")
77
+ return { app: "Team", label: humanize(action) };
78
+ if (server.startsWith("claude_ai_")) {
79
+ return { app: server.slice("claude_ai_".length).replace(/_/g, " "), label: humanize(action) };
80
+ }
81
+ return { app: humanize(server), label: humanize(action) };
82
+ }
83
+ const composio = COMPOSIO_TOOL.exec(name);
84
+ if (composio)
85
+ return { app: toolkitName(composio[1]), label: humanize(composio[2]) };
86
+ return { app: null, label: humanize(name) };
87
+ }
88
+ /** How many decision rows to read back before filtering to one bot. The
89
+ * file is rotation-capped, so this is a ceiling on parse work, not a
90
+ * window that quietly drops history. */
91
+ const DECISION_SCAN = 20_000;
92
+ /** Runtime events read per thread, newest first; the reader tail-walks. */
93
+ const EVENTS_PER_THREAD = 2_000;
94
+ /** An approval is folded into the tool run it unblocked when the run
95
+ * started inside this window around the decision. A verdict is written
96
+ * as the request opens; the run follows it, occasionally by a clock skew's
97
+ * worth in the other direction. */
98
+ const MATCH_BEFORE_MS = 5_000;
99
+ const MATCH_AFTER_MS = 30_000;
100
+ const OUTCOME_OF_DECISION = {
101
+ "auto-approved": "allowed",
102
+ "auto-denied": "denied",
103
+ "user-approved": "allowed",
104
+ "review-would-approve": "allowed",
105
+ "user-denied": "denied",
106
+ "review-would-deny": "denied",
107
+ "card-shown": "waiting",
108
+ };
109
+ function describeRow(base) {
110
+ const { app, label } = describeTool(base.tool);
111
+ return { ...base, app, label };
112
+ }
113
+ /** One row per request: the decision log writes a row when a card is shown
114
+ * and another when it is answered, and the receipt wants the answer. */
115
+ function foldDecisions(rows) {
116
+ const byRequest = new Map();
117
+ rows.forEach((row, index) => {
118
+ const key = row.requestId ?? `row:${index}`;
119
+ const group = byRequest.get(key);
120
+ if (group)
121
+ group.push(row);
122
+ else
123
+ byRequest.set(key, [row]);
124
+ });
125
+ const folded = [];
126
+ for (const group of byRequest.values()) {
127
+ const opened = group[0];
128
+ const latest = group[group.length - 1];
129
+ const summary = group.find((row) => row.summary)?.summary;
130
+ folded.push(describeRow({
131
+ at: opened.at,
132
+ threadId: opened.threadId,
133
+ requestId: opened.requestId,
134
+ tool: latest.tool ?? opened.tool ?? "unknown",
135
+ ...(summary ? { summary } : {}),
136
+ outcome: OUTCOME_OF_DECISION[latest.decision],
137
+ }));
138
+ }
139
+ return folded;
140
+ }
141
+ function readToolRuns(eventsDir, threadId) {
142
+ const { lines } = readRecentLines(join(eventsDir, `${threadId}.ndjson`), EVENTS_PER_THREAD, isRuntimeEvent);
143
+ const runs = new Map();
144
+ for (const event of lines) {
145
+ if (event.type === "item.started" && event.itemType === "tool") {
146
+ const key = JSON.stringify([event.turnId ?? "", event.itemId ?? event.eventId]);
147
+ runs.set(key, describeRow({
148
+ at: event.createdAt,
149
+ threadId,
150
+ ...(event.turnId ? { turnId: event.turnId } : {}),
151
+ tool: event.title ?? "unknown",
152
+ outcome: "running",
153
+ }));
154
+ }
155
+ else if (event.type === "item.completed" && event.itemType === "tool" && event.itemId) {
156
+ const run = runs.get(JSON.stringify([event.turnId ?? "", event.itemId]));
157
+ if (run)
158
+ run.outcome = event.ok ? "ran" : "failed";
159
+ }
160
+ }
161
+ return [...runs.values()];
162
+ }
163
+ /** The bot's activity, newest first: every tool run in the threads named,
164
+ * and every decision the broker logged for the bot, with an approval folded
165
+ * into the run it unblocked wherever the two can be matched. */
166
+ export function readBotActivity(input) {
167
+ const { dataDir, eventsDir, botId, threadIds, limit } = input;
168
+ const runs = threadIds.flatMap((threadId) => readToolRuns(eventsDir, threadId));
169
+ const decisions = foldDecisions(readDecisions(dataDir, DECISION_SCAN).filter((row) => row.botId === botId));
170
+ const matched = new Set();
171
+ const rows = [...runs];
172
+ for (const decision of decisions) {
173
+ if (decision.outcome !== "allowed") {
174
+ rows.push(decision);
175
+ continue;
176
+ }
177
+ const askedAt = Date.parse(decision.at);
178
+ let best = null;
179
+ for (const run of runs) {
180
+ if (matched.has(run) || run.threadId !== decision.threadId || run.tool !== decision.tool)
181
+ continue;
182
+ const delta = Date.parse(run.at) - askedAt;
183
+ if (delta < -MATCH_BEFORE_MS || delta > MATCH_AFTER_MS)
184
+ continue;
185
+ if (!best || Math.abs(delta) < Math.abs(Date.parse(best.at) - askedAt))
186
+ best = run;
187
+ }
188
+ if (best) {
189
+ matched.add(best);
190
+ if (decision.summary)
191
+ best.summary = decision.summary;
192
+ if (decision.requestId)
193
+ best.requestId = decision.requestId;
194
+ }
195
+ else {
196
+ rows.push(decision);
197
+ }
198
+ }
199
+ rows.sort((a, b) => (a.at < b.at ? 1 : a.at > b.at ? -1 : 0));
200
+ return rows.slice(0, Math.max(1, limit));
201
+ }
@@ -7,6 +7,7 @@
7
7
  // grants the app applies are Full access and the person's exact saved commands.
8
8
  // Questions never come through here: a bot's question always reaches a human.
9
9
  import { supportsApprovalMode } from "../shared/approval-mode.js";
10
+ import { isOutboundTool } from "../shared/outbound.js";
10
11
  /** A failed delivery is a runtime error, not another permission decision.
11
12
  * An expired ask must never become a fresh Allow/Deny card. */
12
13
  export async function deliverFullAccessApproval(adapter, threadId, requestId, turnId, isCurrent = () => false) {
@@ -25,7 +26,7 @@ export async function deliverFullAccessApproval(adapter, threadId, requestId, tu
25
26
  }
26
27
  /** Full access is the person's explicit grant to this receiving bot, including
27
28
  * delegated work. It never inherits the sender's mode or elevates another bot
28
- * — with the one exception below (delegationInheritsFullAccess), applied
29
+ * — with the one exception below (delegatedApprovalMode), applied
29
30
  * where a Chief's delegated thread is created rather than here.
30
31
  * Custom is a provider-config choice rather than an app Full-access grant, so
31
32
  * peer-started Custom turns use Auto. Provider support and grant confirmation
@@ -35,16 +36,38 @@ export function approvalModeForOrigin(mode, origin) {
35
36
  return "auto";
36
37
  return mode;
37
38
  }
38
- /** Whether work a bot hands to a teammate runs with Full access. Only a Chief
39
- * of Staff passes access on, and only the Full access the person gave it for
40
- * the conversation it is delegating from: the Chief exists to get the team's
41
- * work done without the person answering every card, and a teammate stopping
42
- * that work to ask defeats the grant. The recipient's engine has to implement
43
- * Full, or the work keeps the recipient's own level. A bot never elevates
44
- * itself this way. */
45
- export function delegationInheritsFullAccess(input) {
46
- return input.senderIsChief && input.senderHasFullAccess && !input.sameBot
47
- && supportsApprovalMode(input.recipientDriverKind, "full");
39
+ /** Levels by how much runs without asking. Custom is a provider config of
40
+ * its own and is never compared. */
41
+ const LEVEL_RANK = { ask: 0, edits: 1, auto: 2, full: 3 };
42
+ /** The level work a bot hands to a teammate starts at, or null to keep the
43
+ * teammate's own. A Chief of Staff's level flows down: the person set it on
44
+ * the Chief so the team's work runs that way, and switching every thread the
45
+ * Chief opens with a teammate back off "Ask for approval" was the friction
46
+ * people reported. Only a Chief passes its level on, only the level of the
47
+ * conversation it delegates from, and only upward: a teammate already on a
48
+ * higher level keeps it, and one on Custom keeps its own config. The level
49
+ * is capped at what the teammate's engine implements (Full or Auto-accept
50
+ * edits are not everywhere), stepping down to the next level it has. A bot
51
+ * never raises itself this way. */
52
+ export function delegatedApprovalMode(input) {
53
+ if (!input.senderIsChief || input.sameBot)
54
+ return null;
55
+ // A Chief's Custom config is its own; for a teammate it reads as Approve
56
+ // for me, as for any peer-started Custom turn (approvalModeForOrigin).
57
+ const sender = LEVEL_RANK[approvalModeForOrigin(input.senderMode, { peerInitiated: true })] ?? 0;
58
+ const own = LEVEL_RANK[input.recipientMode];
59
+ if (own === undefined)
60
+ return null;
61
+ for (const mode of ["full", "auto", "edits"]) {
62
+ const rank = LEVEL_RANK[mode];
63
+ if (rank > sender)
64
+ continue;
65
+ if (rank <= own)
66
+ return null;
67
+ if (supportsApprovalMode(input.recipientDriverKind, mode))
68
+ return mode;
69
+ }
70
+ return null;
48
71
  }
49
72
  // Tools that ask a PERSON something. A question exists so that a human
50
73
  // decides; any mode answering one on their behalf defeats the only reason
@@ -69,6 +92,8 @@ export function autoVerdict(mode, tool, context) {
69
92
  return { approve: `approved ${tool} (full access)`, source: "full-access" };
70
93
  if (context?.requiresExplicitApproval)
71
94
  return { approve: null, source: "explicit-approval-block" };
95
+ if (isOutboundTool(tool))
96
+ return { approve: null, source: "outbound-guard" };
72
97
  if (context?.commandAllowed)
73
98
  return { approve: `approved ${tool} (saved command)`, source: "command-allowlist" };
74
99
  if (mode === "auto" || mode === "custom")
@@ -83,6 +108,7 @@ export function autoVerdict(mode, tool, context) {
83
108
  * the server keeps sending: cards saved before the key existed still render,
84
109
  * and so do the free-text apply errors that have no key at all. */
85
110
  export const HELD_NOTE = {
111
+ "approval.held.outbound": "This sends something on your behalf, so it always asks first.",
86
112
  "approval.held.native": "The provider requires your approval for this action.",
87
113
  "approval.held.sandbox": "This changes the provider sandbox, so only Full access can approve it automatically.",
88
114
  "approval.held.undeliveredFull": "Full access couldn't deliver this approval.",
@@ -93,6 +119,8 @@ export const HELD_NOTE = {
93
119
  export function approvalHeldNote(context) {
94
120
  if (!context.permission)
95
121
  return undefined;
122
+ if (context.source === "outbound-guard")
123
+ return "approval.held.outbound";
96
124
  if (context.source === "explicit-approval-block")
97
125
  return "approval.held.sandbox";
98
126
  if (context.source === "native-approval")
@@ -7,7 +7,7 @@ export function recoveryCapabilityError(from, to) {
7
7
  supportsWorkspaceFiles(from.driverKind) !== supportsWorkspaceFiles(to.driverKind)) {
8
8
  return "The backup cannot use the same workspace.";
9
9
  }
10
- const tools = ["agentsMcp", "computerMcp", "cloudComputerMcp", "composioMcp",
10
+ const tools = ["agentsMcp", "computerMcp", "composioMcp",
11
11
  "phoneMcp", "browserMcp", "localComputerMcp", "customMcp", "images"];
12
12
  if (tools.some(key => from.capabilities[key] === true && to.capabilities[key] !== true)) {
13
13
  return "The backup does not support all of this engine's tools and attachments.";
@@ -388,13 +388,6 @@ export function resolveBoatCreate(request) {
388
388
  return { ...resolved };
389
389
  });
390
390
  }
391
- /** Read-only deletion guard. Both a key-only request with an ambiguous
392
- * provider outcome and a known-but-not-yet-named Boat must keep its bot owner. */
393
- export function hasUnresolvedBoatCreate(botId) {
394
- if (!BOT_ID.test(botId))
395
- throw new Error("invalid bot id for cloud computer creation");
396
- return withJournalLock((requests) => (requests.some((request) => request.botId === botId && request.resolved !== true)));
397
- }
398
391
  /** Sanitized local recovery authority for configuration guards. The fresh,
399
392
  * lock-protected read can prove a remembered provider id even while the
400
393
  * account-wide LIST endpoint is eventually consistent. Provider request
@@ -442,7 +442,6 @@ export function markBoatDeletionBlocked(boxId, value) {
442
442
  export function boatDeletionSnapshot() {
443
443
  return withJournalLock((records) => records.map(clone));
444
444
  }
445
- export const listBoatDeletions = boatDeletionSnapshot;
446
445
  export function getBoatDeletion(boxId) {
447
446
  if (!BOAT_ID.test(boxId))
448
447
  throw new Error("invalid cloud computer id for deletion");
@@ -451,9 +450,6 @@ export function getBoatDeletion(boxId) {
451
450
  return record ? clone(record) : null;
452
451
  });
453
452
  }
454
- export function isBoatDeletionPending(boxId) {
455
- return getBoatDeletion(boxId) !== null;
456
- }
457
453
  export function hasPendingBoatDeletionForBot(botId) {
458
454
  if (!BOT_ID.test(botId))
459
455
  throw new Error("invalid bot id for cloud computer deletion");
@@ -1,15 +1,11 @@
1
1
  import { stringify as stringifyYaml } from "yaml";
2
2
  import { cronScheduleLabel } from "../shared/cron-label.js";
3
- import { BOTMRR_MARKDOWN_VERSION, isPackageDocument, PACKAGE_FORMAT, PACKAGE_V1_MAX_SKILLS, PACKAGE_V1_VERSION, parsePackageV1, utf8Bytes, } from "../shared/package-format.js";
3
+ import { BOTMRR_MARKDOWN_VERSION, isPackageDocument, PACKAGE_V1_MAX_SKILLS, parsePackageV1, utf8Bytes, } from "../shared/package-format.js";
4
4
  // The package file format (v1 and v2) lives in shared/package-format.ts: one
5
5
  // validation gate for the server, the renderer and Admin. This module keeps
6
6
  // the v1 names the v1 exporter and its Markdown playbook are written in, and
7
7
  // re-exports the v2 reader so server code has one place to import from.
8
8
  export { canonicalJson, downgradeToV1, PACKAGE_MAX_BYTES, PACKAGE_VERSION, PackageFormatError, packageKeys, packageScanFindings, packageSecretFindings, packageSummary, parsePackageDocument, redactPackageSecrets, upgradeV1, } from "../shared/package-format.js";
9
- export const BOT_PACKAGE_FORMAT = PACKAGE_FORMAT;
10
- export const BOT_PACKAGE_VERSION = PACKAGE_V1_VERSION;
11
- export { BOTMRR_MARKDOWN_VERSION };
12
- export const BOT_PACKAGE_SKILLS_VERSION = 1;
13
9
  export const BOT_PACKAGE_MAX_SKILLS = PACKAGE_V1_MAX_SKILLS;
14
10
  const BOT_PACKAGE_MARKDOWN_MAX_BYTES = 1_000_000;
15
11
  export function isBotPackage(value) {
@@ -98,17 +94,3 @@ export function renderBotPackageMarkdown(document) {
98
94
  throw new Error("The bot playbook is too large");
99
95
  return markdown;
100
96
  }
101
- export function packageAgentAsMember(agent) {
102
- return {
103
- key: agent.key,
104
- name: agent.name,
105
- title: agent.title ?? "",
106
- description: agent.description ?? "",
107
- ...(agent.soul !== undefined ? { soul: agent.soul } : {}),
108
- appearance: {
109
- color: agent.appearance.color,
110
- ...(agent.appearance.mascotExpression ? { mascotExpression: agent.appearance.mascotExpression } : {}),
111
- ...(agent.appearance.mascotBody ? { mascotBody: agent.appearance.mascotBody } : {}),
112
- },
113
- };
114
- }
@@ -10,6 +10,9 @@ const HEARTBEAT_MS = 10_000;
10
10
  // The native press resolver supplies the virtual key codes and Enter/Tab text
11
11
  // that its raw input_keyboard relay omits. Keep unknown keys literal.
12
12
  const DISCRETE_KEYS = new Set(["Backspace", "Enter", "Tab", "Escape", "Delete", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Home", "End", "PageUp", "PageDown"]);
13
+ // The panel has no take-control button: interacting takes control, and an
14
+ // interrupted action is recovered from the panel's menu.
15
+ const RESTART_NEEDED = "A browser action was interrupted. Choose Restart browser… in the browser menu before continuing.";
13
16
  export class BrowserLiveError extends Error {
14
17
  status;
15
18
  constructor(message, status = 400) { super(message); this.name = "BrowserLiveError"; this.status = status; }
@@ -569,22 +572,24 @@ export class BrowserLive {
569
572
  try {
570
573
  const taking = this.runtime.take(viewer.session, viewer.id);
571
574
  this.control(viewer.session);
572
- await taking;
575
+ // waited: the bot's own action finished first, so input the person
576
+ // aimed at the page before the grant may no longer fit it.
577
+ const waited = await taking;
573
578
  if (!this.current(viewer)) {
574
579
  this.close(viewer);
575
580
  throw new BrowserLiveError("This browser view closed.", 409);
576
581
  }
577
- return { ok: true };
582
+ return { ok: true, waited: waited === true };
578
583
  }
579
584
  catch {
580
- throw new BrowserLiveError("Another browser view or bot action is using this browser. Try again shortly.", 409);
585
+ throw this.refusal(viewer, "Another browser view or bot action is using this browser. Try again shortly.");
581
586
  }
582
587
  finally {
583
588
  this.control(viewer.session);
584
589
  }
585
590
  }
586
591
  if (!this.runtime.canControl(viewer.session, viewer.id))
587
- throw new BrowserLiveError("Take control of this browser before interacting.", 409);
592
+ throw this.refusal(viewer, "Browser control changed. Try again.");
588
593
  if (viewer.pendingActions >= 32)
589
594
  throw new BrowserLiveError("Too many browser actions are pending. Try again shortly.", 429);
590
595
  viewer.pendingActions += 1;
@@ -600,13 +605,17 @@ export class BrowserLive {
600
605
  return { ok: true };
601
606
  }
602
607
  catch (error) {
603
- throw error instanceof BrowserLiveError ? error : new BrowserLiveError("Browser control changed. Take control again to continue.", 409);
608
+ throw error instanceof BrowserLiveError ? error : this.refusal(viewer, "Browser control changed. Try again.");
604
609
  }
605
610
  finally {
606
611
  viewer.pendingActions -= 1;
607
612
  this.control(viewer.session);
608
613
  }
609
614
  }
615
+ /** Why this viewer cannot act now: an interrupted browser needs a restart. */
616
+ refusal(viewer, otherwise) {
617
+ return new BrowserLiveError(this.runtime.interrupted(viewer.session) ? RESTART_NEEDED : otherwise, 409);
618
+ }
610
619
  closeForSession(session) { for (const viewer of this.viewers.values())
611
620
  if (viewer.session === session)
612
621
  this.close(viewer); }
@@ -216,8 +216,7 @@ export class BrowserRuntime {
216
216
  options;
217
217
  closeBrowser;
218
218
  constructor({ closeBrowser, ...options } = {}) {
219
- const budget = Number(process.env.OMB_BROWSER_RESULT_BUDGET);
220
- this.options = { requestTimeoutMs: 120_000, takeoverTimeoutMs: 15_000, idleMs: 60_000, maxPending: 16, resultBudget: Number.isFinite(budget) && budget > 0 ? budget : DEFAULT_BROWSER_RESULT_BUDGET, ...options };
219
+ this.options = { requestTimeoutMs: 120_000, takeoverTimeoutMs: 15_000, idleMs: 60_000, maxPending: 16, resultBudget: DEFAULT_BROWSER_RESULT_BUDGET, ...options };
221
220
  this.closeBrowser = closeBrowser ?? (async () => false);
222
221
  }
223
222
  gate(session) {
@@ -364,6 +363,8 @@ export class BrowserRuntime {
364
363
  };
365
364
  return method === "tools/call" ? this.withAgentAction(session, invoke) : invoke();
366
365
  }
366
+ /** Resolves true when the grant had to wait for the bot's own browser
367
+ * action to finish: the page may have changed since the person aimed. */
367
368
  async take(session, owner) {
368
369
  if (!owner)
369
370
  throw new Error("Browser control requires an owner.");
@@ -374,6 +375,7 @@ export class BrowserRuntime {
374
375
  throw new Error("Another person controls this browser.");
375
376
  gate.owner = owner; // synchronous: no new agent work slips in while draining.
376
377
  gate.ready = false;
378
+ const waited = gate.agents > 0;
377
379
  await new Promise((resolve, reject) => {
378
380
  const finish = (error) => {
379
381
  clearTimeout(timer);
@@ -398,12 +400,15 @@ export class BrowserRuntime {
398
400
  gate.changed.add(check);
399
401
  check();
400
402
  });
403
+ return waited;
401
404
  }
402
405
  canControl(session, owner) {
403
406
  const gate = this.gates.get(session);
404
407
  return Boolean(owner && gate?.owner === owner && gate.ready && !gate.releasing && !gate.closing && !gate.uncertain && gate.agents === 0);
405
408
  }
406
409
  heldBy(session) { return this.gates.get(session)?.owner ?? null; }
410
+ /** An interrupted action left the browser's state unknown; only a restart clears it. */
411
+ interrupted(session) { return this.gates.get(session)?.uncertain === true; }
407
412
  /** A disconnected viewer may leave a physical key/button pressed. Never
408
413
  * let an agent inherit that input state; explicit restart clears it. */
409
414
  abandonHumanInput(session, owner) {
@@ -17,8 +17,11 @@ export function restoreChannelMessages() {
17
17
  const entry = queues.get(row.threadId) ?? { groupId: row.ownerId, items: [] };
18
18
  if (entry.groupId !== row.ownerId)
19
19
  throw new Error("queued task belongs to another channel");
20
+ // a channel line is only ever stamped "api"; "call" belongs to 1:1 rows
21
+ const { via, ...payload } = row.payload;
20
22
  entry.items.push({
21
- ...row.payload,
23
+ ...payload,
24
+ ...(via === "api" ? { via } : {}),
22
25
  id: row.id,
23
26
  mode: row.payload.mode ?? "chat",
24
27
  // rows queued before timestamps were kept read as queued at restore
@@ -9,7 +9,7 @@ const sectionKey = (section) => section?.trim() || "";
9
9
  /** Dynamic system context for a section's Chief of Staff.
10
10
  * It names the current team on every turn, while list_bots remains the
11
11
  * authoritative tool for IDs and live availability at delegation time. */
12
- export function chiefOfStaffSystemPrompt(chiefId, bots, canDelegate, trustedOpenMausStatus = "", boundedCoordination = false) {
12
+ export function chiefOfStaffSystemPrompt(chiefId, bots, canDelegate, boundedCoordination = false) {
13
13
  const chief = bots.find((bot) => bot.id === chiefId);
14
14
  const chiefSection = sectionKey(chief?.section);
15
15
  const sectionName = peerName(chiefSection) || "General";
@@ -29,7 +29,7 @@ export function chiefOfStaffSystemPrompt(chiefId, bots, canDelegate, trustedOpen
29
29
  });
30
30
  const delegation = canDelegate
31
31
  ? boundedCoordination
32
- ? "Use list_bots or list_room_targets for the live reachable roster. Use coordinate_bots to ask actual teammates for advice or assign concrete work. Outside a room, each distinct assignment starts a fresh thread for that teammate, using their own model and permissions. Include the context they need in each brief. Busy teammates queue. Give self-contained briefs, then end your turn; you resume automatically after their results return. Leads can coordinate their own specialists. Do not poll, send acknowledgements as new work, or substitute native helpers for named bots. On return, verify the requested outcome, resolve decisions within the user's scope, request concrete corrections with rework=true when necessary, and return one consolidated answer. Consultations are advice, not proof that work or tests ran. A refusal from coordinate_bots means nothing was sent: fix what it names (usually the id — copy it from list_bots or your roster; a unique teammate name also works) and retry, and never describe a handoff the tool did not accept."
32
+ ? "Use list_bots or list_room_targets for the live reachable roster. Use coordinate_bots to ask actual teammates for advice or assign concrete work. Give clear briefs, then end your turn; you resume automatically after their results return. Leads can coordinate their own specialists. Do not poll, send acknowledgements as new work, or substitute native helpers for named bots. On return, verify the requested outcome, resolve decisions within the user's scope, request concrete corrections with rework=true when necessary, and return one consolidated answer. Consultations are advice, not proof that work or tests ran. A refusal from coordinate_bots means nothing was sent: fix what it names (usually the id — copy it from list_bots or your roster; a unique teammate name also works) and retry, and never describe a handoff the tool did not accept."
33
33
  : [
34
34
  "Use list_bots to confirm the live roster and IDs. When assigning work to a teammate, use delegate_bot: it returns immediately, keeps you available to the user, and delivers the teammate's outcome back into this conversation automatically — success or failure. When the result arrives you are woken with it: report it to the user and act. If the teammate fails or stalls, tell the user plainly and decide the next step yourself.",
35
35
  "After delegate_bot accepts the task, acknowledge the handoff and continue with any independent work or end your turn. Do not call wait_delegation or repeatedly poll check_delegation in the same turn.",
@@ -52,6 +52,5 @@ export function chiefOfStaffSystemPrompt(chiefId, bots, canDelegate, trustedOpen
52
52
  canDelegate ? "When the user asks you to assemble or configure a team, use list_team_setup for the exact authorized teams, bot IDs and model catalog, then propose_team_setup once with all named specialists and their profile/model changes. Include new teams explicitly; the plan covers their creation and your access. Existing thread models and other bots' execution permissions stay unchanged. Follow the tool result: granted Full Access may apply the plan immediately; after an applied result, continue already-requested work without another confirmation. Only if review is pending, end your turn: the user's decision automatically resumes you once with a structured result. Report failed or cancelled results honestly. Do not ask for another yes, poll, or repeat the proposal. After successful setup, use the available coordination tools for already requested work. Use create_bot only for a single specialist when no combined setup was requested. For explicitly requested bot deletion, use propose_bot_deletion separately and follow its applied or pending result too. Do not create duplicate or unnecessary bots." : "",
53
53
  chief?.managedSections?.length ? "Reachable teammates in your allowed teams:" : `Current ${sectionName} section team:`,
54
54
  roster,
55
- trustedOpenMausStatus,
56
55
  ].filter(Boolean).join("\n");
57
56
  }