openmausbot 0.1.90 → 0.1.92

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 (177) hide show
  1. package/dist/assets/DesktopViewer-Do6lBa4C.js +5 -0
  2. package/dist/assets/{abnfDiagram-O67JEVCF-DUyXd0kL.js → abnfDiagram-O67JEVCF-Cd3X7Hxc.js} +1 -1
  3. package/dist/assets/{arc-q73qM-At.js → arc-DIYc7Ymx.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-D33O3Wur.js → architectureDiagram-NJMV4G6O-DBmRGVyP.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-DLsJeoXC.js → blockDiagram-BEXU5L5S-CPFePSEE.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-DnaljZnu.js → c4Diagram-YGBWAQC7-DD_DO1qy.js} +1 -1
  7. package/dist/assets/channel-Dtk5Mtic.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-Da0h2i79.js → chunk-2Q5K7J3B-BNEeNSjX.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-CfzHi54f.js → chunk-3FUC2YCW-BbinLaqH.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-LjE4d9Jf.js → chunk-5VM5RSS4-BBgYrgDR.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-DY-UGmvc.js → chunk-GTNCS2PH-ezXj-kA-.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-BqeGhyhP.js → chunk-GWA4HPMP-9FxZ3diV.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-BYa9EBAg.js → chunk-JWPE2WC7-CO-40JpI.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-vTrDtWyb.js → chunk-NETBCI7D-BEhjslYE.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-ic5tXadM.js → chunk-WEXAMYUT-CNeq4n7_.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-DvRCY-c6.js → chunk-XXDRQBXY-OsCY7KSi.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-DVbmgz_L.js → classDiagram-v2-NBCMYWYE-CloRUEtk.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-vdUs2n57.js → cose-bilkent-JH36ORCC-D1BGZhp4.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-BvLGlV3U.js → cynefin-OW5HDTMX-CUf4T2CN.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-C4GcJQDt.js → cynefinDiagram-VND7K2PF-DPEZrFGA.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-BMSaHpt_.js → dagre-6A5THRUB-bDe6GIv5.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-D5b9UBhC.js → diagram-22UHCM2B-CdcUQz55.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-CIKLlB7Y.js → diagram-3UASUU5V-BZpqGg7c.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-BzXb-7k7.js → diagram-ATOU4E4O-DvGM6ugp.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-HsTvJ7uT.js → diagram-CDSNMT55-DSTmMm0s.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-Db9ySNbQ.js → diagram-MLGK6HIB-15cucnMn.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-D6BKbpqY.js → diagram-MPIPVDR6-tMbuaE6n.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-Bq067L0t.js → ebnfDiagram-ZINNZB2B-BYxYzjHI.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-DhaD7fNJ.js → elk-276RUBZZ-v6bj852W.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-fwIhfz36.js → erDiagram-OPXOYQCR-5lBBlnd2.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-DygIyu0V.js → flowDiagram-KWPJA3E3-CZyl1zIj.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-DPYfSO96.js → ganttDiagram-FUAMR5RP-BiAI-ltZ.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-DhkHeCle.js → gitGraphDiagram-X574FWY7-BAZISkj3.js} +1 -1
  34. package/dist/assets/index-Cdmjrl28.css +1 -0
  35. package/dist/assets/{index-G1PHKGyh.js → index-DGBaEXrM.js} +1 -1
  36. package/dist/assets/index-DR7YPMkW.js +597 -0
  37. package/dist/assets/{infoDiagram-VRGFBTTK-DstBLxME.js → infoDiagram-VRGFBTTK-B6ooOy-Z.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-CrIsGgch.js → ishikawaDiagram-OU5B5YK6-BZ9ksi-B.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-CYypGDbB.js → journeyDiagram-ZHPQQLJL-DWqlzIEe.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-DGjPt8E_.js → kanban-definition-PNTS6WVX-JX6piDwL.js} +1 -1
  41. package/dist/assets/{linear-Dwc3Hrru.js → linear-CmOWzX0P.js} +1 -1
  42. package/dist/assets/{mermaid.core-Dc4OePBL.js → mermaid.core-CF1zwoQ-.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-DV-vepj-.js → mindmap-definition-NLK3R4M7-CGh2K4fW.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-lx5Kj-n5.js → pegDiagram-GJSIUBJH-CDwYN0s7.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-Bq7Vmm15.js → pieDiagram-5QR66LMP-1rfjJocB.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-DeqeqOBj.js → quadrantDiagram-O4NWA36T-tKPvPiok.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-CXwRQzSC.js → railroadDiagram-XR7U4H2S-BHLm5vXR.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-8tAeoB88.js → requirementDiagram-PLB6GJNP-CB45STp7.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-BG5zjGWU.js → sankeyDiagram-IPEJSGJF-55QA_huC.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-DKF3iLcw.js → sequenceDiagram-PO4LG4MO-BckFmQHn.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-B-y1qUgT.js → sizeCapture-INFHLROL-BFCOJF11.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-MkzPgxAc.js → stateDiagram-v2-GCMORJYK-CZvmiVVN.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-DnbaOEwH.js → swimlanes-2SLR337P-BpDUCEKy.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-M0GYW9Y3.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-Bb4xG_Bt.js → timeline-definition-EJHVYXUP-BpxHq6IK.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-JgkR9pFf.js → usecaseDiagram-POWQR4AR-Cj66vm02.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-CIPIrAYe.js → vennDiagram-UO4OBE2U-C9nRkWE_.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-BjCLuUvJ.js → wardleyDiagram-VNRHLVJA-D-me_nWj.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-DKgiaYxT.js → xychartDiagram-PMCCYNJV-Dc_fL4mk.js} +1 -1
  60. package/dist/index.html +2 -2
  61. package/dist/novnc-NOTICE.txt +118 -0
  62. package/dist-server/cloud-home-start.js +128 -15
  63. package/dist-server/container-mcp.js +277 -45
  64. package/dist-server/drivers/agents-proxy.js +16 -4
  65. package/dist-server/enterprise/server/index.js +5 -1
  66. package/dist-server/index.js +46703 -39955
  67. package/dist-server/local-computer-proxy.js +128 -7
  68. package/dist-server/mcp-gate.js +11 -5
  69. package/dist-server/model-catalog/models-dev.snapshot.json +7299 -0
  70. package/dist-server/openmausbot.js +4099 -1217
  71. package/dist-server/pair-cli.js +4099 -1217
  72. package/dist-server/server/boat.js +67 -22
  73. package/dist-server/server/bot-overview.js +7 -4
  74. package/dist-server/server/browser-engine.js +10 -1
  75. package/dist-server/server/browser-live.js +46 -2
  76. package/dist-server/server/browser-runtime.js +74 -8
  77. package/dist-server/server/browser-tool-shape.js +6 -4
  78. package/dist-server/server/chief-of-staff.js +1 -1
  79. package/dist-server/server/cloud-home-start.js +136 -26
  80. package/dist-server/server/cloud-home.js +64 -5
  81. package/dist-server/server/cloud-lending.js +167 -0
  82. package/dist-server/server/cloud-move-http.js +268 -0
  83. package/dist-server/server/cloud-move.js +418 -0
  84. package/dist-server/server/cloud-owner.js +196 -0
  85. package/dist-server/server/cloud-secrets-boot.js +6 -0
  86. package/dist-server/server/cloud-secrets.js +65 -0
  87. package/dist-server/server/config.js +215 -17
  88. package/dist-server/server/container-computer.js +3 -2
  89. package/dist-server/server/decider/index.js +238 -0
  90. package/dist-server/server/decider/jev.js +183 -0
  91. package/dist-server/server/decider/log.js +62 -0
  92. package/dist-server/server/decider/relay.js +52 -0
  93. package/dist-server/server/decider/room-routing.js +88 -0
  94. package/dist-server/server/decider/types.js +5 -0
  95. package/dist-server/server/desktop-viewer-targets.js +26 -0
  96. package/dist-server/server/drivers/acp/core.js +167 -21
  97. package/dist-server/server/drivers/acp/opencode-go.js +614 -85
  98. package/dist-server/server/drivers/agents-catalog.js +22 -6
  99. package/dist-server/server/drivers/antigravity-acp.js +69 -11
  100. package/dist-server/server/drivers/antigravity-temp.js +360 -0
  101. package/dist-server/server/drivers/antigravity.js +21 -0
  102. package/dist-server/server/drivers/boatagent.js +20 -12
  103. package/dist-server/server/drivers/chatgpt-plan-auth.js +481 -0
  104. package/dist-server/server/drivers/claude.js +313 -34
  105. package/dist-server/server/drivers/codex.js +228 -18
  106. package/dist-server/server/drivers/grok.js +2 -2
  107. package/dist-server/server/drivers/mistral.js +4 -3
  108. package/dist-server/server/drivers/openai-chat-protocol.js +4 -3
  109. package/dist-server/server/drivers/openai-chat.js +41 -3
  110. package/dist-server/server/drivers/openai-compat.js +73 -23
  111. package/dist-server/server/engine-install.js +46 -12
  112. package/dist-server/server/env-path.js +15 -3
  113. package/dist-server/server/group-goal-run.js +6 -4
  114. package/dist-server/server/harness/registry.js +2 -2
  115. package/dist-server/server/harness-capabilities.js +2 -0
  116. package/dist-server/server/included-services.js +70 -0
  117. package/dist-server/server/index.js +1753 -263
  118. package/dist-server/server/lending-memory.js +479 -0
  119. package/dist-server/server/local-vm-idle.js +24 -6
  120. package/dist-server/server/managed-desktop.js +3 -0
  121. package/dist-server/server/mcp-bridge.js +5 -3
  122. package/dist-server/server/mcp-http.js +6 -3
  123. package/dist-server/server/mcp-oauth-discovery.js +133 -0
  124. package/dist-server/server/mcp-oauth-store.js +70 -0
  125. package/dist-server/server/mcp-oauth.js +550 -0
  126. package/dist-server/server/mcp-probe.js +14 -1
  127. package/dist-server/server/mcp-registry.js +43 -0
  128. package/dist-server/server/memory-upkeep.js +94 -84
  129. package/dist-server/server/model-catalog/catalog.js +374 -0
  130. package/dist-server/server/model-catalog/presets.js +176 -0
  131. package/dist-server/server/model-catalog/trim.js +219 -0
  132. package/dist-server/server/model-context-window.js +4 -4
  133. package/dist-server/server/package-export.js +17 -10
  134. package/dist-server/server/package-import.js +6 -3
  135. package/dist-server/server/package-parts.js +3 -1
  136. package/dist-server/server/provider-key-check.js +4 -2
  137. package/dist-server/server/recent-work.js +1 -1
  138. package/dist-server/server/request-auth.js +25 -0
  139. package/dist-server/server/routes/antigravity-leftovers.js +45 -0
  140. package/dist-server/server/routes/bot-memory.js +31 -6
  141. package/dist-server/server/routes/decider.js +19 -0
  142. package/dist-server/server/routes/desktop-viewer.js +194 -0
  143. package/dist-server/server/routine-requests.js +1 -1
  144. package/dist-server/server/routines.js +1 -1
  145. package/dist-server/server/sessions.js +56 -9
  146. package/dist-server/server/shared-computers.js +116 -30
  147. package/dist-server/server/skills.js +25 -10
  148. package/dist-server/server/store.js +79 -124
  149. package/dist-server/server/surface.js +13 -0
  150. package/dist-server/server/system-prompt.js +11 -0
  151. package/dist-server/server/team-backup.js +6 -2
  152. package/dist-server/server/team-manifest.js +11 -3
  153. package/dist-server/server/thread-events.js +2 -0
  154. package/dist-server/server/thread-starters.js +33 -0
  155. package/dist-server/server/thread-title.js +46 -0
  156. package/dist-server/server/tts/elevenlabs.js +11 -6
  157. package/dist-server/server/tts/fish.js +4 -5
  158. package/dist-server/server/tts/index.js +20 -11
  159. package/dist-server/server/vps-computer.js +79 -12
  160. package/dist-server/server/webhooks.js +18 -1
  161. package/dist-server/server/workspace-backup-policy.js +25 -3
  162. package/dist-server/server/workspace-backup.js +28 -8
  163. package/dist-server/server/workspace.js +65 -15
  164. package/dist-server/shared/cloud-home.js +4 -0
  165. package/dist-server/shared/package-format.js +30 -4
  166. package/dist-server/shared/team-backup.js +20 -3
  167. package/dist-server/shared/workspace-backup-client.js +1 -0
  168. package/dist-server/src/lib/citations.js +177 -0
  169. package/dist-server/src/lib/composer-attachments.js +7 -1
  170. package/dist-server/vps-container-mcp.js +278 -45
  171. package/dist-server/workspace-backup.worker.js +184 -38
  172. package/enterprise/server/index.js +5 -1
  173. package/package.json +1 -1
  174. package/dist/assets/channel-BCuyHjFV.js +0 -1
  175. package/dist/assets/index-CRvo-DS2.css +0 -1
  176. package/dist/assets/index-CWYGlA63.js +0 -588
  177. package/dist/assets/swimlanesDiagram-TC7HE7FX-BZQ07DGE.js +0 -8
@@ -41,6 +41,7 @@ export function createBotMemoryRoutes(deps) {
41
41
  return async ({ req, res, url, path, method, auth, json, readBody }) => {
42
42
  /** scratch for route matches, shared by every `path.match` below */
43
43
  let m = null;
44
+ const write = (botId, change) => deps.ownersWrite ? deps.ownersWrite(auth, botId, change) : change();
44
45
  m = path.match(/^\/api\/bots\/([\w-]+)\/memory$/);
45
46
  if (m && method === "GET") {
46
47
  if (!deps.bot(m[1]))
@@ -49,7 +50,9 @@ export function createBotMemoryRoutes(deps) {
49
50
  const overview = memoryOverview(m[1]);
50
51
  // `text` and `truncated` ride along one release for clients of the
51
52
  // old whole-file shape; the panel reads the file through /memory/file
52
- return json(res, 200, { ...overview, text: readMemoryDoc(m[1], MEMORY_INDEX).text, truncated: overview.index.truncated });
53
+ const review = deps.lendingReview?.needed(m[1]);
54
+ return json(res, 200, { ...overview, text: readMemoryDoc(m[1], MEMORY_INDEX).text, truncated: overview.index.truncated,
55
+ ...(review ? { lendingReview: review } : {}) });
53
56
  }
54
57
  catch (error) {
55
58
  return replyMemoryError(res, error);
@@ -65,7 +68,8 @@ export function createBotMemoryRoutes(deps) {
65
68
  if (!parsed.success)
66
69
  return json(res, 400, { error: "text must be a string" });
67
70
  try {
68
- const { doc } = journalMemoryWrite(m[1], MEMORY_INDEX, parsed.data.text, { actor: "person", via: "api" });
71
+ const botId = m[1];
72
+ const { doc } = write(botId, () => journalMemoryWrite(botId, MEMORY_INDEX, parsed.data.text, { actor: "person", via: "api" }));
69
73
  return json(res, 200, { ok: true, hash: doc.hash, truncated: memoryCapacity(doc.text).truncated });
70
74
  }
71
75
  catch (error) {
@@ -76,6 +80,24 @@ export function createBotMemoryRoutes(deps) {
76
80
  return replyMemoryError(res, error);
77
81
  }
78
82
  }
83
+ m = path.match(/^\/api\/bots\/([\w-]+)\/memory\/reviewed$/);
84
+ if (m && method === "POST" && deps.lendingReview) {
85
+ if (!deps.bot(m[1]))
86
+ return json(res, 404, { error: "no such bot" });
87
+ if (!deps.lendingReview.owner(auth)) {
88
+ return json(res, 403, { error: "Only the owner of this Cloud can review this bot's memory, from one of their own devices." });
89
+ }
90
+ // Bound to what the owner was shown: the token names the files exactly
91
+ // as the panel read them. Anything changed since is shown again.
92
+ const parsed = z.object({ token: z.string().regex(/^[a-f0-9]{64}$/) }).safeParse(await readBody(req));
93
+ if (!parsed.success)
94
+ return json(res, 400, { error: "token is required: the lendingReview token the Memory panel was shown" });
95
+ if (!deps.lendingReview.accept(m[1], parsed.data.token)) {
96
+ return json(res, 409, { error: "This bot's memory changed since you looked. Review it again.", code: "changed",
97
+ lendingReview: deps.lendingReview.needed(m[1]) });
98
+ }
99
+ return json(res, 200, { ok: true, lendingReview: false });
100
+ }
79
101
  m = path.match(/^\/api\/bots\/([\w-]+)\/memory\/file$/);
80
102
  if (m && method === "GET") {
81
103
  if (!deps.bot(m[1]))
@@ -96,11 +118,12 @@ export function createBotMemoryRoutes(deps) {
96
118
  if (!parsed.success)
97
119
  return json(res, 400, { error: "text must be a string; path and expectedHash are optional strings" });
98
120
  try {
99
- const { doc, entry } = journalMemoryWrite(m[1], parsed.data.path, parsed.data.text, {
121
+ const botId = m[1];
122
+ const { doc, entry } = write(botId, () => journalMemoryWrite(botId, parsed.data.path, parsed.data.text, {
100
123
  actor: "person",
101
124
  via: "ui",
102
125
  expectedHash: parsed.data.expectedHash,
103
- });
126
+ }));
104
127
  return json(res, 200, { ok: true, ...doc, entry: entry ? journalEntryForClient(m[1], entry) : null, overview: memoryOverview(m[1]) });
105
128
  }
106
129
  catch (error) {
@@ -112,7 +135,8 @@ export function createBotMemoryRoutes(deps) {
112
135
  return json(res, 404, { error: "no such bot" });
113
136
  const file = url.searchParams.get("path") ?? "";
114
137
  try {
115
- const entry = journalMemoryDelete(m[1], file, { actor: "person", via: "ui" });
138
+ const botId = m[1];
139
+ const entry = write(botId, () => journalMemoryDelete(botId, file, { actor: "person", via: "ui" }));
116
140
  return json(res, 200, { ok: true, path: file, entry: entry ? journalEntryForClient(m[1], entry) : null, overview: memoryOverview(m[1]) });
117
141
  }
118
142
  catch (error) {
@@ -134,7 +158,8 @@ export function createBotMemoryRoutes(deps) {
134
158
  if (!deps.bot(m[1]))
135
159
  return json(res, 404, { error: "no such bot" });
136
160
  await flushMemoryJournal(m[1]);
137
- const result = revertMemoryChange(m[1], m[2]);
161
+ const botId = m[1], changeId = m[2];
162
+ const result = write(botId, () => revertMemoryChange(botId, changeId));
138
163
  if (!result.ok)
139
164
  return json(res, result.status, { error: result.error });
140
165
  return json(res, 200, { ok: true, ...result.doc, entry: result.entry ? journalEntryForClient(m[1], result.entry) : null, overview: memoryOverview(m[1]) });
@@ -0,0 +1,19 @@
1
+ import { PASS } from "./table.js";
2
+ const MAX_KEY_LENGTH = 512;
3
+ export function createDeciderRoutes(deps) {
4
+ return async ({ req, res, path, method, json, readBody }) => {
5
+ if (path !== "/api/decider/test" || method !== "POST")
6
+ return PASS;
7
+ const body = await readBody(req, 8192);
8
+ if (body?.key !== undefined && typeof body.key !== "string")
9
+ return json(res, 400, { error: "key must be a string" });
10
+ const draft = typeof body?.key === "string" ? body.key.trim() : "";
11
+ if (draft.length > MAX_KEY_LENGTH)
12
+ return json(res, 400, { error: "That does not look like an API key." });
13
+ res.setHeader("cache-control", "no-store");
14
+ const result = await deps.decider.testKey(draft ? { key: draft } : {});
15
+ if (result.ok)
16
+ return json(res, 200, { ok: true, latencyMs: result.latencyMs });
17
+ return json(res, 200, { ok: false, reason: result.reason, ...(result.status ? { status: result.status } : {}) });
18
+ };
19
+ }
@@ -0,0 +1,194 @@
1
+ // Only the RFB WebSocket crosses the container boundary. The viewer page is
2
+ // bundled with the app: serving container-controlled JavaScript on the app's
3
+ // origin would give a bot access to the person's authenticated workspace.
4
+ import { createHash } from "node:crypto";
5
+ import { request, ServerResponse } from "node:http";
6
+ import { Socket } from "node:net";
7
+ import { isSameOrigin } from "../request-auth.js";
8
+ import { PASS } from "./table.js";
9
+ const ROUTE = /^\/api\/desktop-viewer\/(local\/(?:shared|bot-[a-f0-9]{64}|pool-\d+)|vps\/[\w-]+)(\/websockify)?$/;
10
+ const HANDSHAKE_MS = 10_000;
11
+ const RECHECK_MS = 5_000;
12
+ const WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
13
+ export function desktopViewerUrl(target, threadId) {
14
+ const params = new URLSearchParams({ target, ...(threadId ? { threadId } : {}) });
15
+ return `/desktop-viewer#${params}`;
16
+ }
17
+ export function createDesktopViewer(deps) {
18
+ const upgrades = new Map();
19
+ let stopped = false;
20
+ /** Run upgrades through the very same authentication, hosted-workspace and
21
+ * maintenance gates as HTTP. Only this route can detach the response socket. */
22
+ function attach(server, handle) {
23
+ server.on("upgrade", (req, socket, head) => {
24
+ if (stopped || !(socket instanceof Socket)) {
25
+ socket.destroy();
26
+ return;
27
+ }
28
+ const res = new ServerResponse(req);
29
+ res.assignSocket(socket);
30
+ // Rejected upgrades end this socket; do not let HTTP clients pool it.
31
+ res.setHeader("connection", "close");
32
+ socket.on("error", () => socket.destroy());
33
+ res.once("finish", () => socket.end(() => socket.destroy()));
34
+ let path;
35
+ try {
36
+ path = new URL(req.url ?? "", "http://localhost").pathname;
37
+ }
38
+ catch {
39
+ res.writeHead(400).end();
40
+ return;
41
+ }
42
+ if (!ROUTE.exec(path)?.[2]) {
43
+ res.writeHead(404).end();
44
+ return;
45
+ }
46
+ // Keep reading while auth/inspection awaits, so a closed tab's FIN is
47
+ // observed. Bound and preserve any eagerly sent WebSocket bytes.
48
+ const upgrade = { socket, head, release: () => socket.off("data", buffer), close: () => socket.destroy() };
49
+ const buffer = (data) => {
50
+ if (upgrade.head.length + data.length > 64 * 1024)
51
+ socket.destroy();
52
+ else
53
+ upgrade.head = Buffer.concat([upgrade.head, data]);
54
+ };
55
+ socket.on("data", buffer);
56
+ socket.once("end", () => socket.destroy());
57
+ socket.once("close", () => { upgrade.release(); upgrades.delete(req); });
58
+ upgrades.set(req, upgrade);
59
+ const timeout = setTimeout(() => socket.destroy(), HANDSHAKE_MS);
60
+ timeout.unref();
61
+ socket.once("close", () => clearTimeout(timeout));
62
+ void handle(req, res).catch(() => {
63
+ if (!res.headersSent)
64
+ res.writeHead(500).end();
65
+ else
66
+ socket.destroy();
67
+ }).finally(() => clearTimeout(timeout));
68
+ });
69
+ }
70
+ const route = async ({ req, res, path, method, auth, json }) => {
71
+ const match = ROUTE.exec(path);
72
+ if (!match)
73
+ return PASS;
74
+ res.setHeader("cache-control", "private, no-store");
75
+ if (method !== "GET")
76
+ return json(res, 405, { error: "method not allowed" });
77
+ // Also enforce at this boundary; the central gate defaults these paths
78
+ // to admin, like the existing Local VM status and control endpoints.
79
+ if (!auth.scopes.includes("admin") || !isSameOrigin(req))
80
+ return json(res, 403, { error: "forbidden" });
81
+ const target = deps.target(match[1]);
82
+ if (!target)
83
+ return json(res, 404, { error: "Desktop not found" });
84
+ const upgrade = upgrades.get(req);
85
+ if (upgrade && auth.kind === "session")
86
+ upgrade.owner = auth.session.id;
87
+ let connection;
88
+ try {
89
+ connection = await target.resolve();
90
+ }
91
+ catch (error) {
92
+ const status = error && typeof error === "object" && "status" in error ? error.status : 502;
93
+ return json(res, typeof status === "number" && status >= 400 && status < 600 ? status : 502, { error: "The desktop is not available. Open it again from OpenMausBot." });
94
+ }
95
+ // Inspection may outlive a closed tab or the handshake deadline.
96
+ if (res.destroyed || upgrade?.socket.destroyed)
97
+ return;
98
+ const live = () => deps.live(auth) && deps.target(match[1])?.key === target.key && (connection.live?.() ?? true);
99
+ if (!live())
100
+ return json(res, 401, { error: "Viewer access expired" });
101
+ if (!Number.isInteger(connection.port) || connection.port < 1 || connection.port > 65535) {
102
+ return json(res, 409, { error: "The desktop viewer is not available." });
103
+ }
104
+ if (!match[2])
105
+ return json(res, 200, { password: connection.password });
106
+ if (!upgrade)
107
+ return json(res, 426, { error: "WebSocket upgrade required" });
108
+ const key = req.headers["sec-websocket-key"];
109
+ if (req.headers.upgrade?.toLowerCase() !== "websocket" || req.headers["sec-websocket-version"] !== "13"
110
+ || typeof key !== "string" || !/^[A-Za-z0-9+/]{22}==$/.test(key)) {
111
+ return json(res, 400, { error: "Invalid WebSocket handshake" });
112
+ }
113
+ // Fixed host and path, port from an inspected managed container. Never
114
+ // forward cookies, bearer tokens, query parameters or forwarded headers.
115
+ const upstream = request({
116
+ hostname: "127.0.0.1", port: connection.port, path: "/websockify",
117
+ headers: { Upgrade: "websocket", Connection: "Upgrade", "Sec-WebSocket-Key": key, "Sec-WebSocket-Version": "13" },
118
+ });
119
+ const { socket } = upgrade;
120
+ let peer;
121
+ let recheck;
122
+ const release = connection.retain?.();
123
+ let closed = false;
124
+ const timeout = setTimeout(() => fail(), HANDSHAKE_MS);
125
+ timeout.unref();
126
+ upgrade.close = () => {
127
+ if (closed)
128
+ return;
129
+ closed = true;
130
+ release?.();
131
+ clearTimeout(timeout);
132
+ clearInterval(recheck);
133
+ upstream.destroy();
134
+ peer?.destroy();
135
+ socket.destroy();
136
+ };
137
+ const fail = () => {
138
+ if (closed)
139
+ return;
140
+ if (peer || socket.destroyed) {
141
+ upgrade.close();
142
+ return;
143
+ }
144
+ if (!res.headersSent && !socket.destroyed)
145
+ json(res, 502, { error: "Could not connect to the desktop viewer" });
146
+ upstream.destroy();
147
+ };
148
+ socket.once("close", upgrade.close);
149
+ upstream.once("error", fail);
150
+ upstream.once("response", (answer) => { answer.resume(); fail(); });
151
+ upstream.once("upgrade", (answer, remote, remoteHead) => {
152
+ const accept = createHash("sha1").update(key + WS_GUID).digest("base64");
153
+ if (!live() || socket.destroyed || answer.headers["sec-websocket-accept"] !== accept
154
+ || answer.headers.upgrade?.toLowerCase() !== "websocket") {
155
+ remote.destroy();
156
+ fail();
157
+ return;
158
+ }
159
+ clearTimeout(timeout);
160
+ peer = remote;
161
+ remote.once("error", upgrade.close);
162
+ remote.once("close", upgrade.close);
163
+ upgrade.release();
164
+ res.detachSocket(socket);
165
+ socket.write(`HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ${accept}\r\n\r\n`);
166
+ if (remoteHead.length)
167
+ socket.write(remoteHead);
168
+ if (upgrade.head.length)
169
+ remote.write(upgrade.head);
170
+ upgrade.head = Buffer.alloc(0);
171
+ socket.pipe(remote).pipe(socket);
172
+ connection.touch?.();
173
+ recheck = setInterval(() => {
174
+ if (!live())
175
+ upgrade.close();
176
+ else
177
+ connection.touch?.();
178
+ }, RECHECK_MS);
179
+ recheck.unref();
180
+ });
181
+ upstream.end();
182
+ };
183
+ return {
184
+ route, attach,
185
+ closeForOwner: (owner) => { for (const upgrade of upgrades.values())
186
+ if (upgrade.owner === owner)
187
+ upgrade.close(); },
188
+ closeAll: () => {
189
+ stopped = true;
190
+ for (const upgrade of upgrades.values())
191
+ upgrade.close();
192
+ },
193
+ };
194
+ }
@@ -468,7 +468,7 @@ function normalizedOperation(manager, botId, validated, now) {
468
468
  ...(validated.forBot ? { forBot: validated.forBot } : {}),
469
469
  };
470
470
  }
471
- function asSchedule(schedule, now) {
471
+ export function asSchedule(schedule, now) {
472
472
  if (schedule.type === "cron")
473
473
  return { ...schedule };
474
474
  if (schedule.type === "once")
@@ -1293,7 +1293,7 @@ export class RoutineManager {
1293
1293
  ? run.groupId
1294
1294
  ? this.options.createGoalTask?.(run.groupId, run.routineName) ?? null
1295
1295
  : null
1296
- : this.options.createTask(run.botId, run.routineName, run.triggerSource === "webhook");
1296
+ : this.options.createTask(run.botId, run.routineName, run.triggerSource === "webhook", run.routineId);
1297
1297
  if (!task) {
1298
1298
  this.failRun(run, run.target === "room-goal"
1299
1299
  ? "Could not create a room task for this goal"
@@ -72,9 +72,18 @@ const sessionSchema = z.object({
72
72
  email: z.string().max(320).optional(),
73
73
  /** Set only by the internal verified-portal issuance path, never by email or pairing input. */
74
74
  membershipAuthority: z.literal("portal").optional(),
75
+ /** A browser sign-in (PairingCode `browser`): accepted only as its browser's cookie, never as a bearer token. */
76
+ cookieOnly: z.literal(true).optional(),
77
+ /** Whose OMB Cloud a browser sign-in signed in to (its PairingCode `owner`). */
78
+ owner: z.string().max(254).optional(),
75
79
  });
76
80
  const fileSchema = z.object({ version: z.literal(1), sessions: z.array(sessionSchema) });
77
81
  const sha256 = (value) => createHash("sha256").update(value).digest("hex");
82
+ /** A device's name as the sessions list shows it: what a client sent, less
83
+ * control and bidirectional-formatting characters. */
84
+ function deviceLabel(value) {
85
+ return (value ?? "").replace(/[\p{Cc}\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/gu, "").trim();
86
+ }
78
87
  function sameDigest(a, b) {
79
88
  const left = Buffer.from(a, "hex");
80
89
  const right = Buffer.from(b, "hex");
@@ -130,6 +139,8 @@ function publicSession(record) {
130
139
  };
131
140
  if (record.email)
132
141
  view.email = record.email;
142
+ if (record.owner)
143
+ view.owner = record.owner;
133
144
  return view;
134
145
  }
135
146
  export class SessionRegistry {
@@ -144,6 +155,8 @@ export class SessionRegistry {
144
155
  options;
145
156
  openMarker;
146
157
  closed = false;
158
+ /** Set on an OMB Cloud home (requireAdmin): only admin scope, anywhere. */
159
+ adminOnly = null;
147
160
  // No parameter properties: the server runs this file under Node's
148
161
  // strip-only TypeScript mode, which only erases types.
149
162
  constructor(options) {
@@ -252,6 +265,20 @@ export class SessionRegistry {
252
265
  for (const listener of this.onRevoked)
253
266
  listener(sessionId);
254
267
  }
268
+ /** From now on no pairing window, sign-in or session without admin scope
269
+ * is opened, redeemed, issued or accepted; `refusal` says why (an OMB
270
+ * Cloud home is personal: server/cloud-owner.ts). Sessions stored without
271
+ * it stop authenticating; the caller revokes them. */
272
+ requireAdmin(refusal) {
273
+ this.adminOnly = refusal;
274
+ }
275
+ personal(scopes) {
276
+ return this.adminOnly !== null && !scopes.includes("admin");
277
+ }
278
+ refusePersonal(scopes) {
279
+ if (this.personal(scopes))
280
+ throw Object.assign(new Error(this.adminOnly), { status: 403, code: "cloud_personal" });
281
+ }
255
282
  // ── pairing ────────────────────────────────────────────────────────────
256
283
  openPairing(input = {}) {
257
284
  this.prune();
@@ -259,6 +286,7 @@ export class SessionRegistry {
259
286
  const code = generatePairingCode();
260
287
  const credential = generatePairingCredential();
261
288
  const scopes = input.scopes?.length ? [...new Set(input.scopes)] : [...SCOPES];
289
+ this.refusePersonal(scopes);
262
290
  const pairing = {
263
291
  id: randomUUID(),
264
292
  codeHash: sha256(code),
@@ -267,6 +295,7 @@ export class SessionRegistry {
267
295
  label: (input.label ?? "").trim().slice(0, 80),
268
296
  createdAt: now,
269
297
  expiresAt: now + (input.ttlMs ?? PAIRING_CODE_TTL_MS),
298
+ ...(input.browser ? { browser: true, ...(input.owner ? { owner: input.owner } : {}) } : {}),
270
299
  };
271
300
  this.pairings.push(pairing);
272
301
  return { id: pairing.id, code, credential, expiresAt: pairing.expiresAt };
@@ -275,6 +304,17 @@ export class SessionRegistry {
275
304
  this.prune();
276
305
  return this.pairings.map((p) => ({ id: p.id, label: p.label, scopes: [...p.scopes], createdAt: p.createdAt, expiresAt: p.expiresAt }));
277
306
  }
307
+ /** What a browser sign-in's page shows before anything is redeemed: whose
308
+ * Cloud it signs in to. Only a browser sign-in's 256-bit credential can
309
+ * match, so this consumes nothing and counts toward no lockout. */
310
+ previewBrowserSignIn(credential) {
311
+ this.prune();
312
+ if (!isPairingCredential(credential))
313
+ return null;
314
+ const presented = sha256(credential);
315
+ const pairing = this.pairings.find((p) => p.browser === true && sameDigest(p.credentialHash, presented));
316
+ return pairing ? { owner: pairing.owner ?? null, expiresAt: pairing.expiresAt } : null;
317
+ }
278
318
  cancelPairing(id) {
279
319
  const before = this.pairings.length;
280
320
  this.pairings = this.pairings.filter((p) => p.id !== id);
@@ -313,7 +353,8 @@ export class SessionRegistry {
313
353
  * the lockout (an IP); `label` names the device in the sessions list. */
314
354
  /** `label` is what the client asked to be called; the code's own label
315
355
  * (set by whoever minted it) comes next; `fallbackLabel` (derived from the
316
- * user agent) last. */
356
+ * user agent) last. `browser`: this is a browser sign-in (PairingCode
357
+ * `browser`), which redeems only a window opened for one. */
317
358
  exchange(input) {
318
359
  this.prune();
319
360
  const now = this.now();
@@ -323,8 +364,9 @@ export class SessionRegistry {
323
364
  const presented = isPairingCredential(input.code)
324
365
  ? sha256(input.code)
325
366
  : sha256(normalizePairingCode(input.code));
367
+ const browser = input.browser === true;
326
368
  const attemptId = typeof input.attemptId === "string" && /^[\w-]{8,64}$/.test(input.attemptId) ? input.attemptId : null;
327
- const replay = attemptId ? this.replays.find((r) => r.attemptId === attemptId && sameDigest(r.codeHash, presented)) : undefined;
369
+ const replay = attemptId ? this.replays.find((r) => r.attemptId === attemptId && r.browser === browser && sameDigest(r.codeHash, presented)) : undefined;
328
370
  if (replay)
329
371
  return replay.result;
330
372
  const lock = this.lockState(input.source);
@@ -332,31 +374,35 @@ export class SessionRegistry {
332
374
  const seconds = Math.ceil(lock.retryAfterMs / 1000);
333
375
  return { ok: false, status: 429, error: `too many failed pairing attempts from your address; try again in ${seconds}s` };
334
376
  }
335
- const index = this.pairings.findIndex((p) => sameDigest(p.codeHash, presented) || sameDigest(p.credentialHash, presented));
377
+ const index = this.pairings.findIndex((p) => (p.browser === true) === browser &&
378
+ (sameDigest(p.credentialHash, presented) || (!p.browser && sameDigest(p.codeHash, presented))));
336
379
  if (index < 0) {
337
380
  this.recordFailure(input.source);
338
381
  return { ok: false, status: 401, error: "pairing code is wrong or has expired; create a new one on the server" };
339
382
  }
340
383
  const [pairing] = this.pairings.splice(index, 1); // single use
341
384
  this.failures.delete(input.source);
385
+ if (this.personal(pairing.scopes))
386
+ return { ok: false, status: 403, error: this.adminOnly };
342
387
  const token = `omb_sess_${randomBytes(32).toString("base64url")}`;
343
388
  const record = {
344
389
  id: randomUUID(),
345
390
  tokenHash: sha256(token),
346
- label: (input.label.trim() || pairing.label || input.fallbackLabel?.trim() || "Unnamed device").slice(0, 80),
391
+ label: (deviceLabel(input.label) || deviceLabel(pairing.label) || deviceLabel(input.fallbackLabel) || "Unnamed device").slice(0, 80),
347
392
  scopes: [...pairing.scopes],
348
393
  createdAt: now,
349
394
  lastSeenAt: now,
350
395
  // The absolute cap applies from the first term, so a TTL configured
351
396
  // longer than the cap does not hand out a session the cap forbids.
352
397
  expiresAt: now + Math.min(SESSION_TTL_MS, SESSION_MAX_AGE_MS),
398
+ ...(pairing.browser ? { cookieOnly: true, ...(pairing.owner ? { owner: pairing.owner } : {}) } : {}),
353
399
  };
354
400
  this.sessions.push(record);
355
401
  this.lastSeenWrites.set(record.id, now); // the exchange itself was the first sighting
356
402
  this.persist();
357
403
  const result = { ok: true, token, session: publicSession(record) };
358
404
  if (attemptId)
359
- this.replays.push({ codeHash: presented, attemptId, result, expiresAt: now + EXCHANGE_REPLAY_MS });
405
+ this.replays.push({ codeHash: presented, attemptId, browser, result, expiresAt: now + EXCHANGE_REPLAY_MS });
360
406
  return result;
361
407
  }
362
408
  /** A session from a verified account sign-in (server/account-signin.ts)
@@ -372,13 +418,14 @@ export class SessionRegistry {
372
418
  return this.issueAccount({ label: "Hosted workspace", email: input.email, userId: `portal:${input.grant}`, scopes: input.scopes }, "portal");
373
419
  }
374
420
  issueAccount(input, membershipAuthority) {
421
+ this.refusePersonal(input.scopes);
375
422
  this.prune();
376
423
  const now = this.now();
377
424
  const token = `omb_sess_${randomBytes(32).toString("base64url")}`;
378
425
  const record = {
379
426
  id: randomUUID(),
380
427
  tokenHash: sha256(token),
381
- label: (input.label.trim() || "Unnamed device").slice(0, 80),
428
+ label: (deviceLabel(input.label) || "Unnamed device").slice(0, 80),
382
429
  scopes: [...new Set(input.scopes)],
383
430
  createdAt: now,
384
431
  lastSeenAt: now,
@@ -457,7 +504,7 @@ export class SessionRegistry {
457
504
  const hash = sha256(token);
458
505
  const now = this.now();
459
506
  const record = this.sessions.find((s) => sameDigest(s.tokenHash, hash));
460
- if (!record || record.expiresAt <= now)
507
+ if (!record || record.expiresAt <= now || this.personal(record.scopes))
461
508
  return null;
462
509
  const lastWrite = this.lastSeenWrites.get(record.id) ?? 0;
463
510
  if (now - lastWrite >= LAST_SEEN_WRITE_INTERVAL_MS) {
@@ -493,7 +540,7 @@ export class SessionRegistry {
493
540
  /** Still valid right now (prunes expiry first). */
494
541
  isLive(sessionId) {
495
542
  this.prune();
496
- return this.sessions.some((s) => s.id === sessionId);
543
+ return this.sessions.some((s) => s.id === sessionId && !this.personal(s.scopes));
497
544
  }
498
545
  list() {
499
546
  this.prune();
@@ -529,6 +576,6 @@ export class SessionRegistry {
529
576
  this.tickets.delete(hash);
530
577
  const now = this.now();
531
578
  const record = this.sessions.find((s) => s.id === entry.sessionId);
532
- return record && record.expiresAt > now ? record : null;
579
+ return record && record.expiresAt > now && !this.personal(record.scopes) ? record : null;
533
580
  }
534
581
  }