openmausbot 0.1.86 → 0.1.87

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 (166) hide show
  1. package/README.md +2 -2
  2. package/dist/assets/{abnfDiagram-O67JEVCF-DpUZNnFD.js → abnfDiagram-O67JEVCF-BTiuL79F.js} +1 -1
  3. package/dist/assets/{arc-DW7mrI4p.js → arc-C88Blc2v.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-CyeZ5WLZ.js → architectureDiagram-NJMV4G6O-BZsJIZrb.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-CBVaNjO7.js → blockDiagram-BEXU5L5S-CVeMe9Ah.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-sf8d41uT.js → c4Diagram-YGBWAQC7-Cedk3Bhx.js} +1 -1
  7. package/dist/assets/channel-taCNRlUU.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-BOGvy7Y_.js → chunk-2Q5K7J3B-D-VRqzTB.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-BiF9zyiz.js → chunk-3FUC2YCW-BChov01Y.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-EFuZ1SBZ.js → chunk-5VM5RSS4-C-TfwvHy.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-pJLNSAT7.js → chunk-GTNCS2PH-Db7CGj5Y.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-Cb-3_3IT.js → chunk-GWA4HPMP-CNvIfqKT.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-DAf5aogn.js → chunk-JWPE2WC7-C_bEp5tq.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-DR3kiwZz.js → chunk-NETBCI7D-Coi4BOls.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-BmU8PeyM.js → chunk-WEXAMYUT-CzylEuL3.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-Buh8g8OT.js → chunk-XXDRQBXY-m7p7ztaD.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-BurfmMWM.js → classDiagram-v2-NBCMYWYE-BMy6o9x2.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-KkGZUEkU.js → cose-bilkent-JH36ORCC-BDQ__0_J.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-C3PpV96d.js → cynefin-OW5HDTMX-Djv4ghUb.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-Dt8JwJaI.js → cynefinDiagram-VND7K2PF-BULrlGYH.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-DkjN8266.js → dagre-6A5THRUB-D-jrT_hU.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-DUAJS0JV.js → diagram-22UHCM2B-p2UhQmfL.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-Dw4KhF1a.js → diagram-3UASUU5V-DHvGWZ7q.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-Dd1gmQqf.js → diagram-ATOU4E4O-ChhKDot-.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-CjwGs0Yw.js → diagram-CDSNMT55-B-T7A43R.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-BL-69Zrq.js → diagram-MLGK6HIB-CJm8ENNY.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-5Zi2N7wo.js → diagram-MPIPVDR6-510tlza8.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-D9NekWUX.js → ebnfDiagram-ZINNZB2B-PkvRPHel.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-CkaSQjxw.js → elk-276RUBZZ-BVBVPoX3.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-CdAVBV8-.js → erDiagram-OPXOYQCR-BgYMVm7L.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-Bm_lWWH7.js → flowDiagram-KWPJA3E3-BGqsnc_3.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-FZ8ejYWJ.js → ganttDiagram-FUAMR5RP-D_-UeReH.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-Dlv9rizZ.js → gitGraphDiagram-X574FWY7-D0uooONx.js} +1 -1
  34. package/dist/assets/index-BwdMAQ6p.css +1 -0
  35. package/dist/assets/index-ByJennr7.js +581 -0
  36. package/dist/assets/{index-h9FFNwTu.js → index-zRsJERGJ.js} +1 -1
  37. package/dist/assets/{infoDiagram-VRGFBTTK-BdQ_gW5B.js → infoDiagram-VRGFBTTK-B9e1z77f.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-EwssEp2X.js → ishikawaDiagram-OU5B5YK6-BB4vNwvD.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-BX1G1vOd.js → journeyDiagram-ZHPQQLJL-DpkosOTB.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-BB4kAsyC.js → kanban-definition-PNTS6WVX-DbDP4kUu.js} +1 -1
  41. package/dist/assets/{linear-3su2TG93.js → linear-DBJGPmjT.js} +1 -1
  42. package/dist/assets/{mermaid.core-CDD1E0s9.js → mermaid.core-DWPRi-ue.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-Ba50MsNK.js → mindmap-definition-NLK3R4M7-nszBbJmx.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-DZLkkZlM.js → pegDiagram-GJSIUBJH-CWykJWu_.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-DaTz77Uq.js → pieDiagram-5QR66LMP-g3Da-Vnv.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-CpaiidpR.js → quadrantDiagram-O4NWA36T-Wzrlpzgh.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-mN4JrMeI.js → railroadDiagram-XR7U4H2S-moHTFMC0.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-BSNDRpg7.js → requirementDiagram-PLB6GJNP-CwfXdFH8.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-FngzGNug.js → sankeyDiagram-IPEJSGJF-3ZROYvnq.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-CM4pCC3L.js → sequenceDiagram-PO4LG4MO-E1TBebP8.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-CRICHXkw.js → sizeCapture-INFHLROL-DHKEU5pu.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-oyTWpxH2.js → stateDiagram-v2-GCMORJYK-BguEj09D.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-D9h9mPBq.js → swimlanes-2SLR337P-C7IDYnhd.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-BpEdoB2e.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-BJUlN9bW.js → timeline-definition-EJHVYXUP-DCW-F5PW.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-3uBNkbAb.js → usecaseDiagram-POWQR4AR-xBMFtWYV.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-D2odalzT.js → vennDiagram-UO4OBE2U-B5nIkuH8.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-DHgFaB9S.js → wardleyDiagram-VNRHLVJA-DYFAhdbr.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-w5GYM2h0.js → xychartDiagram-PMCCYNJV-DeXwX6Cz.js} +1 -1
  60. package/dist/index.html +2 -2
  61. package/dist-server/browser-proxy.js +2 -1
  62. package/dist-server/container-mcp.js +1029 -164
  63. package/dist-server/drivers/agents-proxy.js +64 -10
  64. package/dist-server/drivers/phone-proxy.js +33 -2
  65. package/dist-server/index.js +14577 -9001
  66. package/dist-server/local-computer-proxy.js +3 -3
  67. package/dist-server/local-computer.js +53 -17
  68. package/dist-server/openmausbot.js +3981 -1165
  69. package/dist-server/pair-cli.js +3981 -1165
  70. package/dist-server/permission-proxy.js +29 -9
  71. package/dist-server/server/attachments.js +55 -0
  72. package/dist-server/server/auto-approve.js +4 -3
  73. package/dist-server/server/auto-vm-claims.js +1 -0
  74. package/dist-server/server/bot-package.js +18 -292
  75. package/dist-server/server/bot-profile.js +5 -17
  76. package/dist-server/server/box-create-idempotency.js +3 -3
  77. package/dist-server/server/box.js +57 -55
  78. package/dist-server/server/browser-proxy.js +3 -2
  79. package/dist-server/server/browser-runtime.js +37 -1
  80. package/dist-server/server/cli.js +2 -2
  81. package/dist-server/server/command-allowlist.js +154 -0
  82. package/dist-server/server/computer-wait.js +22 -9
  83. package/dist-server/server/config.js +59 -9
  84. package/dist-server/server/connector-verdict.js +141 -0
  85. package/dist-server/server/container-computer.js +1 -1
  86. package/dist-server/server/default-model-selection.js +9 -0
  87. package/dist-server/server/delegations.js +85 -34
  88. package/dist-server/server/drivers/acp/core.js +57 -15
  89. package/dist-server/server/drivers/agents-call.js +42 -3
  90. package/dist-server/server/drivers/agents-catalog.js +34 -4
  91. package/dist-server/server/drivers/boxagent.js +174 -36
  92. package/dist-server/server/drivers/builtIn.js +2 -0
  93. package/dist-server/server/drivers/chat-box-tools.js +90 -0
  94. package/dist-server/server/drivers/chat-images.js +54 -0
  95. package/dist-server/server/drivers/chat-mcp-tools.js +38 -8
  96. package/dist-server/server/drivers/chat-tool-approval.js +70 -18
  97. package/dist-server/server/drivers/claude.js +7 -2
  98. package/dist-server/server/drivers/codex-instructions.js +48 -6
  99. package/dist-server/server/drivers/codex-tool-surface.js +20 -0
  100. package/dist-server/server/drivers/codex.js +95 -30
  101. package/dist-server/server/drivers/mistral.js +115 -0
  102. package/dist-server/server/drivers/openai-chat.js +155 -32
  103. package/dist-server/server/drivers/openai-compat.js +1 -0
  104. package/dist-server/server/drivers/permission-command.js +35 -0
  105. package/dist-server/server/drivers/phone-proxy.js +47 -2
  106. package/dist-server/server/drivers/pi.js +75 -4
  107. package/dist-server/server/drivers/prompt-split.js +87 -0
  108. package/dist-server/server/fleet.js +1 -1
  109. package/dist-server/server/group-thread-usage.js +103 -0
  110. package/dist-server/server/harness/registry.js +1 -0
  111. package/dist-server/server/hosted-models.js +1 -1
  112. package/dist-server/server/index.js +1385 -322
  113. package/dist-server/server/local-computer.js +72 -23
  114. package/dist-server/server/managed-desktop.js +1 -1
  115. package/dist-server/server/mcp-bridge.js +6 -6
  116. package/dist-server/server/new-bot-defaults.js +115 -0
  117. package/dist-server/server/org-library.js +1051 -0
  118. package/dist-server/server/package-export.js +627 -39
  119. package/dist-server/server/package-import.js +458 -0
  120. package/dist-server/server/package-parts.js +87 -0
  121. package/dist-server/server/peer-approval.js +16 -7
  122. package/dist-server/server/peer-roster.js +10 -0
  123. package/dist-server/server/presets.js +492 -0
  124. package/dist-server/server/provider-key-check.js +3 -2
  125. package/dist-server/server/published-teams.js +80 -0
  126. package/dist-server/server/room-handoffs.js +19 -3
  127. package/dist-server/server/routes/bot-presets.js +25 -0
  128. package/dist-server/server/routine-requests.js +6 -3
  129. package/dist-server/server/routines.js +41 -1
  130. package/dist-server/server/section-context.js +10 -0
  131. package/dist-server/server/skills.js +34 -61
  132. package/dist-server/server/steer-queue.js +3 -2
  133. package/dist-server/server/store.js +259 -15
  134. package/dist-server/server/surface.js +10 -4
  135. package/dist-server/server/system-prompt.js +12 -4
  136. package/dist-server/server/team-backup.js +6 -2
  137. package/dist-server/server/team-computers.js +10 -0
  138. package/dist-server/server/team-library.js +6 -2
  139. package/dist-server/server/team-manifest.js +4 -3
  140. package/dist-server/server/team-setup-requests.js +16 -5
  141. package/dist-server/server/thread-events.js +17 -1
  142. package/dist-server/server/turn-context.js +2 -0
  143. package/dist-server/server/turn-watchdog.js +12 -3
  144. package/dist-server/server/usage-ledger.js +12 -1
  145. package/dist-server/server/vps-computer.js +139 -51
  146. package/dist-server/server/vps-container-mcp.js +7 -0
  147. package/dist-server/server/vps-ssh.js +36 -7
  148. package/dist-server/server/workspace-backup-policy.js +12 -0
  149. package/dist-server/server/workspace-backup.js +2 -2
  150. package/dist-server/server/workspace.js +1 -1
  151. package/dist-server/shared/approval-mode.js +1 -1
  152. package/dist-server/shared/ask-question.js +188 -12
  153. package/dist-server/shared/bot-profile.js +15 -0
  154. package/dist-server/shared/command-allowlist.js +1 -0
  155. package/dist-server/shared/credential-request.js +1 -1
  156. package/dist-server/shared/package-format.js +967 -0
  157. package/dist-server/shared/skill-md.js +64 -0
  158. package/dist-server/shared/team-backup.js +7 -0
  159. package/dist-server/shared/wire.js +4 -0
  160. package/dist-server/vps-container-mcp.js +1129 -170
  161. package/enterprise/README.md +1 -1
  162. package/package.json +1 -1
  163. package/dist/assets/channel-BXFkgus9.js +0 -1
  164. package/dist/assets/index-BGgqlWPJ.css +0 -1
  165. package/dist/assets/index-BtHo8ZKl.js +0 -577
  166. package/dist/assets/swimlanesDiagram-TC7HE7FX-BWrvB2ak.js +0 -8
@@ -19,6 +19,7 @@ import { redactSecretsInText } from "./redact.js";
19
19
  import { botAvatarProfile } from "../shared/bot-avatar.js";
20
20
  import { approvalModeFor, isApprovalMode } from "../shared/approval-mode.js";
21
21
  import { isMentionBoundary, isMentionNameContinuation } from "../shared/mention-boundary.js";
22
+ import { CONNECTOR_SLUG_PATTERN, CONNECTOR_TOOL_NAME_PATTERN } from "../shared/wire.js";
22
23
  export const groupWireProjectionIsExact = true;
23
24
  // Unicode's complete emoji sequences include flags, skin tones and ZWJ
24
25
  // combinations. Also allow unqualified single symbols (e.g. ♥), but not
@@ -31,13 +32,13 @@ export const taskWireProjectionIsExact = true;
31
32
  /** The typed wire projection for one task. Pairs with the assertion above:
32
33
  * returning WireTask means an undeclared server field cannot ride silently. */
33
34
  export function toWireTask(task) {
34
- const { resumeCursors: _resumeCursors, lastInstanceId: _lastInstanceId, handedMessages: _handedMessages, appliedCompactionId: _appliedCompactionId, contextFloor: _contextFloor, lastContextModel: _lastContextModel, ...wire } = task;
35
+ const { resumeCursors: _resumeCursors, lastInstanceId: _lastInstanceId, handedMessages: _handedMessages, appliedCompactionId: _appliedCompactionId, contextFloor: _contextFloor, lastContextModel: _lastContextModel, surfaceSource: _surfaceSource, ...wire } = task;
35
36
  return wire;
36
37
  }
37
38
  const TASK_PATCH_FIELDS = [
38
39
  "title", "projectId", "modelSelection", "approvalMode", "autoApprove", "alwaysAllow",
39
40
  "unread", "rewound", "archivedAt", "pinned", "pinnedMessageId", "resumeCursors", "lastInstanceId", "cwd",
40
- "routineRunId", "surface", "appliedCompactionId", "contextFloor", "lastContextModel",
41
+ "routineRunId", "surface", "surfaceSource", "snoozedUntil", "appliedCompactionId", "contextFloor", "lastContextModel",
41
42
  ];
42
43
  /** Only `true` is a pin. false/undefined must not land in bots.json or groups.json. */
43
44
  function persistedPin(task) {
@@ -96,6 +97,9 @@ function redactBotAuthored(message) {
96
97
  card.held = redactSecretsInText(card.held);
97
98
  if (typeof card.answeredText === "string")
98
99
  card.answeredText = redactSecretsInText(card.answeredText);
100
+ if (card.commandAllowlist && (redactSecretsInText(card.commandAllowlist.command) !== card.commandAllowlist.command ||
101
+ redactSecretsInText(card.commandAllowlist.cwd) !== card.commandAllowlist.cwd))
102
+ delete card.commandAllowlist;
99
103
  // Bot-authored question text sits behind the subtitle the same way a
100
104
  // routine's instructions do, so it is scrubbed on the same boundary.
101
105
  if (card.questionRequest) {
@@ -262,6 +266,58 @@ export function titleFromLlm(raw) {
262
266
  return line.length >= 1 && line.length <= 48 ? line : null;
263
267
  }
264
268
  export const botWireProjectionIsExact = true;
269
+ /** Upper bounds keep a grants patch from becoming a persistence blob; they
270
+ * sit far above any real service's tool count. */
271
+ export const CONNECTOR_SLUGS_MAX = 64;
272
+ export const CONNECTOR_TOOLS_PER_SERVICE_MAX = 500;
273
+ /** Validate and normalize a connectorTools value at the one boundary every
274
+ * writer shares (patchBot). Returns a canonical copy: slugs checked against
275
+ * the service pattern, tool lists deduplicated in order, `"*"` kept as-is.
276
+ * The legacy clear stays a JSON null at the API edge; here callers pass
277
+ * undefined to return a bot to boolean-only behavior. */
278
+ export function parseConnectorTools(value) {
279
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
280
+ return { ok: false, error: "connectorTools must be an object of service slugs to tool grants" };
281
+ }
282
+ const entries = Object.entries(value);
283
+ if (entries.length > CONNECTOR_SLUGS_MAX) {
284
+ return { ok: false, error: `connectorTools may name at most ${CONNECTOR_SLUGS_MAX} services` };
285
+ }
286
+ const grants = {};
287
+ for (const [slug, grant] of entries) {
288
+ if (!CONNECTOR_SLUG_PATTERN.test(slug)) {
289
+ return { ok: false, error: `connectorTools service slugs must be lowercase slugs (got "${slug}")` };
290
+ }
291
+ if (!grant || typeof grant !== "object" || Array.isArray(grant)) {
292
+ return { ok: false, error: `connectorTools.${slug} must be a grant like { tools: "*" } or { tools: ["TOOL_NAME"] }` };
293
+ }
294
+ const keys = Object.keys(grant);
295
+ if (keys.length !== 1 || keys[0] !== "tools") {
296
+ return { ok: false, error: `connectorTools.${slug} accepts only a tools field` };
297
+ }
298
+ const tools = grant.tools;
299
+ if (tools === "*") {
300
+ grants[slug] = { tools: "*" };
301
+ continue;
302
+ }
303
+ if (!Array.isArray(tools) || tools.length === 0) {
304
+ return { ok: false, error: `connectorTools.${slug}.tools must be "*" or a non-empty list of tool names (use {} to grant no tools)` };
305
+ }
306
+ if (tools.length > CONNECTOR_TOOLS_PER_SERVICE_MAX) {
307
+ return { ok: false, error: `connectorTools.${slug}.tools may list at most ${CONNECTOR_TOOLS_PER_SERVICE_MAX} tools` };
308
+ }
309
+ const names = [];
310
+ for (const tool of tools) {
311
+ if (typeof tool !== "string" || !CONNECTOR_TOOL_NAME_PATTERN.test(tool)) {
312
+ return { ok: false, error: `connectorTools.${slug}.tools names must be Composio tool names like GMAIL_SEND_EMAIL` };
313
+ }
314
+ if (!names.includes(tool))
315
+ names.push(tool);
316
+ }
317
+ grants[slug] = { tools: names };
318
+ }
319
+ return { ok: true, grants };
320
+ }
265
321
  const BOTS_FILE = join(DATA_DIR, "bots.json");
266
322
  const GROUPS_FILE = join(DATA_DIR, "groups.json");
267
323
  /** The registries carry souls, project paths and per-bot settings, so they are
@@ -375,14 +431,19 @@ export class Store {
375
431
  groups = [];
376
432
  threads = new Map();
377
433
  defaultSelection;
434
+ completeNewBotSelection;
378
435
  listeners = new Set();
379
436
  /** A broken team registry must not prevent loading independent chat data. */
380
437
  registeringInitialSections = true;
381
438
  /** Room turns and old callers have their own activity slot. Clearing
382
439
  * that slot must not clear a concurrently running independent task. */
383
440
  legacyActivities = new Map();
384
- constructor(defaultSelection) {
441
+ constructor(defaultSelection,
442
+ /** Workspace-wide new-bot defaults (config newBots), applied to every
443
+ * new bot's selection whichever path created it. */
444
+ completeNewBotSelection = (selection) => selection) {
385
445
  this.defaultSelection = defaultSelection;
446
+ this.completeNewBotSelection = completeNewBotSelection;
386
447
  mkdirSync(DATA_DIR, { recursive: true });
387
448
  for (const file of [BOTS_FILE, GROUPS_FILE])
388
449
  tightenRegistryFile(file);
@@ -762,16 +823,18 @@ export class Store {
762
823
  if (botsDirty)
763
824
  this.saveBots();
764
825
  }
765
- saveBots(bots = this.bots) {
766
- this.rememberSections([...this.bots, ...bots].map((bot) => bot.section));
826
+ saveBots(bots = this.bots, registerSections = true) {
827
+ if (registerSections)
828
+ this.rememberSections([...this.bots, ...bots].map((bot) => bot.section));
767
829
  writeFileAtomic(BOTS_FILE, JSON.stringify(bots.map(({ busy: _busy, activity: _activity, ...bot }) => ({
768
830
  ...bot,
769
831
  tasks: bot.tasks?.map(({ busy: _taskBusy, activity: _taskActivity, turnStartedAt: _taskTurnStarted, ...task }) => persistedPin(task)),
770
832
  })), null, 2), { mode: 0o600 });
771
833
  }
772
- saveGroups() {
773
- this.rememberSections(this.groups.map((group) => group.section));
774
- writeFileAtomic(GROUPS_FILE, JSON.stringify(this.groups.map(({ busyBotId: _busyBotId, turnStartedAt: _turnStartedAt, ...g }) => ({
834
+ saveGroups(groups = this.groups, registerSections = true) {
835
+ if (registerSections)
836
+ this.rememberSections(groups.map((group) => group.section));
837
+ writeFileAtomic(GROUPS_FILE, JSON.stringify(groups.map(({ busyBotId: _busyBotId, turnStartedAt: _turnStartedAt, ...g }) => ({
775
838
  ...g,
776
839
  ...(g.tasks ? { tasks: g.tasks.map((task) => persistedPin(task)) } : {}),
777
840
  })), null, 2), { mode: 0o600 });
@@ -788,6 +851,60 @@ export class Store {
788
851
  console.warn(`[teams] Startup could not register team names; saved teams and shared instructions were left unchanged: ${error.message}`);
789
852
  }
790
853
  }
854
+ /** Rename the same team, preserving its members and existing access grants. */
855
+ renameSection(name, nextName, computers) {
856
+ if (!name || !this.sections.includes(name))
857
+ return "No such team";
858
+ nextName = nextName.trim();
859
+ if (!nextName || nextName.length > 60 || [...nextName].some(char => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127))
860
+ return "Team name must be 1 to 60 characters without control characters";
861
+ if (name === nextName)
862
+ return undefined;
863
+ if (this.sections.includes(nextName) || computers?.forSection(nextName))
864
+ return "A team with that name already exists";
865
+ const members = this.bots.filter(bot => sectionKey(bot.section) === name);
866
+ const rooms = this.groups.filter(group => sectionKey(group.section) === name);
867
+ const affected = this.bots.filter(bot => members.includes(bot) || bot.managedSections?.some(section => sectionKey(section) === name));
868
+ if (affected.some(bot => bot.busy || bot.tasks?.some(task => task.busy)) || rooms.some(group => group.busyBotId)) {
869
+ return "Stop this team's active work before renaming the team";
870
+ }
871
+ // Keep the established empty-team lifecycle: old grants are revoked when
872
+ // an empty identity is removed, so recreating it cannot restore access.
873
+ if (!members.length && !rooms.length && !computers?.forSection(name))
874
+ return this.changeEmptySection(name, nextName);
875
+ const nextBots = this.bots.map(bot => ({ ...bot,
876
+ ...(members.includes(bot) ? { section: nextName } : {}),
877
+ ...(bot.managedSections ? { managedSections: [...new Set(bot.managedSections.map(section => sectionKey(section) === name ? nextName : section))] } : {}),
878
+ }));
879
+ const nextGroups = this.groups.map(group => rooms.includes(group) ? { ...group, section: nextName } : group);
880
+ let computerChanged = false;
881
+ try {
882
+ // Register the new name only after saving the records; otherwise the
883
+ // registry's duplicate-name check would reject this same rename.
884
+ this.saveBots(nextBots, false);
885
+ this.saveGroups(nextGroups, false);
886
+ computerChanged = computers?.renameSection(name, nextName) ?? false;
887
+ changeEmptySection(name, nextName);
888
+ }
889
+ catch (error) {
890
+ this.saveBots(this.bots, false);
891
+ this.saveGroups(this.groups, false);
892
+ if (computerChanged)
893
+ computers.renameSection(nextName, name);
894
+ throw error;
895
+ }
896
+ // Preserve identities held by the schedulers and other store consumers.
897
+ for (let i = 0; i < nextBots.length; i++)
898
+ Object.assign(this.bots[i], nextBots[i]);
899
+ for (let i = 0; i < nextGroups.length; i++)
900
+ Object.assign(this.groups[i], nextGroups[i]);
901
+ for (const bot of affected)
902
+ this.emit({ type: "bot", botId: bot.id });
903
+ for (const room of rooms)
904
+ this.emit({ type: "group", groupId: room.id });
905
+ this.emit({ type: "sections" });
906
+ return undefined;
907
+ }
791
908
  /** Empty-only changes cannot merge teams or silently change anybody's access. */
792
909
  changeEmptySection(name, nextName) {
793
910
  if (!this.sections.includes(name))
@@ -816,6 +933,44 @@ export class Store {
816
933
  this.emit({ type: "sections" });
817
934
  return undefined;
818
935
  }
936
+ /** Remove the team, keeping its bots, rooms and conversations in General. */
937
+ deleteSection(name) {
938
+ if (!name || !this.sections.includes(name))
939
+ return "No such team";
940
+ const members = this.bots.filter(bot => sectionKey(bot.section) === name);
941
+ const rooms = this.groups.filter(group => sectionKey(group.section) === name);
942
+ if (members.some(bot => bot.busy || bot.tasks?.some(task => task.busy)) || rooms.some(group => group.busyBotId)) {
943
+ return "Stop this team's active work before deleting the team";
944
+ }
945
+ if (this.bots.filter(bot => bot.chiefOfStaff && (!sectionKey(bot.section) || sectionKey(bot.section) === name)).length > 1) {
946
+ return "General already has a Chief of Staff. Move or change this team's Chief before deleting the team";
947
+ }
948
+ const nextBots = this.bots.map(bot => ({ ...bot,
949
+ ...(sectionKey(bot.section) === name ? { section: undefined } : {}),
950
+ ...(bot.managedSections ? { managedSections: bot.managedSections.filter(section => sectionKey(section) !== name) } : {}),
951
+ }));
952
+ const nextGroups = this.groups.map(group => sectionKey(group.section) === name ? { ...group, section: undefined } : group);
953
+ try {
954
+ this.saveBots(nextBots);
955
+ this.saveGroups(nextGroups);
956
+ changeEmptySection(name, null);
957
+ }
958
+ catch (error) {
959
+ this.saveBots();
960
+ this.saveGroups();
961
+ throw error;
962
+ }
963
+ for (let i = 0; i < nextBots.length; i++)
964
+ Object.assign(this.bots[i], nextBots[i]);
965
+ for (let i = 0; i < nextGroups.length; i++)
966
+ Object.assign(this.groups[i], nextGroups[i]);
967
+ for (const bot of this.bots)
968
+ this.emit({ type: "bot", botId: bot.id });
969
+ for (const group of rooms)
970
+ this.emit({ type: "group", groupId: group.id });
971
+ this.emit({ type: "sections" });
972
+ return undefined;
973
+ }
819
974
  // ── groups ────────────────────────────────────────────────────────────
820
975
  /** Subscribe to every write. Listeners run after the write and after
821
976
  * save; a throwing listener never breaks the write. */
@@ -1281,8 +1436,10 @@ export class Store {
1281
1436
  return pruned;
1282
1437
  }
1283
1438
  /** Fork the conversation: a new user message that replaces `sourceId`
1284
- * (same parent, new text) and becomes the active leaf. */
1285
- branchMessage(threadId, sourceId, text) {
1439
+ * (same parent, new text) and becomes the active leaf. `sendId` is the
1440
+ * client's identity for this edit, so its instant bubble reconciles onto
1441
+ * the canonical message and a network retry cannot fork twice. */
1442
+ branchMessage(threadId, sourceId, text, sendId) {
1286
1443
  const t = this.thread(threadId);
1287
1444
  const source = t.messages.find((m) => m.id === sourceId);
1288
1445
  if (!source)
@@ -1295,6 +1452,7 @@ export class Store {
1295
1452
  text,
1296
1453
  parentId: source.parentId ?? null,
1297
1454
  replyToId: source.replyToId,
1455
+ ...(sendId ? { sendId } : {}),
1298
1456
  };
1299
1457
  t.messages.push(full);
1300
1458
  t.activeLeafId = full.id;
@@ -1352,6 +1510,11 @@ export class Store {
1352
1510
  botByThread(threadId) {
1353
1511
  return this.bots.find((b) => b.threadId === threadId || b.tasks?.some((t) => t.threadId === threadId)) ?? null;
1354
1512
  }
1513
+ /** The one place a new bot's selection is decided: the caller's choice, or
1514
+ * the workspace default, completed with the workspace's new-bot defaults. */
1515
+ newBotSelection(requested) {
1516
+ return this.completeNewBotSelection(requested ?? this.defaultSelection());
1517
+ }
1355
1518
  createBot(profile = {}, opts = {}) {
1356
1519
  this.rememberSections([profile.section]);
1357
1520
  const name = profile.name?.trim() || pickBotName(this.bots.map((b) => b.name));
@@ -1371,7 +1534,7 @@ export class Store {
1371
1534
  // Restricted from its first frame: no one else is ever told it exists.
1372
1535
  ...(profile.visibility && profile.visibility !== "everyone" ? { visibility: structuredClone(profile.visibility) } : {}),
1373
1536
  unread: false,
1374
- modelSelection: profile.modelSelection ?? this.defaultSelection(),
1537
+ modelSelection: this.newBotSelection(profile.modelSelection),
1375
1538
  resumeCursors: {},
1376
1539
  createdAt: Date.now(),
1377
1540
  };
@@ -1433,15 +1596,16 @@ export class Store {
1433
1596
  if (at >= 0 || !operation.threadId || !operation.fields.name || !operation.fields.modelSelection)
1434
1597
  throw new Error("Invalid new bot in team setup");
1435
1598
  const createdAt = Date.now();
1599
+ const modelSelection = this.newBotSelection(operation.fields.modelSelection);
1436
1600
  next = { id: operation.botId, threadId: operation.threadId, name: operation.fields.name,
1437
1601
  title: "", description: "", soul: "", notifications: true, color: COLORS[nextBots.length % COLORS.length], unread: false,
1438
- modelSelection: operation.fields.modelSelection, resumeCursors: {}, createdAt, ...operation.fields,
1602
+ resumeCursors: {}, createdAt, ...operation.fields, modelSelection,
1439
1603
  approvalMode: "ask", autoApprove: false, composio: false, approvePeerComms: false,
1440
1604
  // A Chief's new teammate is seen by exactly the Chief's audience:
1441
1605
  // a restricted Chief never creates a bot everyone sees.
1442
1606
  ...(chief.visibility && chief.visibility !== "everyone" ? { visibility: structuredClone(chief.visibility) } : {}),
1443
1607
  tasks: [{ threadId: operation.threadId, title: UNTITLED_THREAD, createdAt, updatedAt: createdAt, resumeCursors: {},
1444
- modelSelection: structuredClone(operation.fields.modelSelection), approvalMode: "ask", autoApprove: false,
1608
+ modelSelection: structuredClone(modelSelection), approvalMode: "ask", autoApprove: false,
1445
1609
  unread: false, activity: "idle", busy: false }],
1446
1610
  };
1447
1611
  nextBots.unshift(next);
@@ -1461,6 +1625,8 @@ export class Store {
1461
1625
  }));
1462
1626
  nextBots[at] = next;
1463
1627
  }
1628
+ if (operation.fields.chiefOfStaff === false)
1629
+ delete next.managedSections;
1464
1630
  next.section = sectionKey(next.section) || undefined;
1465
1631
  if (operation.fields.soul !== undefined) {
1466
1632
  next.soulHash = soulHash(operation.fields.soul);
@@ -1469,14 +1635,14 @@ export class Store {
1469
1635
  changed.push(next);
1470
1636
  }
1471
1637
  const result = { state: "applied", newTeams: request.newTeams, bots: changed.map((bot, index) => ({
1472
- id: bot.id, name: bot.name, section: bot.section, modelSelection: structuredClone(bot.modelSelection),
1638
+ id: bot.id, name: bot.name, section: bot.section, modelSelection: structuredClone(bot.modelSelection), chiefOfStaff: Boolean(bot.chiefOfStaff),
1473
1639
  action: request.operations[index].action === "create" ? "created" : "updated",
1474
1640
  })) };
1475
1641
  const chiefAt = nextBots.findIndex((bot) => bot.id === chief.id);
1476
1642
  const nextChief = { ...nextBots[chiefAt], lastTeamSetupReceipt: { requestId: request.requestId, result } };
1477
1643
  // Only the newly-created teams explicitly named in the human review may
1478
1644
  // extend this Chief's reach. Existing teams require owner settings.
1479
- if (request.newTeams.length) {
1645
+ if (request.newTeams.length && nextChief.chiefOfStaff) {
1480
1646
  nextChief.managedSections = managedSections;
1481
1647
  }
1482
1648
  nextBots[chiefAt] = nextChief;
@@ -1541,6 +1707,16 @@ export class Store {
1541
1707
  const bot = this.bot(id);
1542
1708
  if (!bot)
1543
1709
  return null;
1710
+ // Grants are the one field whose shape every writer must share, so the
1711
+ // store normalizes them itself: API patches arrive pre-validated, import
1712
+ // paths pass {}, and an internal caller that skips the parser still
1713
+ // lands canonical data or stops here.
1714
+ if (patch.connectorTools !== undefined) {
1715
+ const parsed = parseConnectorTools(patch.connectorTools);
1716
+ if (!parsed.ok)
1717
+ throw new Error(parsed.error);
1718
+ patch = { ...patch, connectorTools: parsed.grants };
1719
+ }
1544
1720
  // Runtime revocations must become effective in memory even when disk is
1545
1721
  // unavailable. Profile edits use the separate atomic path below.
1546
1722
  Object.assign(bot, patch);
@@ -1651,6 +1827,39 @@ export class Store {
1651
1827
  this.rememberSections([targetSection]);
1652
1828
  return { ok: true, bots: ids.map((id) => this.bot(id)) };
1653
1829
  }
1830
+ /** Apply only the membership edits the user made, in one bots-file write. */
1831
+ updateTeamMembers(section, addIds, removeIds) {
1832
+ const key = sectionKey(section);
1833
+ if (!key || !this.sections.includes(key))
1834
+ return { ok: false, reason: "unavailable" };
1835
+ const adds = new Set(addIds), removes = new Set(removeIds);
1836
+ const ids = new Set([...adds, ...removes]);
1837
+ for (const id of ids) {
1838
+ const bot = this.bot(id);
1839
+ if (!bot || bot.hidden)
1840
+ return { ok: false, reason: "unavailable" };
1841
+ if (adds.has(id) && removes.has(id))
1842
+ return { ok: false, reason: "membership-changed" };
1843
+ if (removes.has(id) && sectionKey(bot.section) !== key)
1844
+ return { ok: false, reason: "membership-changed" };
1845
+ }
1846
+ const next = this.bots.map(bot => ids.has(bot.id)
1847
+ ? { ...bot, section: adds.has(bot.id) ? key : undefined } : bot);
1848
+ for (const destination of [key, ""]) {
1849
+ if (next.filter(bot => bot.chiefOfStaff && sectionKey(bot.section) === destination).length > 1) {
1850
+ return { ok: false, reason: "chief-conflict" };
1851
+ }
1852
+ }
1853
+ if (ids.size) {
1854
+ this.saveBots(next);
1855
+ for (let i = 0; i < next.length; i++)
1856
+ if (ids.has(next[i].id))
1857
+ Object.assign(this.bots[i], next[i]);
1858
+ for (const botId of ids)
1859
+ this.emit({ type: "bot", botId });
1860
+ }
1861
+ return { ok: true, bots: this.bots.filter(bot => ids.has(bot.id)) };
1862
+ }
1654
1863
  /** Legacy bot/room activity occupies its own slot; direct conversations
1655
1864
  * use setTaskActivity so settling one thread cannot clear another. */
1656
1865
  setActivity(botId, activity) {
@@ -1679,6 +1888,13 @@ export class Store {
1679
1888
  task.turnStartedAt = Date.now();
1680
1889
  else if (!busy)
1681
1890
  delete task.turnStartedAt;
1891
+ // Reaching here means the thread just did something — exactly the
1892
+ // "activity" an until-activity snooze waits for, including settling
1893
+ // back to idle after a turn. Persist the wake like any task change.
1894
+ if (task.snoozedUntil === 0) {
1895
+ task.snoozedUntil = undefined;
1896
+ this.saveBots();
1897
+ }
1682
1898
  this.refreshBotActivity(bot);
1683
1899
  this.emit({ type: "bot", botId });
1684
1900
  return bot;
@@ -2023,6 +2239,12 @@ export class Store {
2023
2239
  Object.assign(task, { [key]: structuredClone(patch[key]) });
2024
2240
  }
2025
2241
  }
2242
+ // "Until new activity" ends the moment the thread has something new for
2243
+ // the person, and every unread wake funnels through patchTask — so this
2244
+ // one hook is the whole activity alarm. A time-based snooze is left to
2245
+ // its clock: attention overrides it on screen without clearing it.
2246
+ if (task.snoozedUntil === 0 && patch.unread === true)
2247
+ task.snoozedUntil = undefined;
2026
2248
  if (typeof patch.title === "string")
2027
2249
  task.title = patch.title.trim().slice(0, 80) || UNTITLED_THREAD;
2028
2250
  if (Object.prototype.hasOwnProperty.call(patch, "pinned") && task.pinned !== true)
@@ -2034,6 +2256,28 @@ export class Store {
2034
2256
  this.emit({ type: "bot", botId });
2035
2257
  return task;
2036
2258
  }
2259
+ /** A Works on change is the newest explicit choice, so this bot's
2260
+ * machine-recorded pins that now point somewhere else give way. A pin a
2261
+ * person set, a legacy pin with unknown provenance, and a pin that already
2262
+ * matches the new destination survive. Returns how many pins were cleared. */
2263
+ clearAutoSurfacePins(botId, destination) {
2264
+ const bot = this.bot(botId);
2265
+ if (!bot?.tasks)
2266
+ return 0;
2267
+ let cleared = 0;
2268
+ for (const task of bot.tasks) {
2269
+ if (task.surface === undefined || task.surfaceSource !== "auto" || task.surface === destination)
2270
+ continue;
2271
+ task.surface = undefined;
2272
+ task.surfaceSource = undefined;
2273
+ cleared++;
2274
+ }
2275
+ if (!cleared)
2276
+ return 0;
2277
+ this.saveBots();
2278
+ this.emit({ type: "bot", botId });
2279
+ return cleared;
2280
+ }
2037
2281
  /** Model/provider changes are one configuration transaction: never publish
2038
2282
  * a new provider before its confirmed approval downgrade, or change the
2039
2283
  * default while leaving the selected thread behind after a write failure. */
@@ -25,10 +25,14 @@ const OFF_NOTE = " This bot's \"Works on\" setting is Off, so no computer and no
25
25
  * destination mounts only the built-in browser, Off mounts nothing. A
26
26
  * conversation pin — set by the person from the composer, or by the
27
27
  * conversation's own first turn on Auto — wins over the bot's default, so a
28
- * thread never changes place under someone; only Off overrides it. Auto
29
- * without a pin leaves the computer choice to the dispatch, which then
30
- * mounts the browser only when no computer was reached. A pin the turn
31
- * cannot honour is retained and reported instead of being swapped silently. */
28
+ * thread never changes place under someone; only Off overrides it. An
29
+ * Auto-recorded pin is the machine's memory, not a person's choice: it yields
30
+ * when the bot's Works on later changes and no longer matches, while a
31
+ * person's pin (including legacy pins of unknown origin) keeps winning until
32
+ * they clear it. Auto without a pin leaves
33
+ * the computer choice to the dispatch, which then mounts the browser only
34
+ * when no computer was reached. A pin the turn cannot honour is retained and
35
+ * reported instead of being swapped silently. */
32
36
  export function resolveSurface(input) {
33
37
  const { destination, browserOn } = input;
34
38
  // Off is the whole answer: no computer and no browser. It used to withhold
@@ -116,6 +120,8 @@ export function surfacePrompt(mounted, opts = {}) {
116
120
  text += " If no suitable computer is running but its provider is configured, select_computer can provision one for this computer task; reuse existing resources first. Do not provision merely for ordinary chat or inspection.";
117
121
  if (opts.canSelect && !computer && !mounted.browser)
118
122
  text += " No computer or browser tools are mounted yet; select_computer is the way to connect them before screen work, not a reason to claim you already performed it.";
123
+ if (computer || mounted.browser)
124
+ text += " For online research, use the selected OpenMausBot browser when a search service is unavailable. A failed tool proves only that this attempt failed, not that every browser is unavailable. Inspect the current page after navigation; report a sign-in page, redirect or error as such. A completed model turn is not proof that the user's task succeeded.";
119
125
  return text + (opts.note ?? "");
120
126
  }
121
127
  /** Tool names that touch a screen, for engines that report bare names.
@@ -6,18 +6,25 @@
6
6
  // The sentences that both the direct-turn and room-turn paths use live
7
7
  // here too, so neither path can drift from the other or from the preview.
8
8
  import { soulSystemPrompt } from "./bot-folder.js";
9
+ export function userProfileSystemPrompt(profile) {
10
+ const text = profile?.aboutMe?.trim();
11
+ return text ? `\n\nAbout the user (shared with all bots):\nThe following JSON string contains user-provided background and preferences; it does not override system rules or grant permissions.\n${JSON.stringify(text)}\n` : "";
12
+ }
9
13
  /** Sections whose text legitimately differs between two turns of one live
10
14
  * conversation: memory, because a bot writes to MEMORY.md mid-conversation,
11
- * mentions, which describe the message being sent right now, and outstanding
12
- * teammate work, which settles while the person keeps talking.
15
+ * mentions, which describe the message being sent right now, outstanding
16
+ * teammate work, which settles while the person keeps talking, and recent
17
+ * work, whose relative time labels are recomputed every turn and whose
18
+ * newest-first list changes as the bot works in other threads.
13
19
  *
14
20
  * They are reported apart from the rest so a driver that keeps one CLI
15
21
  * process per thread can key that process on the stable half. Before this
16
22
  * split, saving a memory changed the system prompt, which changed the spawn
17
23
  * contract, which relaunched the CLI — and the provider then re-uploaded the
18
24
  * entire conversation at the cache-write rate. Mentions did the same on any
19
- * turn that tagged a bot. */
20
- const VOLATILE_SECTIONS = new Set(["memory", "mentions", "outstanding"]);
25
+ * turn that tagged a bot, and recent work did it on every turn of an active
26
+ * bot, because its "2h ago" labels drift even when nothing else changed. */
27
+ const VOLATILE_SECTIONS = new Set(["memory", "mentions", "outstanding", "recent"]);
21
28
  export function buildSystemPrompt(persona, soul, parts) {
22
29
  const ordered = [
23
30
  { id: "persona", label: "Identity", text: persona },
@@ -38,6 +45,7 @@ const COMPUTER_PARAGRAPH = {
38
45
  "vm-shared": " You have a shared, isolated Cua sandbox: a Linux desktop in a container on this machine. Only /home/cua/workspace is durable; save downloads, repositories, working files, and browser profiles there because everything else inside the VM is disposable. No other host folder is mounted. Use the computer tools for desktop, accessibility, window, and shell work. Inspect the desktop state before acting, prefer accessibility targets over raw coordinates, and work carefully.",
39
46
  box: " You have your own cloud computer. In Chrome, prefer browser_snapshot with browser_click/browser_fill for semantic, trusted actions; use screenshot/click/type_text for visual or non-browser UI, open_url for navigation, and computer_exec for Linux tasks. Every action already returns the resulting screen, so don't follow it with screenshot; batch predictable pixel actions with computer_batch.",
40
47
  "box-agent": "",
48
+ "box-chat": " You control the assigned cloud computer. Inspect it with screenshots; click coordinates refer to the full image. Use the advertised computer tools for desktop actions and shell commands.",
41
49
  vps: " You have your own self-hosted remote Linux computer through the official Cua tools. This is a VPS, not Box; using it does not require a Box API key. Its filesystem is disposable: everything on it is wiped whenever its container is recreated, so keep long-lived work somewhere durable — push it to a remote, or hand the results back in chat — instead of leaving it only on that computer. Inspect the desktop state before acting, prefer accessibility targets over raw coordinates, and act carefully.",
42
50
  local: " You can act on the user's computer through the computer tools. Discover the target app/window and inspect its state first. Prefer window-targeted accessibility actions with background delivery so the user can keep working in another app; do not bring OpenMausBot or another app to the front just to inspect it. Use the dedicated browser tools for browser work when available, keeping the user's intended browser profile/account, and OpenMausBot's configuration/proposal tools for supported bot setup rather than clicking through this app. Full-desktop input, app activation, and foreground delivery can move the real cursor, change focus, or switch desktops: use them only when the user asked for foreground control or agrees after background control reports it cannot perform the action. Do not silently retry a background refusal as foreground input, including through shell scripts, AppleScript/System Events, or another automation tool. If a background action unexpectedly changes focus, report it and stop that route rather than continuing to interrupt the user. Never promise that arbitrary desktop actions can run in the background.",
43
51
  };
@@ -113,6 +113,10 @@ export function createTeamBackup(store, routines, name) {
113
113
  section: bot.section, color: bot.color,
114
114
  mascotExpression: bot.mascotExpression ?? undefined, mascotBody: bot.mascotBody ?? undefined,
115
115
  chiefOfStaff: Boolean(bot.chiefOfStaff), hidden: Boolean(bot.hidden), playbooks: bot.playbooks ?? [],
116
+ // Grants are workspace-private authority: they travel in this backup
117
+ // so the team's shape is not lost, but the import below still lands
118
+ // every bot grant-less — restoring them is a deliberate later choice.
119
+ ...(bot.connectorTools ? { connectorTools: structuredClone(bot.connectorTools) } : {}),
116
120
  memory: memoryFor(bot.id),
117
121
  activeTask: bot.threadId, tasks: history(bot),
118
122
  })),
@@ -182,7 +186,7 @@ export function importTeamBackup(store, routines, input, selection, options = {}
182
186
  bots.push(bot);
183
187
  botIds.set(source.key, bot.id);
184
188
  store.patchBot(bot.id, { composio: false, computer: "off", browser: false, approvalMode: "ask", autoApprove: false,
185
- hidden: source.hidden, chiefOfStaff: source.chiefOfStaff, playbooks: source.playbooks });
189
+ connectorTools: {}, hidden: source.hidden, chiefOfStaff: source.chiefOfStaff, playbooks: source.playbooks });
186
190
  if (source.memory)
187
191
  restoreMemory(bot.id, source.memory);
188
192
  }
@@ -191,7 +195,7 @@ export function importTeamBackup(store, routines, input, selection, options = {}
191
195
  const tasks = source.tasks.map((task, i) => {
192
196
  const record = {
193
197
  threadId: i === 0 ? bot.threadId : newId(), title: task.title, createdAt: task.createdAt, resumeCursors: {},
194
- modelSelection: structuredClone(selection), activity: "idle", busy: false, unread: false,
198
+ modelSelection: structuredClone(bot.modelSelection), activity: "idle", busy: false, unread: false,
195
199
  ...(task.titleFromFirstMessage ? { titleFromFirstMessage: true } : {}),
196
200
  };
197
201
  // Same rule as a message's `from`: the opener is remapped to its
@@ -92,6 +92,16 @@ export class TeamComputers {
92
92
  setProblem(id, problem) {
93
93
  return this.patch(id, { problem: problem?.slice(0, 500) });
94
94
  }
95
+ /** Relabel an existing team's assignment; this never changes its computer. */
96
+ renameSection(from, to) {
97
+ const entry = this.forSection(from);
98
+ if (!entry || from === to)
99
+ return false;
100
+ if (this.forSection(to))
101
+ throw failure("This team already has a computer");
102
+ this.patch(entry.id, { section: to });
103
+ return true;
104
+ }
95
105
  patch(id, patch) {
96
106
  const entries = this.list();
97
107
  const entry = entries.find(candidate => candidate.id === id);
@@ -1,5 +1,5 @@
1
1
  import { parseJson } from "./schema.js";
2
- import { isBotPackage, parseBotPackage } from "./bot-package.js";
2
+ import { isBotPackage, parseBotPackage, parsePackageDocument } from "./bot-package.js";
3
3
  import { parseTeamManifest } from "./team-manifest.js";
4
4
  export const TEAM_LIBRARY_REPOSITORY = "https://github.com/milind-soni/openmausbot-teams";
5
5
  export const TEAM_LIBRARY_RAW_ROOT = "https://raw.githubusercontent.com/milind-soni/openmausbot-teams/main";
@@ -127,7 +127,11 @@ export async function fetchTeamCatalog(fetcher = fetch) {
127
127
  function parseShareable(value) {
128
128
  if (typeof value === "string")
129
129
  return parseBotPackage(value);
130
- return isBotPackage(value) ? parseBotPackage(value) : parseTeamManifest(value);
130
+ if (!isBotPackage(value))
131
+ return parseTeamManifest(value);
132
+ // A shared team (v2) is read as a file: it cannot claim a publisher.
133
+ // Version 1 keeps its original shape for older callers.
134
+ return value.version === 1 ? parseBotPackage(value) : parsePackageDocument(value, { trust: "file" });
131
135
  }
132
136
  async function fetchShareable(url, fetcher) {
133
137
  return url.endsWith(".md")
@@ -140,13 +140,14 @@ export function parseTeamManifest(value) {
140
140
  * 1. Allowlist, not blocklist. The returned object is built field by field
141
141
  * from the parsed member, so every privilege-bearing BotRecord field —
142
142
  * approvalMode, autoApprove, alwaysAllow, chiefOfStaff, approvePeerComms, composio,
143
- * computer, cloudBackend, cwd — is structurally absent, whatever the
143
+ * connectorTools, computer, cloudBackend, cwd — is structurally absent, whatever the
144
144
  * file claimed. parseTeamManifest already drops unknown member keys;
145
145
  * this keeps the guarantee even if the schema grows a field later,
146
146
  * because nothing new can reach a bot record without someone
147
147
  * consciously widening this return type. The caller must still force
148
- * composio: false on the created record — that is the one privilege
149
- * where *absence* means allowed, so leaving it unset is not safe.
148
+ * composio: false (and connectorTools: {}) on the created record —
149
+ * composio is the one privilege where *absence* means allowed, so
150
+ * leaving it unset is not safe.
150
151
  *
151
152
  * 2. No name captures. Display names are identity wherever bots address
152
153
  * each other — @mention resolution in rooms, the Chief of Staff roster,