openmausbot 0.1.92 → 0.1.94

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 (163) hide show
  1. package/dist/assets/{DesktopViewer-Do6lBa4C.js → DesktopViewer-bxM7NV3N.js} +1 -1
  2. package/dist/assets/{abnfDiagram-O67JEVCF-Cd3X7Hxc.js → abnfDiagram-O67JEVCF-BhMdJhRJ.js} +1 -1
  3. package/dist/assets/{arc-DIYc7Ymx.js → arc-B5itKDcx.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-DBmRGVyP.js → architectureDiagram-NJMV4G6O-C5d7UY6U.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-CPFePSEE.js → blockDiagram-BEXU5L5S-BrZIhMmu.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-DD_DO1qy.js → c4Diagram-YGBWAQC7-7I0ArfbD.js} +1 -1
  7. package/dist/assets/channel-CdJX6i8k.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-BNEeNSjX.js → chunk-2Q5K7J3B-BnEsyN36.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-BbinLaqH.js → chunk-3FUC2YCW-BXATQ52s.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-BBgYrgDR.js → chunk-5VM5RSS4-B0BpTkEx.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-ezXj-kA-.js → chunk-GTNCS2PH-BtLTz6FR.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-9FxZ3diV.js → chunk-GWA4HPMP-D6gcZOYt.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-CO-40JpI.js → chunk-JWPE2WC7-DCLrosvf.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-BEhjslYE.js → chunk-NETBCI7D-B9JsknV9.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-CNeq4n7_.js → chunk-WEXAMYUT-CsoTxw7z.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-OsCY7KSi.js → chunk-XXDRQBXY-0F_frB0c.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-CloRUEtk.js → classDiagram-v2-NBCMYWYE-DkrpnflX.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-D1BGZhp4.js → cose-bilkent-JH36ORCC-BNDwYBt9.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-CUf4T2CN.js → cynefin-OW5HDTMX-JUkNr1Mk.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-DPEZrFGA.js → cynefinDiagram-VND7K2PF-DKeh9N4M.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-bDe6GIv5.js → dagre-6A5THRUB-ve-6T_wX.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-CdcUQz55.js → diagram-22UHCM2B-BuJ2d8rT.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-BZpqGg7c.js → diagram-3UASUU5V-BgOs_Ni-.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-DvGM6ugp.js → diagram-ATOU4E4O-DMK6VmcP.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-DSTmMm0s.js → diagram-CDSNMT55-DksASlcY.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-15cucnMn.js → diagram-MLGK6HIB-Bgn8I5k6.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-tMbuaE6n.js → diagram-MPIPVDR6-zK0jqO8N.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-BYxYzjHI.js → ebnfDiagram-ZINNZB2B-vKrbzTwr.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-v6bj852W.js → elk-276RUBZZ-Bzv2mPNf.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-5lBBlnd2.js → erDiagram-OPXOYQCR-CvOyYuxu.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-CZyl1zIj.js → flowDiagram-KWPJA3E3-CtbnDbc0.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-BiAI-ltZ.js → ganttDiagram-FUAMR5RP-c4dMRYyG.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-BAZISkj3.js → gitGraphDiagram-X574FWY7-B34uaQho.js} +1 -1
  34. package/dist/assets/index-CPYrGsv1.js +610 -0
  35. package/dist/assets/{index-DGBaEXrM.js → index-CPiqc_DN.js} +1 -1
  36. package/dist/assets/index-CzT42GR8.css +1 -0
  37. package/dist/assets/{infoDiagram-VRGFBTTK-B6ooOy-Z.js → infoDiagram-VRGFBTTK-o64ma4CI.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-BZ9ksi-B.js → ishikawaDiagram-OU5B5YK6-DV4C9BAm.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-DWqlzIEe.js → journeyDiagram-ZHPQQLJL-Bf0AHk1H.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-JX6piDwL.js → kanban-definition-PNTS6WVX-B1UMSKmE.js} +1 -1
  41. package/dist/assets/{linear-CmOWzX0P.js → linear-BpKhuyLm.js} +1 -1
  42. package/dist/assets/{mermaid.core-CF1zwoQ-.js → mermaid.core-B_BKNCw5.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-CGh2K4fW.js → mindmap-definition-NLK3R4M7-DeARruT1.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-CDwYN0s7.js → pegDiagram-GJSIUBJH-BOQEk_1D.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-1rfjJocB.js → pieDiagram-5QR66LMP-EIfoiTZo.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-tKPvPiok.js → quadrantDiagram-O4NWA36T-GgZwT-fi.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-BHLm5vXR.js → railroadDiagram-XR7U4H2S-D9rDQomj.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-CB45STp7.js → requirementDiagram-PLB6GJNP-DAh1MIeo.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-55QA_huC.js → sankeyDiagram-IPEJSGJF-CPr6Uz64.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-BckFmQHn.js → sequenceDiagram-PO4LG4MO-BU6VIdcm.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-BFCOJF11.js → sizeCapture-INFHLROL-qF2HO41r.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-CZvmiVVN.js → stateDiagram-v2-GCMORJYK-DlJ8G4MD.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-BpDUCEKy.js → swimlanes-2SLR337P-DzYE8I54.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-D-U4GtLQ.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-BpxHq6IK.js → timeline-definition-EJHVYXUP-B7wzEQ0A.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-Cj66vm02.js → usecaseDiagram-POWQR4AR-BvGgrxIP.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-C9nRkWE_.js → vennDiagram-UO4OBE2U-CEECdgQg.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-D-me_nWj.js → wardleyDiagram-VNRHLVJA-XQHA0P_P.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-Dc_fL4mk.js → xychartDiagram-PMCCYNJV-B5mLcZI2.js} +1 -1
  60. package/dist/index.html +2 -2
  61. package/dist-server/cloud-home-start.js +1 -0
  62. package/dist-server/companion/src/routes.js +45 -1
  63. package/dist-server/container-mcp.js +99 -77
  64. package/dist-server/drivers/agents-proxy.js +40 -1
  65. package/dist-server/drivers/pi-mcp-extension.ts +245 -299
  66. package/dist-server/enterprise/server/index.js +1 -0
  67. package/dist-server/index.js +18361 -13794
  68. package/dist-server/local-computer-proxy.js +5 -1
  69. package/dist-server/local-computer.js +1 -0
  70. package/dist-server/mcp-gate.js +152 -20
  71. package/dist-server/mcp-remote-proxy.js +437 -0
  72. package/dist-server/openmausbot.js +8872 -7620
  73. package/dist-server/pair-cli.js +8872 -7620
  74. package/dist-server/permission-proxy.js +65 -1
  75. package/dist-server/proxy-paths.js +1 -0
  76. package/dist-server/server/auto-approve.js +33 -11
  77. package/dist-server/server/boat.js +3 -3
  78. package/dist-server/server/browser-live.js +14 -5
  79. package/dist-server/server/browser-runtime.js +6 -0
  80. package/dist-server/server/channel-queue.js +4 -1
  81. package/dist-server/server/cli.js +32 -12
  82. package/dist-server/server/cloud-move-http.js +9 -3
  83. package/dist-server/server/cloud-move.js +18 -0
  84. package/dist-server/server/cloud-owner.js +1 -1
  85. package/dist-server/server/computer-control.js +6 -0
  86. package/dist-server/server/computer-wait.js +33 -13
  87. package/dist-server/server/config.js +57 -10
  88. package/dist-server/server/container-computer.js +43 -32
  89. package/dist-server/server/decision-log.js +9 -6
  90. package/dist-server/server/delegations.js +47 -22
  91. package/dist-server/server/digest.js +12 -2
  92. package/dist-server/server/drivers/acp/core.js +229 -20
  93. package/dist-server/server/drivers/acp/cursor.js +13 -1
  94. package/dist-server/server/drivers/acp/grok.js +208 -3
  95. package/dist-server/server/drivers/acp/quiet-status.js +257 -0
  96. package/dist-server/server/drivers/acp/qwen.js +18 -0
  97. package/dist-server/server/drivers/agents-call.js +28 -0
  98. package/dist-server/server/drivers/agents-catalog.js +10 -1
  99. package/dist-server/server/drivers/boatagent.js +2 -0
  100. package/dist-server/server/drivers/builtIn.js +2 -0
  101. package/dist-server/server/drivers/cerebras.js +108 -0
  102. package/dist-server/server/drivers/chat-mcp-tools.js +45 -15
  103. package/dist-server/server/drivers/claude.js +35 -9
  104. package/dist-server/server/drivers/codex.js +84 -10
  105. package/dist-server/server/drivers/openai-chat.js +37 -5
  106. package/dist-server/server/drivers/pi.js +48 -4
  107. package/dist-server/server/drivers/prompt-split.js +2 -0
  108. package/dist-server/server/env-path.js +6 -2
  109. package/dist-server/server/environment.js +3 -0
  110. package/dist-server/server/guarded-requests.js +2 -1
  111. package/dist-server/server/index.js +1353 -311
  112. package/dist-server/server/live-call-controller.js +868 -0
  113. package/dist-server/server/live-call.js +216 -0
  114. package/dist-server/server/local-vm-stop-reason.js +26 -0
  115. package/dist-server/server/mcp-gate-config.js +43 -12
  116. package/dist-server/server/mcp-gate.js +127 -25
  117. package/dist-server/server/mcp-http.js +46 -8
  118. package/dist-server/server/mcp-remote-proxy.js +108 -0
  119. package/dist-server/server/new-bot-defaults.js +3 -0
  120. package/dist-server/server/peer-provenance.js +4 -2
  121. package/dist-server/server/permission-proxy.js +15 -1
  122. package/dist-server/server/plan-usage.js +797 -0
  123. package/dist-server/server/provider-key-check.js +2 -1
  124. package/dist-server/server/proxy-paths.js +1 -0
  125. package/dist-server/server/redact.js +2 -0
  126. package/dist-server/server/request-auth.js +3 -2
  127. package/dist-server/server/routes/desktop-viewer.js +50 -6
  128. package/dist-server/server/routes/live.js +98 -0
  129. package/dist-server/server/routines.js +6 -3
  130. package/dist-server/server/skill-library.js +207 -0
  131. package/dist-server/server/skills-library-migration.js +235 -0
  132. package/dist-server/server/skills.js +66 -3
  133. package/dist-server/server/steer-queue.js +13 -1
  134. package/dist-server/server/store.js +86 -4
  135. package/dist-server/server/surface.js +9 -3
  136. package/dist-server/server/team-backup.js +2 -1
  137. package/dist-server/server/thread-events.js +1 -1
  138. package/dist-server/server/tunnel.js +20 -1
  139. package/dist-server/server/turn-log.js +10 -0
  140. package/dist-server/server/turn-resources.js +124 -0
  141. package/dist-server/server/vps-computer.js +1 -1
  142. package/dist-server/server/workspace-backup.js +22 -0
  143. package/dist-server/server/workspace.js +1 -1
  144. package/dist-server/shared/approval-mode.js +1 -1
  145. package/dist-server/shared/ask-question.js +13 -0
  146. package/dist-server/shared/call-consent.js +28 -0
  147. package/dist-server/shared/live-approval.js +124 -0
  148. package/dist-server/shared/live-call.js +176 -0
  149. package/dist-server/shared/redact.js +1 -0
  150. package/dist-server/shared/skill-md.js +10 -0
  151. package/dist-server/shared/team-backup.js +3 -0
  152. package/dist-server/shared/tool-name.js +22 -0
  153. package/dist-server/shared/tool-scope-support.js +25 -0
  154. package/dist-server/shared/tool-scope.js +136 -0
  155. package/dist-server/src/lib/thread-refs.js +406 -0
  156. package/dist-server/vps-container-mcp.js +99 -77
  157. package/dist-server/workspace-backup.worker.js +103 -100
  158. package/enterprise/server/index.js +1 -0
  159. package/package.json +1 -1
  160. package/dist/assets/channel-Dtk5Mtic.js +0 -1
  161. package/dist/assets/index-Cdmjrl28.css +0 -1
  162. package/dist/assets/index-DR7YPMkW.js +0 -597
  163. package/dist/assets/swimlanesDiagram-TC7HE7FX-M0GYW9Y3.js +0 -8
@@ -0,0 +1,235 @@
1
+ // Skills library migration (features.skillsLibrary, skills lane S1).
2
+ //
3
+ // Per-bot skill copies move into the shared library, deduplicated by the
4
+ // reviewed SKILL.md sha256. Originals are archived under the library root —
5
+ // write-once, never deleted — and the per-bot manifest entry is dropped
6
+ // only after the archive exists and the assignment is durably recorded. The
7
+ // sweep is idempotent through state, not markers: a migrated skill has no
8
+ // manifest entry left to re-read, and an existing archive short-circuits
9
+ // the move.
10
+ import { createHash } from "node:crypto";
11
+ import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync } from "node:fs";
12
+ import { dirname, join } from "node:path";
13
+ import { writeFileAtomic } from "./atomic.js";
14
+ import { installLibrarySkill, libraryArchiveDirectory, readSkillLibraryIndex, skillsLibraryRoot, } from "./skill-library.js";
15
+ import { listMigratableSkills, removeManifestEntry } from "./skills.js";
16
+ function sha256Hex(text) {
17
+ return createHash("sha256").update(text).digest("hex");
18
+ }
19
+ /** Move a per-bot skill directory into its archive slot. Same-volume moves
20
+ * rename; a cross-device fallback copies recursively and verifies the
21
+ * SKILL.md hash before the copy counts as the archive. */
22
+ function archiveSkillDirectory(source, archive, expectSha256) {
23
+ mkdirSync(dirname(archive), { recursive: true, mode: 0o700 });
24
+ // Every source directory gets its own slot: the same sha256 can be
25
+ // reinstalled after its manifest entry was cleared, so a collision takes
26
+ // a hash-qualified name, then numeric suffixes, never a silent drop.
27
+ let destination = archive;
28
+ if (existsSync(destination)) {
29
+ const stem = `${archive}.${expectSha256}`;
30
+ destination = stem;
31
+ let suffix = 1;
32
+ while (existsSync(destination))
33
+ destination = `${stem}.${suffix++}`;
34
+ }
35
+ try {
36
+ renameSync(source, destination);
37
+ }
38
+ catch (error) {
39
+ if (error.code !== "EXDEV")
40
+ throw error;
41
+ cpSync(source, destination, { recursive: true, force: false, errorOnExist: true });
42
+ }
43
+ const archived = readFileSync(join(destination, "SKILL.md"), "utf8");
44
+ if (sha256Hex(archived) !== expectSha256) {
45
+ throw new Error("the archived copy does not match the reviewed sha256");
46
+ }
47
+ }
48
+ /** Migrate one bot's per-bot skill copies into the library. Org-stamped
49
+ * skills carry their stamp onto the library entry, so an organization
50
+ * install becomes a library source instead of a parallel system. A bot is
51
+ * assigned only when its own copy was enabled; the first copy's enabled
52
+ * flag still decides the entry's reviewState. */
53
+ export function migrateBotSkillsToLibrary(botId, root = skillsLibraryRoot()) {
54
+ const outcomes = [];
55
+ const assigned = [];
56
+ for (const copy of listMigratableSkills(botId)) {
57
+ if (!copy.intact || !copy.directory) {
58
+ outcomes.push({ botId, name: copy.name, outcome: "skipped", detail: "stored SKILL.md is missing or changed after review" });
59
+ continue;
60
+ }
61
+ // The shared library currently stores SKILL.md only. Moving a package
62
+ // with scripts/references/assets would make its relative paths vanish
63
+ // from the bot's usable directory, so keep that working private copy.
64
+ try {
65
+ if (readdirSync(copy.directory).some((name) => name !== "SKILL.md")) {
66
+ outcomes.push({ botId, name: copy.name, outcome: "skipped", detail: "skill has auxiliary files — keeping its complete private directory" });
67
+ continue;
68
+ }
69
+ }
70
+ catch {
71
+ outcomes.push({ botId, name: copy.name, outcome: "skipped", detail: "could not inspect the private skill directory" });
72
+ continue;
73
+ }
74
+ let instructions;
75
+ try {
76
+ instructions = readFileSync(join(copy.directory, "SKILL.md"), "utf8");
77
+ }
78
+ catch (error) {
79
+ outcomes.push({ botId, name: copy.name, outcome: "skipped", detail: `could not read the stored SKILL.md: ${error instanceof Error ? error.message : String(error)}` });
80
+ continue;
81
+ }
82
+ if (sha256Hex(instructions) !== copy.sha256) {
83
+ outcomes.push({ botId, name: copy.name, outcome: "skipped", detail: "stored SKILL.md changed after review" });
84
+ continue;
85
+ }
86
+ const existing = readSkillLibraryIndex(root)[copy.name];
87
+ if (existing && existing.sha256 !== copy.sha256) {
88
+ outcomes.push({ botId, name: copy.name, outcome: "skipped", detail: `the library already has a different "${copy.name}" — rename one of them` });
89
+ continue;
90
+ }
91
+ // The library entry's review state is shared by every assigned bot, so
92
+ // deduplicating a copy whose own on/off differs from it would silently
93
+ // flip that bot's switch. Keep the per-bot copy in place for a human to
94
+ // reconcile instead of archiving and assigning it.
95
+ if (existing && (existing.reviewState === "approved") !== copy.enabled) {
96
+ outcomes.push({
97
+ botId,
98
+ name: copy.name,
99
+ outcome: "skipped",
100
+ detail: `the library "${copy.name}" is ${existing.reviewState === "approved" ? "enabled" : "disabled"} while this bot had it ${copy.enabled ? "enabled" : "disabled"} — align them before migrating`,
101
+ });
102
+ continue;
103
+ }
104
+ if (!existing) {
105
+ const installed = installLibrarySkill({
106
+ name: copy.name,
107
+ description: copy.description,
108
+ instructions,
109
+ source: copy.source,
110
+ license: copy.license,
111
+ compatibility: copy.compatibility,
112
+ reviewState: copy.enabled ? "approved" : "disabled",
113
+ package: copy.package,
114
+ root,
115
+ });
116
+ if ("error" in installed) {
117
+ outcomes.push({ botId, name: copy.name, outcome: "skipped", detail: installed.error });
118
+ continue;
119
+ }
120
+ outcomes.push({ botId, name: copy.name, outcome: "migrated" });
121
+ }
122
+ else {
123
+ outcomes.push({ botId, name: copy.name, outcome: "deduplicated", detail: "identical sha256 already in the library" });
124
+ }
125
+ // The manifest entry is the only re-readable record of this skill, so
126
+ // the assignment must be durable before it goes away. Record it in the
127
+ // pending-assignments file the boot sweep and the standalone script both
128
+ // replay: if this write — or the later patch to the bot record — fails,
129
+ // the next boot restores the assignment instead of stranding an
130
+ // archived skill with no manifest and no assignment.
131
+ try {
132
+ const pendingAssignments = readPendingAssignments(root);
133
+ if (copy.enabled)
134
+ writePendingAssignments({
135
+ ...pendingAssignments,
136
+ [botId]: [...new Set([...(pendingAssignments[botId] ?? []), ...assigned, copy.name])].sort(),
137
+ }, root);
138
+ }
139
+ catch (error) {
140
+ outcomes.push({ botId, name: copy.name, outcome: "skipped", detail: `could not record the assignment durably: ${error instanceof Error ? error.message : String(error)}` });
141
+ continue;
142
+ }
143
+ try {
144
+ archiveSkillDirectory(copy.directory, libraryArchiveDirectory(root, botId, copy.name), copy.sha256);
145
+ }
146
+ catch (error) {
147
+ // The library entry is idempotent, so leaving the per-bot copy in
148
+ // place only means the next sweep retries this skill. The assignment
149
+ // recorded above replays idempotently, so the extra pending entry a
150
+ // failed archive leaves behind is harmless.
151
+ outcomes.push({ botId, name: copy.name, outcome: "skipped", detail: `archiving failed: ${error instanceof Error ? error.message : String(error)}` });
152
+ continue;
153
+ }
154
+ const removed = removeManifestEntry(botId, copy.name, copy.sha256);
155
+ if ("error" in removed) {
156
+ outcomes.push({ botId, name: copy.name, outcome: "skipped", detail: removed.error });
157
+ continue;
158
+ }
159
+ // Assignment follows this bot's own copy.enabled: a disabled copy
160
+ // migrates its bytes but assigns nothing, so nobody gains a skill
161
+ // they never turned on.
162
+ if (copy.enabled)
163
+ assigned.push(copy.name);
164
+ }
165
+ return { ranAt: new Date().toISOString(), outcomes, assignments: assigned.length ? { [botId]: assigned } : {} };
166
+ }
167
+ export function pendingAssignmentsPath(root = skillsLibraryRoot()) {
168
+ return join(root, "pending-assignments.json");
169
+ }
170
+ /** Assignments waiting to reach bot records: recorded by the standalone
171
+ * script for a stopped server, and by the migration itself before a
172
+ * manifest entry is dropped. The next flag-on boot applies them through
173
+ * the Store, the single writer of bots.json. */
174
+ export function readPendingAssignments(root = skillsLibraryRoot()) {
175
+ try {
176
+ const parsed = JSON.parse(readFileSync(pendingAssignmentsPath(root), "utf8"));
177
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
178
+ return {};
179
+ const result = {};
180
+ for (const [botId, names] of Object.entries(parsed)) {
181
+ if (Array.isArray(names) && names.every((name) => typeof name === "string"))
182
+ result[botId] = [...names];
183
+ }
184
+ return result;
185
+ }
186
+ catch {
187
+ return {};
188
+ }
189
+ }
190
+ export function writePendingAssignments(assignments, root = skillsLibraryRoot()) {
191
+ mkdirSync(root, { recursive: true, mode: 0o700 });
192
+ writeFileAtomic(pendingAssignmentsPath(root), `${JSON.stringify(assignments, null, 2)}\n`, { mode: 0o600 });
193
+ }
194
+ /** The boot sweep: migrate every bot's copies, then apply assignments.
195
+ * With a `patch` callback (the live server) assignments land on the bot
196
+ * records directly and any script-recorded pending file is cleared; without
197
+ * one this only reports. Runs under features.skillsLibrary only. */
198
+ export function runSkillsLibraryBootSweep(options) {
199
+ const root = options.root ?? skillsLibraryRoot();
200
+ const outcomes = [];
201
+ const assignments = {};
202
+ const pending = options.patch ? readPendingAssignments(root) : {};
203
+ for (const bot of options.bots) {
204
+ const report = migrateBotSkillsToLibrary(bot.id, root);
205
+ outcomes.push(...report.outcomes);
206
+ const current = bot.assignedSkills ?? [];
207
+ const merged = [...new Set([...(pending[bot.id] ?? []), ...(report.assignments[bot.id] ?? []), ...current])].sort();
208
+ const changed = merged.length !== current.length || merged.some((name, index) => name !== current[index]);
209
+ if (!changed)
210
+ continue;
211
+ if (options.patch)
212
+ options.patch(bot.id, merged);
213
+ assignments[bot.id] = merged;
214
+ }
215
+ // Clear the pending file once every patch applied: entries recorded by
216
+ // the script before this boot and recovery records written by this run's
217
+ // migrations are both spent.
218
+ if (options.patch && (Object.keys(pending).length || Object.keys(readPendingAssignments(root)).length)) {
219
+ writePendingAssignments({}, root);
220
+ }
221
+ return { ranAt: new Date().toISOString(), outcomes, assignments };
222
+ }
223
+ /** Bot ids that have per-bot skill manifests on disk. Used by the
224
+ * standalone script, which cannot reach the Store. */
225
+ export function botIdsWithSkillState(dataDir) {
226
+ try {
227
+ return readdirSync(join(dataDir, "skill-state"), { withFileTypes: true })
228
+ .filter((entry) => entry.isDirectory())
229
+ .map((entry) => entry.name)
230
+ .sort();
231
+ }
232
+ catch {
233
+ return [];
234
+ }
235
+ }
@@ -33,6 +33,7 @@ import { writeFileAtomic } from "./atomic.js";
33
33
  import { DATA_DIR } from "./config.js";
34
34
  import { redactSecretsInText } from "./redact.js";
35
35
  import { LEARN_SOURCE_PREFIX } from "./skill-learn.js";
36
+ import { librarySkillFilePath, listLibrarySkills } from "./skill-library.js";
36
37
  import { workspaceDir } from "./workspace.js";
37
38
  import { isSkillName, parseSkillMd, scanSkillText, SKILL_FILE_MAX_BYTES } from "../shared/skill-md.js";
38
39
  // SKILL.md parsing and the static scan live in shared/ so the package
@@ -1191,11 +1192,14 @@ export function applySkillWriteWithReceipt(botId, staged, recordApplied) {
1191
1192
  * index lines only — the same progressive-disclosure shape the spec asks
1192
1193
  * agents for. Bodies never ride the prompt; the bot reads the file when a
1193
1194
  * task matches. */
1194
- export function skillsSystemPrompt(botId) {
1195
+ export function skillsSystemPrompt(botId, assignedLibrary) {
1196
+ return composeSkillsSystemPrompt(botId, assignedLibrary);
1197
+ }
1198
+ function composeSkillsSystemPrompt(botId, assignedLibrary) {
1195
1199
  // Reconcile links on every turn. If the workspace copy changed since its
1196
1200
  // review, integrity filtering below removes it from native discovery too.
1197
1201
  syncSkillLinks(botId);
1198
- const enabled = listSkills(botId).filter((skill) => skill.enabled);
1202
+ const enabled = resolveBotSkills(botId, assignedLibrary).filter((skill) => skill.enabled);
1199
1203
  if (!enabled.length) {
1200
1204
  loggedIndexOmissions.delete(botId);
1201
1205
  return "";
@@ -1212,7 +1216,7 @@ export function skillsSystemPrompt(botId) {
1212
1216
  (omitted ? `${entries.length ? "\n" : ""}${notice(omitted, entries.length)}` : "");
1213
1217
  for (const skill of enabled.slice(0, INDEX_MAX_SKILLS)) {
1214
1218
  const entry = manifest[skill.name];
1215
- const file = join(skillTarget(root, skill.name, entry), "SKILL.md");
1219
+ const file = entry ? join(skillTarget(root, skill.name, entry), "SKILL.md") : librarySkillFilePath(skill.name);
1216
1220
  const line = `- ${skill.name}: ${skill.description} Read ${JSON.stringify(file)}.`;
1217
1221
  if (Buffer.byteLength(block([...lines, line], enabled.length - lines.length - 1), "utf8") > INDEX_MAX_BYTES)
1218
1222
  break;
@@ -1231,3 +1235,62 @@ export function skillsSystemPrompt(botId) {
1231
1235
  }
1232
1236
  return block(lines, omitted.length);
1233
1237
  }
1238
+ // --- Skills library composition (features.skillsLibrary) ---
1239
+ /** One bot's private skills plus its assigned library skills, private
1240
+ * winning any name collision (the lane's resolution order: bot-private >
1241
+ * library > bundled). `undefined` assignments return exactly
1242
+ * `listSkills(botId)` — the flag-off contract. */
1243
+ export function resolveBotSkills(botId, assignedLibrary) {
1244
+ const own = listSkills(botId);
1245
+ if (!assignedLibrary?.length)
1246
+ return own;
1247
+ const ownNames = new Set(own.map((skill) => skill.name));
1248
+ const assigned = new Set(assignedLibrary);
1249
+ const fromLibrary = listLibrarySkills().filter((skill) => assigned.has(skill.name) && !ownNames.has(skill.name));
1250
+ return [...own, ...fromLibrary];
1251
+ }
1252
+ export function listBotSkillsWithOrigin(botId, assignedLibrary) {
1253
+ const own = listSkills(botId);
1254
+ const ownNames = new Set(own.map((skill) => skill.name));
1255
+ const assigned = new Set(assignedLibrary ?? []);
1256
+ const skills = resolveBotSkills(botId, assignedLibrary).map((skill) => ({
1257
+ ...skill,
1258
+ origin: ownNames.has(skill.name) ? "private" : "library",
1259
+ }));
1260
+ const library = listLibrarySkills().filter((entry) => !ownNames.has(entry.name) && !assigned.has(entry.name));
1261
+ return { skills, library };
1262
+ }
1263
+ export function listMigratableSkills(botId) {
1264
+ return Object.entries(readManifest(botId))
1265
+ .map(([name, entry]) => ({
1266
+ name,
1267
+ directory: skillDirectory(botId, name, entry),
1268
+ sha256: entry.sha256,
1269
+ enabled: entry.enabled,
1270
+ source: entry.source,
1271
+ description: entry.description,
1272
+ ...(entry.license ? { license: entry.license } : {}),
1273
+ ...(entry.compatibility ? { compatibility: entry.compatibility } : {}),
1274
+ ...(entry.package ? { package: { ...entry.package } } : {}),
1275
+ intact: skillContentMatches(botId, name, entry),
1276
+ }))
1277
+ .sort((a, b) => a.name.localeCompare(b.name));
1278
+ }
1279
+ /** Drop manifest entries the migration has archived. The caller archives
1280
+ * the bytes first and passes the sha256 it read, so a skill that changed
1281
+ * between read and removal is left untouched. */
1282
+ export function removeManifestEntry(botId, name, expectSha256) {
1283
+ if (!isSkillName(name))
1284
+ return { error: "invalid skill name" };
1285
+ const manifest = readManifest(botId);
1286
+ const entry = manifest[name];
1287
+ if (!entry)
1288
+ return { removed: true };
1289
+ if (entry.sha256 !== expectSha256) {
1290
+ return { error: `skill "${name}" changed since it was read — leaving it in place` };
1291
+ }
1292
+ delete manifest[name];
1293
+ writeManifest(botId, manifest);
1294
+ syncSkillLinks(botId);
1295
+ return { removed: true };
1296
+ }
@@ -28,8 +28,11 @@ export function restoreSteeredMessages() {
28
28
  const entry = queues.get(row.threadId) ?? { botId: row.ownerId, items: [] };
29
29
  if (entry.botId !== row.ownerId)
30
30
  throw new Error("queued task belongs to another bot");
31
+ // a 1:1 line is only ever stamped "call"; "api" belongs to channel rows
32
+ const { via, ...payload } = row.payload;
31
33
  entry.items.push({
32
- ...row.payload,
34
+ ...payload,
35
+ ...(via === "call" ? { via } : {}),
33
36
  messageId: row.id,
34
37
  prompt: row.payload.prompt ?? row.payload.text,
35
38
  // rows queued before timestamps were kept read as queued at restore
@@ -83,6 +86,7 @@ export function queueSteeredMessage(botId, threadId, text, options = {}) {
83
86
  sender: options.sender,
84
87
  trigger: options.trigger,
85
88
  queuedAt: Date.now(),
89
+ via: options.via,
86
90
  };
87
91
  saveChatFollowup({ id, kind: "bot", ownerId: botId, threadId, payload: item });
88
92
  entry.items.push(item);
@@ -112,6 +116,13 @@ export function hasQueuedSteeredMessages(botId, threadId) {
112
116
  const entry = queues.get(threadId);
113
117
  return entry?.botId === botId && entry.items.length > 0;
114
118
  }
119
+ /** Whether one send still waits in its thread's queue. False while it is
120
+ * out of the queue: drained onto the thread, held for a steer into the
121
+ * running turn, or cancelled (editing a queued line cancels it). */
122
+ export function isSteeredMessageQueued(botId, threadId, queueId) {
123
+ const entry = queues.get(threadId);
124
+ return entry?.botId === botId && entry.items.some((item) => item.messageId === queueId);
125
+ }
115
126
  /** Drain every queue whose task is idle: append the held lines (leaf is now
116
127
  * the finished turn's last item), then one run per thread whose prompt is
117
128
  * the drained group's texts separated by a blank line. `userMessage` is the last appended line
@@ -161,6 +172,7 @@ export function drainSteeredMessages(store, run, isBlocked) {
161
172
  queueId: item.messageId,
162
173
  peerAsk: item.peerAsk,
163
174
  sender: item.sender,
175
+ ...(item.via ? { via: item.via } : {}),
164
176
  }));
165
177
  }
166
178
  const last = appended.at(-1);
@@ -6,6 +6,7 @@ import { createHash } from "node:crypto";
6
6
  import { chmodSync, existsSync, readFileSync, mkdirSync, rmSync, statSync, unlinkSync } from "node:fs";
7
7
  import { join } from "node:path";
8
8
  import { writeFileAtomic } from "./atomic.js";
9
+ import { parseToolScope, toolScopeWidens } from "../shared/tool-scope.js";
9
10
  import { ensureSections, readSections, changeEmptySection } from "./section-context.js";
10
11
  import { removeBotFolder, soulFile, soulHash, writeSoulMirror } from "./bot-folder.js";
11
12
  import { peerAllowKey } from "./peer-approval-key.js";
@@ -32,8 +33,10 @@ export const taskWireProjectionIsExact = true;
32
33
  /** The typed wire projection for one task. Pairs with the assertion above:
33
34
  * returning WireTask means an undeclared server field cannot ride silently. */
34
35
  export function toWireTask(task) {
35
- const { resumeCursors: _resumeCursors, lastInstanceId: _lastInstanceId, handedMessages: _handedMessages, appliedCompactionId: _appliedCompactionId, contextFloor: _contextFloor, lastContextModel: _lastContextModel, surfaceSource: _surfaceSource, ...wire } = task;
36
- return wire;
36
+ const { resumeCursors: _resumeCursors, lastInstanceId: _lastInstanceId, handedMessages: _handedMessages, appliedCompactionId: _appliedCompactionId, contextFloor: _contextFloor, lastContextModel: _lastContextModel, surfaceSource, ...wire } = task;
37
+ // Who pinned stays private. A client learns only whether the pin is the
38
+ // machine's own record, so it never presents one as the person's choice.
39
+ return surfaceSource === "auto" && wire.surface !== undefined ? { ...wire, surfaceAuto: true } : wire;
37
40
  }
38
41
  const TASK_PATCH_FIELDS = [
39
42
  "title", "projectId", "modelSelection", "approvalMode", "autoApprove", "alwaysAllow",
@@ -467,8 +470,13 @@ export class Store {
467
470
  mkdirSync(DATA_DIR, { recursive: true });
468
471
  for (const file of [BOTS_FILE, GROUPS_FILE])
469
472
  tightenRegistryFile(file);
473
+ // Whether the bot list is the real one. Room repair below trusts it to
474
+ // say which members no longer exist; an unreadable file must never read
475
+ // as "every member was deleted".
476
+ let botsLoaded = false;
470
477
  try {
471
478
  this.bots = JSON.parse(readFileSync(BOTS_FILE, "utf8"));
479
+ botsLoaded = Array.isArray(this.bots);
472
480
  }
473
481
  catch {
474
482
  this.bots = [];
@@ -637,9 +645,17 @@ export class Store {
637
645
  botsMigrated = true;
638
646
  }
639
647
  }
648
+ const botIds = new Set(this.bots.map((b) => b.id));
640
649
  for (const g of this.groups) {
641
650
  g.busyBotId = null;
642
651
  delete g.turnStartedAt;
652
+ // A deleted bot used to stay a member for good: counted on the room's
653
+ // Save button and refused by the roster check on every save (MOCA-264).
654
+ // Bot-to-bot channels keep their pair; they are not edited as rooms.
655
+ if (botsLoaded && !g.dm && g.memberIds.some((id) => !botIds.has(id))) {
656
+ g.memberIds = g.memberIds.filter((id) => botIds.has(id));
657
+ groupsMigrated = true;
658
+ }
643
659
  const normalized = normalizeGroupDefaultResponder(g.defaultResponder, g.memberIds, Boolean(g.dm));
644
660
  if (JSON.stringify(normalized) !== JSON.stringify(g.defaultResponder))
645
661
  groupsMigrated = true;
@@ -1573,6 +1589,9 @@ export class Store {
1573
1589
  return this.completeNewBotSelection(requested ?? this.defaultSelection());
1574
1590
  }
1575
1591
  createBot(profile = {}, opts = {}) {
1592
+ const toolScope = parseToolScope(profile.toolScope);
1593
+ if (!toolScope.ok)
1594
+ throw new Error(toolScope.error);
1576
1595
  this.rememberSections([profile.section]);
1577
1596
  const name = profile.name?.trim() || pickBotName(this.bots.map((b) => b.name));
1578
1597
  const section = sectionKey(profile.section);
@@ -1590,6 +1609,7 @@ export class Store {
1590
1609
  ...(profile.mascotBody ? { mascotBody: profile.mascotBody } : {}),
1591
1610
  // Restricted from its first frame: no one else is ever told it exists.
1592
1611
  ...(profile.visibility && profile.visibility !== "everyone" ? { visibility: structuredClone(profile.visibility) } : {}),
1612
+ ...(toolScope.scope ? { toolScope: toolScope.scope } : {}),
1593
1613
  unread: false,
1594
1614
  modelSelection: this.newBotSelection(profile.modelSelection),
1595
1615
  resumeCursors: {},
@@ -1610,8 +1630,10 @@ export class Store {
1610
1630
  activity: "idle",
1611
1631
  busy: false,
1612
1632
  }];
1633
+ // Persist the selection in the first record, before publishing the bot
1634
+ // or starting its greeting. An interrupted creation cannot inherit tools.
1635
+ this.saveBots([bot, ...this.bots]);
1613
1636
  this.bots.unshift(bot);
1614
- this.saveBots();
1615
1637
  // The folder exists from the first moment, so the user can open
1616
1638
  // SOUL.md before the bot has said a word. The record is canonical: a
1617
1639
  // mirror-write failure must never fail bot creation.
@@ -1764,6 +1786,27 @@ export class Store {
1764
1786
  this.saveBots(nextBots);
1765
1787
  this.bots = nextBots;
1766
1788
  this.legacyActivities.delete(id);
1789
+ // A deleted bot leaves every room it was in, and a room it led falls back
1790
+ // to its next member. Bot-to-bot channels keep their pair.
1791
+ const rooms = this.groups.filter((g) => !g.dm && g.memberIds.includes(id));
1792
+ for (const g of rooms) {
1793
+ g.memberIds = g.memberIds.filter((member) => member !== id);
1794
+ if (g.busyBotId === id) {
1795
+ g.busyBotId = null;
1796
+ delete g.turnStartedAt;
1797
+ }
1798
+ g.defaultResponder = normalizeGroupDefaultResponder(g.defaultResponder, g.memberIds, false);
1799
+ }
1800
+ if (rooms.length) {
1801
+ // Bot removal is already durable. Finish erasing its data even if this
1802
+ // write fails; startup repair removes these stale memberships later.
1803
+ try {
1804
+ this.saveGroups();
1805
+ }
1806
+ catch (error) {
1807
+ console.warn("store: room cleanup will retry on restart", error);
1808
+ }
1809
+ }
1767
1810
  // every task's transcript goes with the bot, not just the open one
1768
1811
  for (const threadId of new Set([bot.threadId, ...(bot.tasks ?? []).map((t) => t.threadId)])) {
1769
1812
  this.deleteThreadRecord(threadId);
@@ -1786,6 +1829,8 @@ export class Store {
1786
1829
  // The bot folder (SOUL.md mirror) is the bot's too.
1787
1830
  removeBotFolder(id);
1788
1831
  this.emit({ type: "bot.deleted", botId: id });
1832
+ for (const g of rooms)
1833
+ this.emit({ type: "group", groupId: g.id });
1789
1834
  return true;
1790
1835
  }
1791
1836
  patchBot(id, patch) {
@@ -1802,6 +1847,18 @@ export class Store {
1802
1847
  throw new Error(parsed.error);
1803
1848
  patch = { ...patch, connectorTools: parsed.grants };
1804
1849
  }
1850
+ if (Object.hasOwn(patch, "toolScope")) {
1851
+ const parsed = parseToolScope(patch.toolScope);
1852
+ if (!parsed.ok)
1853
+ throw new Error(parsed.error);
1854
+ patch = { ...patch, toolScope: parsed.scope };
1855
+ }
1856
+ const wideningToolScope = Object.hasOwn(patch, "toolScope") && toolScopeWidens(bot.toolScope, patch.toolScope);
1857
+ const nextToolScope = patch.toolScope;
1858
+ if (wideningToolScope) {
1859
+ patch = { ...patch };
1860
+ delete patch.toolScope;
1861
+ }
1805
1862
  // Runtime revocations must become effective in memory even when disk is
1806
1863
  // unavailable. Profile edits use the separate atomic path below.
1807
1864
  Object.assign(bot, patch);
@@ -1814,7 +1871,14 @@ export class Store {
1814
1871
  }
1815
1872
  bot.unread = bot.tasks.some(taskCountsAsBotUnread);
1816
1873
  }
1817
- this.saveBots();
1874
+ if (wideningToolScope) {
1875
+ // New authority becomes live only after its durable save succeeds.
1876
+ // Other runtime revocations above still take effect on a failed write.
1877
+ this.saveBots(this.bots.map((candidate) => candidate === bot ? { ...bot, toolScope: nextToolScope } : candidate));
1878
+ bot.toolScope = nextToolScope;
1879
+ }
1880
+ else
1881
+ this.saveBots();
1818
1882
  this.emit({ type: "bot", botId: id });
1819
1883
  return bot;
1820
1884
  }
@@ -2435,6 +2499,24 @@ export class Store {
2435
2499
  this.emit({ type: "bot", botId });
2436
2500
  return bot;
2437
2501
  }
2502
+ /** Copy this bot's saved approval level and standing approvals onto one
2503
+ * thread. A new thread already gets them; this is how an existing
2504
+ * conversation catches up. Other threads, the transcript, and the bot
2505
+ * default stay put. A grant that has not committed yet is ignored, so a
2506
+ * refresh cannot copy the temporary Ask mask. */
2507
+ refreshTaskPermissions(botId, threadId) {
2508
+ const bot = this.bot(botId);
2509
+ const task = this.taskByThread(botId, threadId);
2510
+ if (!bot || !task)
2511
+ return null;
2512
+ const mode = approvalModeFor({ ...bot, approvalGrant: undefined });
2513
+ const alwaysAllow = structuredClone(bot.alwaysAllow ?? []);
2514
+ const autoApprove = mode === "auto";
2515
+ const sameAllow = JSON.stringify(task.alwaysAllow ?? []) === JSON.stringify(alwaysAllow);
2516
+ if (task.approvalMode === mode && task.autoApprove === autoApprove && sameAllow)
2517
+ return task;
2518
+ return this.patchTask(botId, threadId, { approvalMode: mode, autoApprove, alwaysAllow });
2519
+ }
2438
2520
  mirrorActiveTask(bot, task) {
2439
2521
  bot.threadId = task.threadId;
2440
2522
  bot.resumeCursors = structuredClone(task.resumeCursors);
@@ -85,7 +85,13 @@ export function surfaceLabel(surface) {
85
85
  /** Said once per task, so the person hears the place before the first click
86
86
  * lands there. Same words as the panel and the composer chip. */
87
87
  const RESTATE_SENTENCE = " Before your first action on a screen or page in a task, say in one short sentence where you are working, using that same name.";
88
- const SURFACE_AUTHORITY = " For browser and computer tasks, use OpenMausBot's mounted browser/computer tools first: inspect the target, perform the action, and verify its result before claiming success. Discover deferred tools by their server/name when needed. Do not substitute the provider's own desktop, a shell-launched browser, or another automation path for the selected OpenMausBot surface. Ordinary code and file tasks may still use their normal tools. Announcing an action is not performing it. A request naming another place does not move these tools: this computer is the user's host, Local VM is an isolated desktop, the cloud computer is remote, and the built-in browser is a separate browser. If the requested place differs from the mounted one, explain the mismatch and ask the user to change the conversation's computer selector; never act on a different computer or describe a host window as a VM.";
88
+ /** Where a person moves a conversation: the Computer panel is the one place
89
+ * control both interface modes show (Simple has no composer chip). */
90
+ const PLACE_CONTROL = "the Computer panel";
91
+ /** Without select_computer the person has to switch; with it, surfacePrompt
92
+ * swaps exactly this phrase for the model's own switch. */
93
+ const ASK_TO_SWITCH = `explain the mismatch and ask the user to change where this conversation works in ${PLACE_CONTROL}`;
94
+ const SURFACE_AUTHORITY = ` For browser and computer tasks, use OpenMausBot's mounted browser/computer tools first: inspect the target, perform the action, and verify its result before claiming success. Discover deferred tools by their server/name when needed. Do not substitute the provider's own desktop, a shell-launched browser, or another automation path for the selected OpenMausBot surface. Ordinary code and file tasks may still use their normal tools. Announcing an action is not performing it. A request naming another place does not move these tools: this computer is the user's host, Local VM is an isolated desktop, the cloud computer is remote, and the built-in browser is a separate browser. If the requested place differs from the mounted one, ${ASK_TO_SWITCH}; never act on a different computer or describe a host window as a VM.`;
89
95
  /** A Cloud home offers neither this computer nor a Local VM
90
96
  * (server/cloud-home.ts), so its bots are told only about the places it has.
91
97
  * Each pair rewrites one phrase of the desktop wording above. */
@@ -116,13 +122,13 @@ export function surfacePrompt(mounted, opts = {}) {
116
122
  }
117
123
  if (text)
118
124
  text += RESTATE_SENTENCE + (opts.canSelect
119
- ? SURFACE_AUTHORITY.replace("explain the mismatch and ask the user to change the conversation's computer selector", "inspect connected choices with select_computer and select the requested available place; on a pending result end this turn so OpenMausBot can reconnect the correct tools and continue the original request")
125
+ ? SURFACE_AUTHORITY.replace(ASK_TO_SWITCH, "inspect connected choices with select_computer and select the requested available place; on a pending result end this turn so OpenMausBot can reconnect the correct tools and continue the original request")
120
126
  : SURFACE_AUTHORITY);
121
127
  else if (!opts.note && !opts.canSelect) {
122
128
  text = " No computer or built-in browser tools are mounted this turn. You cannot open apps, click, or inspect a screen through OpenMausBot. If asked for screen work, explain this and ask the user to choose and connect a computer in the Computer panel; do not claim to have opened or checked it.";
123
129
  }
124
130
  if (opts.pinned) {
125
- text += ` This conversation is pinned to ${surfaceLabel(opts.pinned)}; changing places requires ${opts.canSelect ? "select_computer or " : ""}the conversation's computer selector, not a different tool name.`;
131
+ text += ` This conversation is pinned to ${surfaceLabel(opts.pinned)}; changing places requires ${opts.canSelect ? "select_computer or " : ""}the user's choice in ${PLACE_CONTROL}, not a different tool name.`;
126
132
  }
127
133
  if (opts.canSelect)
128
134
  text += " For a screen task, use select_computer with no arguments when you need to inspect the actual available targets. Choose the requested place from its result; if the user left the place open, use a suitable available target or surface auto instead of asking them to operate the menu. Browser-only work can stay in Browser; when it needs desktop apps or capabilities the current Browser lacks, select an available Local VM without asking the user to switch it manually. Choose before taking actions, and do not repeat actions already completed if the task must continue elsewhere. OpenMausBot can start an existing configured computer and highlight the selected target. If the right tools are already mounted, use them directly. On a pending switch, end this turn: the original request resumes automatically with the new tools, and then you must carry out the task. Only ask for input for a genuine blocker, such as missing setup, required sign-in or an approval. Never silently replace an explicitly requested VM with the host desktop.";
@@ -119,6 +119,7 @@ export function createTeamBackup(store, routines, name) {
119
119
  // so the team's shape is not lost, but the import below still lands
120
120
  // every bot grant-less — restoring them is a deliberate later choice.
121
121
  ...(bot.connectorTools ? { connectorTools: structuredClone(bot.connectorTools) } : {}),
122
+ ...(bot.toolScope !== undefined ? { toolScope: structuredClone(bot.toolScope) } : {}),
122
123
  memory: memoryFor(bot.id),
123
124
  activeTask: bot.threadId, tasks: history(bot),
124
125
  })),
@@ -188,7 +189,7 @@ export function importTeamBackup(store, routines, input, selection, options = {}
188
189
  bots.push(bot);
189
190
  botIds.set(source.key, bot.id);
190
191
  store.patchBot(bot.id, { composio: false, computer: "off", browser: false, approvalMode: "ask", autoApprove: false,
191
- connectorTools: {}, hidden: source.hidden, chiefOfStaff: source.chiefOfStaff, playbooks: source.playbooks });
192
+ connectorTools: {}, toolScope: source.toolScope, hidden: source.hidden, chiefOfStaff: source.chiefOfStaff, playbooks: source.playbooks });
192
193
  if (source.memory)
193
194
  restoreMemory(bot.id, source.memory);
194
195
  }
@@ -187,7 +187,7 @@ function isRuntimeEvent(value) {
187
187
  typeof value.waitedMs === "number" &&
188
188
  Number.isFinite(value.waitedMs) &&
189
189
  value.waitedMs >= 0 &&
190
- (value.outcome === "acquired" || value.outcome === "gave_up" || value.outcome === "stopped"));
190
+ (value.outcome === "acquired" || value.outcome === "gave_up" || value.outcome === "parked" || value.outcome === "stopped"));
191
191
  case "item.started":
192
192
  return (value.itemType === "tool" || value.itemType === "reasoning") && stringOrMissing(value.title);
193
193
  case "item.updated":