openmausbot 0.1.89 → 0.1.91

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/dist/assets/{abnfDiagram-O67JEVCF-BwaMaZEm.js → abnfDiagram-O67JEVCF-yP-w8pRq.js} +1 -1
  2. package/dist/assets/{arc-Be-8GKxd.js → arc-DsivfZEh.js} +1 -1
  3. package/dist/assets/{architectureDiagram-NJMV4G6O-Dp_U2Kv1.js → architectureDiagram-NJMV4G6O-CILw7lzT.js} +1 -1
  4. package/dist/assets/{blockDiagram-BEXU5L5S-DQjUjYUk.js → blockDiagram-BEXU5L5S-DEQUV3HF.js} +1 -1
  5. package/dist/assets/{c4Diagram-YGBWAQC7-Bd3aE4Kc.js → c4Diagram-YGBWAQC7-lSXCZoKA.js} +1 -1
  6. package/dist/assets/channel-DrfGMu7X.js +1 -0
  7. package/dist/assets/{chunk-2Q5K7J3B-BijjTBeJ.js → chunk-2Q5K7J3B-QCD5bgWP.js} +1 -1
  8. package/dist/assets/{chunk-3FUC2YCW-CMGdwvzW.js → chunk-3FUC2YCW-C5-iZJuL.js} +2 -2
  9. package/dist/assets/{chunk-5VM5RSS4-0G09BfsZ.js → chunk-5VM5RSS4-cxsawMpY.js} +1 -1
  10. package/dist/assets/{chunk-GTNCS2PH-FOp3vSHe.js → chunk-GTNCS2PH-DV8tV8H7.js} +1 -1
  11. package/dist/assets/{chunk-GWA4HPMP-Dv-20VIn.js → chunk-GWA4HPMP-CJFZyfbW.js} +1 -1
  12. package/dist/assets/{chunk-JWPE2WC7-CRgrdFcz.js → chunk-JWPE2WC7-BigbMXco.js} +1 -1
  13. package/dist/assets/{chunk-NETBCI7D-3hP4f7QB.js → chunk-NETBCI7D-oVxTfpDy.js} +1 -1
  14. package/dist/assets/{chunk-WEXAMYUT-CG98IjlG.js → chunk-WEXAMYUT-CH4BiroW.js} +1 -1
  15. package/dist/assets/{chunk-XXDRQBXY-BrSTC9xG.js → chunk-XXDRQBXY-KEDa6gax.js} +1 -1
  16. package/dist/assets/{classDiagram-v2-NBCMYWYE-rpQQQDFX.js → classDiagram-v2-NBCMYWYE-BiPjfqVv.js} +1 -1
  17. package/dist/assets/{cose-bilkent-JH36ORCC-G6RSafHA.js → cose-bilkent-JH36ORCC-Caxew3OY.js} +1 -1
  18. package/dist/assets/{cynefin-OW5HDTMX-CXjv-IM2.js → cynefin-OW5HDTMX-B_Pui369.js} +1 -1
  19. package/dist/assets/{cynefinDiagram-VND7K2PF-Dw07MR9f.js → cynefinDiagram-VND7K2PF-X64ydIhP.js} +1 -1
  20. package/dist/assets/{dagre-6A5THRUB-BwPnlTg2.js → dagre-6A5THRUB-TqYCI1E1.js} +1 -1
  21. package/dist/assets/{diagram-22UHCM2B-Cm6xcMiz.js → diagram-22UHCM2B-TeOt3rAQ.js} +1 -1
  22. package/dist/assets/{diagram-3UASUU5V-CDpogPbE.js → diagram-3UASUU5V-D70BLhDA.js} +1 -1
  23. package/dist/assets/{diagram-ATOU4E4O-BV8z3Pnc.js → diagram-ATOU4E4O-BbQDLwUC.js} +1 -1
  24. package/dist/assets/{diagram-CDSNMT55-CKCQhOOC.js → diagram-CDSNMT55-ChNJZ1u-.js} +1 -1
  25. package/dist/assets/{diagram-MLGK6HIB-FGvJg69C.js → diagram-MLGK6HIB-DMBl2APa.js} +1 -1
  26. package/dist/assets/{diagram-MPIPVDR6-CwATvgnS.js → diagram-MPIPVDR6-CEqDchOp.js} +1 -1
  27. package/dist/assets/{ebnfDiagram-ZINNZB2B-CH_c3SSC.js → ebnfDiagram-ZINNZB2B-BVDKRp_-.js} +1 -1
  28. package/dist/assets/{elk-276RUBZZ-3guZi71R.js → elk-276RUBZZ-CRkofO9B.js} +1 -1
  29. package/dist/assets/{erDiagram-OPXOYQCR-BMHphR2n.js → erDiagram-OPXOYQCR-DruOMh5e.js} +1 -1
  30. package/dist/assets/{flowDiagram-KWPJA3E3-Vm5DkW5x.js → flowDiagram-KWPJA3E3-8GMQeB7t.js} +1 -1
  31. package/dist/assets/{ganttDiagram-FUAMR5RP-DO-1ct07.js → ganttDiagram-FUAMR5RP-ORE9jcsM.js} +1 -1
  32. package/dist/assets/{gitGraphDiagram-X574FWY7-nxeksEIN.js → gitGraphDiagram-X574FWY7-BmsedsWh.js} +1 -1
  33. package/dist/assets/index-BlnCEcU6.js +588 -0
  34. package/dist/assets/{index-Oxk9x45i.js → index-DKNEM8Q6.js} +1 -1
  35. package/dist/assets/index-DVxjOvID.css +1 -0
  36. package/dist/assets/{infoDiagram-VRGFBTTK-Dkcvr0p7.js → infoDiagram-VRGFBTTK-E_p7AooJ.js} +1 -1
  37. package/dist/assets/{ishikawaDiagram-OU5B5YK6-BwVibkoO.js → ishikawaDiagram-OU5B5YK6-Y-B2mKda.js} +1 -1
  38. package/dist/assets/{journeyDiagram-ZHPQQLJL-ByiBZqgG.js → journeyDiagram-ZHPQQLJL-eA8RELcK.js} +1 -1
  39. package/dist/assets/{kanban-definition-PNTS6WVX-qqjgXBF3.js → kanban-definition-PNTS6WVX-DqIL2e8e.js} +1 -1
  40. package/dist/assets/{linear-CtJnKe6A.js → linear-yC_29R6e.js} +1 -1
  41. package/dist/assets/{mermaid.core-BEiDklrY.js → mermaid.core-VhEMkSND.js} +5 -5
  42. package/dist/assets/{mindmap-definition-NLK3R4M7-DfIYHia9.js → mindmap-definition-NLK3R4M7-oJ7kyQZy.js} +1 -1
  43. package/dist/assets/{pegDiagram-GJSIUBJH-CYyRqa1a.js → pegDiagram-GJSIUBJH-BmfYcY0O.js} +1 -1
  44. package/dist/assets/{pieDiagram-5QR66LMP-hOToy_zH.js → pieDiagram-5QR66LMP-DKvgP7w9.js} +1 -1
  45. package/dist/assets/{quadrantDiagram-O4NWA36T-DKAsNN9I.js → quadrantDiagram-O4NWA36T-BEKL3W0L.js} +1 -1
  46. package/dist/assets/{railroadDiagram-XR7U4H2S-BSA-d8JU.js → railroadDiagram-XR7U4H2S-BEsiqR5s.js} +1 -1
  47. package/dist/assets/{requirementDiagram-PLB6GJNP-CSmlvb-p.js → requirementDiagram-PLB6GJNP-DWBcWvAP.js} +1 -1
  48. package/dist/assets/{sankeyDiagram-IPEJSGJF-CD5F6fnG.js → sankeyDiagram-IPEJSGJF-rpZ2_O4s.js} +1 -1
  49. package/dist/assets/{sequenceDiagram-PO4LG4MO-JUq5sx6d.js → sequenceDiagram-PO4LG4MO-B99HQdJR.js} +1 -1
  50. package/dist/assets/{sizeCapture-INFHLROL-BjIS-Stk.js → sizeCapture-INFHLROL-BjN0qT5T.js} +1 -1
  51. package/dist/assets/{stateDiagram-v2-GCMORJYK-DklkUxOg.js → stateDiagram-v2-GCMORJYK-BbsVgGsC.js} +1 -1
  52. package/dist/assets/{swimlanes-2SLR337P-CsjIU6be.js → swimlanes-2SLR337P-DG3gKkeR.js} +1 -1
  53. package/dist/assets/swimlanesDiagram-TC7HE7FX-BCZhxl7e.js +8 -0
  54. package/dist/assets/{timeline-definition-EJHVYXUP-BkFqK7AB.js → timeline-definition-EJHVYXUP-BTHOdtF_.js} +1 -1
  55. package/dist/assets/{usecaseDiagram-POWQR4AR-70gJB1EU.js → usecaseDiagram-POWQR4AR-acLSLFwc.js} +1 -1
  56. package/dist/assets/{vennDiagram-UO4OBE2U-83Tm62dN.js → vennDiagram-UO4OBE2U-C5JzNX9G.js} +1 -1
  57. package/dist/assets/{wardleyDiagram-VNRHLVJA-tTmseoX9.js → wardleyDiagram-VNRHLVJA-DL1-PrR_.js} +1 -1
  58. package/dist/assets/{xychartDiagram-PMCCYNJV-R1LBA55a.js → xychartDiagram-PMCCYNJV-Bvr1477p.js} +1 -1
  59. package/dist/index.html +2 -2
  60. package/dist-server/cloud-home-start.js +14810 -0
  61. package/dist-server/container-mcp.js +136 -36
  62. package/dist-server/drivers/agents-proxy.js +31 -10
  63. package/dist-server/index.js +48251 -44124
  64. package/dist-server/openmausbot.js +628 -292
  65. package/dist-server/pair-cli.js +628 -292
  66. package/dist-server/permission-proxy.js +1 -1
  67. package/dist-server/server/admission.js +46 -0
  68. package/dist-server/server/{box-create-idempotency.js → boat-create-idempotency.js} +32 -31
  69. package/dist-server/server/{box-delete-journal.js → boat-delete-journal.js} +26 -25
  70. package/dist-server/server/{box.js → boat.js} +381 -334
  71. package/dist-server/server/bot-overview.js +7 -4
  72. package/dist-server/server/bot-profile.js +7 -1
  73. package/dist-server/server/channel-queue.js +58 -18
  74. package/dist-server/server/checkpoints.js +98 -52
  75. package/dist-server/server/claim-idle.js +33 -0
  76. package/dist-server/server/cloud-backend.js +5 -5
  77. package/dist-server/server/cloud-home-start.js +121 -0
  78. package/dist-server/server/cloud-home.js +241 -0
  79. package/dist-server/server/cloud-lending.js +103 -0
  80. package/dist-server/server/cloud-move-http.js +268 -0
  81. package/dist-server/server/cloud-move.js +418 -0
  82. package/dist-server/server/cloud-overflow.js +167 -0
  83. package/dist-server/server/composio.js +5 -0
  84. package/dist-server/server/computer-observation.js +2 -2
  85. package/dist-server/server/config.js +156 -9
  86. package/dist-server/server/container-computer.js +13 -0
  87. package/dist-server/server/control-client.js +2 -2
  88. package/dist-server/server/decider/index.js +238 -0
  89. package/dist-server/server/decider/jev.js +183 -0
  90. package/dist-server/server/decider/log.js +62 -0
  91. package/dist-server/server/decider/relay.js +52 -0
  92. package/dist-server/server/decider/room-routing.js +88 -0
  93. package/dist-server/server/decider/types.js +5 -0
  94. package/dist-server/server/delegations.js +11 -3
  95. package/dist-server/server/drivers/acp/core.js +5 -4
  96. package/dist-server/server/drivers/agents-call.js +5 -1
  97. package/dist-server/server/drivers/agents-catalog.js +25 -3
  98. package/dist-server/server/drivers/{boxagent.js → boatagent.js} +30 -21
  99. package/dist-server/server/drivers/builtIn.js +2 -2
  100. package/dist-server/server/drivers/{chat-box-tools.js → chat-boat-tools.js} +3 -3
  101. package/dist-server/server/drivers/chat-mcp-tools.js +2 -2
  102. package/dist-server/server/drivers/claude.js +178 -13
  103. package/dist-server/server/drivers/native.js +1 -1
  104. package/dist-server/server/drivers/openai-chat.js +15 -4
  105. package/dist-server/server/drivers/pi.js +3 -3
  106. package/dist-server/server/group-goal-run.js +6 -4
  107. package/dist-server/server/harness-capabilities.js +2 -0
  108. package/dist-server/server/hosted-models.js +6 -1
  109. package/dist-server/server/included-services.js +70 -0
  110. package/dist-server/server/index.js +1462 -576
  111. package/dist-server/server/local-vm-inventory.js +11 -1
  112. package/dist-server/server/local-vm-seat-pool.js +141 -0
  113. package/dist-server/server/managed-policy.js +4 -1
  114. package/dist-server/server/memory-capture.js +194 -0
  115. package/dist-server/server/memory-entries.js +76 -0
  116. package/dist-server/server/memory-organize.js +102 -0
  117. package/dist-server/server/memory-tidy.js +144 -0
  118. package/dist-server/server/memory-topics.js +169 -0
  119. package/dist-server/server/memory-upkeep.js +452 -0
  120. package/dist-server/server/message-db.js +49 -5
  121. package/dist-server/server/model-context-window.js +4 -4
  122. package/dist-server/server/package-export.js +17 -10
  123. package/dist-server/server/package-import.js +6 -3
  124. package/dist-server/server/package-parts.js +3 -1
  125. package/dist-server/server/permission-proxy.js +3 -3
  126. package/dist-server/server/profile-learned.js +98 -0
  127. package/dist-server/server/profile-versions.js +45 -11
  128. package/dist-server/server/recall.js +172 -0
  129. package/dist-server/server/redact.js +1 -1
  130. package/dist-server/server/request-auth.js +2 -0
  131. package/dist-server/server/room-cwd.js +1 -1
  132. package/dist-server/server/room-handoffs.js +61 -6
  133. package/dist-server/server/routes/bot-memory.js +215 -0
  134. package/dist-server/server/routes/decider.js +19 -0
  135. package/dist-server/server/routine-requests.js +27 -16
  136. package/dist-server/server/routines.js +10 -6
  137. package/dist-server/server/shared-computers.js +116 -30
  138. package/dist-server/server/steer-queue.js +41 -10
  139. package/dist-server/server/store.js +42 -3
  140. package/dist-server/server/surface.js +14 -1
  141. package/dist-server/server/system-prompt.js +16 -5
  142. package/dist-server/server/team-backup.js +6 -2
  143. package/dist-server/server/team-computers.js +2 -2
  144. package/dist-server/server/team-manifest.js +11 -3
  145. package/dist-server/server/tightening-requests.js +34 -4
  146. package/dist-server/server/tool-summary.js +2 -2
  147. package/dist-server/server/tts/elevenlabs.js +12 -7
  148. package/dist-server/server/tts/index.js +17 -10
  149. package/dist-server/server/turn-resources.js +87 -11
  150. package/dist-server/server/vps-computer.js +1 -1
  151. package/dist-server/server/workspace-backup-policy.js +22 -3
  152. package/dist-server/server/workspace-backup.js +25 -8
  153. package/dist-server/server/workspace.js +57 -22
  154. package/dist-server/shared/ask-question.js +7 -5
  155. package/dist-server/shared/bot-avatar.js +19 -0
  156. package/dist-server/shared/cloud-home.js +4 -0
  157. package/dist-server/shared/credential-request.js +3 -3
  158. package/dist-server/shared/package-format.js +30 -4
  159. package/dist-server/shared/team-backup.js +20 -3
  160. package/dist-server/shared/tool-surface.js +1 -1
  161. package/dist-server/src/lib/composer-attachments.js +9 -0
  162. package/dist-server/vps-container-mcp.js +136 -36
  163. package/dist-server/workspace-backup.worker.js +165 -35
  164. package/package.json +1 -1
  165. package/dist/assets/channel-DqY_gMLg.js +0 -1
  166. package/dist/assets/index-BIMdIbaw.css +0 -1
  167. package/dist/assets/index-BVPdbq2r.js +0 -583
  168. package/dist/assets/swimlanesDiagram-TC7HE7FX-Bk14M0_B.js +0 -8
@@ -0,0 +1,215 @@
1
+ // The bot-memory family, extracted from server/index.ts's inline routes
2
+ // into the route table (server/routes/README.md). The table runs after the
3
+ // auth gate; scope rules stay keyed by path in server/request-auth.ts.
4
+ //
5
+ // The files already belong to the person (plain markdown in the bot's
6
+ // workspace). server/memory-store.ts decides which paths can be reached
7
+ // and refuses a save whose expectedHash no longer matches the file;
8
+ // server/memory-journal.ts records every change made here so it can be
9
+ // read back and reverted. Reads never create the workspace — a bot that
10
+ // has not run yet simply has nothing to show. Admin scope by default
11
+ // (request-auth.ts), like the bot profile routes.
12
+ import { z } from "zod";
13
+ import { json } from "../harness/http.js";
14
+ import { MEMORY_INDEX, MemoryStoreError, memoryCapacity, memoryOverview, openMemoryLocation, readMemoryDoc, } from "../memory-store.js";
15
+ import { flushMemoryJournal, journalMemoryDelete, journalMemoryWrite, readMemoryJournal, revertMemoryChange, } from "../memory-journal.js";
16
+ import { isMemoryTopicName, readMemoryTopic } from "../workspace.js";
17
+ import { upkeepEnabled } from "../memory-upkeep.js";
18
+ import { listLearnedFacts, removeLearned } from "../profile-learned.js";
19
+ import { PASS } from "./table.js";
20
+ export function createBotMemoryRoutes(deps) {
21
+ /** A store refusal is a client error with a status of its own (400 path,
22
+ * 409 conflict, 413 too large); a 409 also carries what is on disk now so
23
+ * the editor can show the bot's version instead of guessing. Anything else
24
+ * is a real failure and goes to the handler's catch-all. */
25
+ function replyMemoryError(res, error) {
26
+ if (!(error instanceof MemoryStoreError))
27
+ throw error;
28
+ if (error.code === "conflict") {
29
+ return json(res, error.status, { error: error.message, code: error.code, currentHash: error.currentHash, current: error.current });
30
+ }
31
+ return json(res, error.status, { error: error.message, code: error.code });
32
+ }
33
+ /** The journal row as the panel shows it: the full prior text stays on
34
+ * the server (a revert needs it there, the list does not), and the thread
35
+ * id becomes the chat title people recognise. */
36
+ function journalEntryForClient(botId, entry) {
37
+ const { before: _before, ...visible } = entry;
38
+ const threadTitle = entry.threadId ? deps.taskByThread(botId, entry.threadId)?.title : undefined;
39
+ return threadTitle ? { ...visible, threadTitle } : visible;
40
+ }
41
+ return async ({ req, res, url, path, method, auth, json, readBody }) => {
42
+ /** scratch for route matches, shared by every `path.match` below */
43
+ let m = null;
44
+ m = path.match(/^\/api\/bots\/([\w-]+)\/memory$/);
45
+ if (m && method === "GET") {
46
+ if (!deps.bot(m[1]))
47
+ return json(res, 404, { error: "no such bot" });
48
+ try {
49
+ const overview = memoryOverview(m[1]);
50
+ // `text` and `truncated` ride along one release for clients of the
51
+ // 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
+ }
54
+ catch (error) {
55
+ return replyMemoryError(res, error);
56
+ }
57
+ }
58
+ if (m && method === "PUT") {
59
+ // The pre-panel whole-file write, kept one release: no hash check, so
60
+ // it can still overwrite a note the bot just wrote — journaled as the
61
+ // person's so at least the journal can undo it.
62
+ if (!deps.bot(m[1]))
63
+ return json(res, 404, { error: "no such bot" });
64
+ const parsed = z.object({ text: z.string() }).safeParse(await readBody(req));
65
+ if (!parsed.success)
66
+ return json(res, 400, { error: "text must be a string" });
67
+ try {
68
+ const { doc } = journalMemoryWrite(m[1], MEMORY_INDEX, parsed.data.text, { actor: "person", via: "api" });
69
+ return json(res, 200, { ok: true, hash: doc.hash, truncated: memoryCapacity(doc.text).truncated });
70
+ }
71
+ catch (error) {
72
+ // the old route answered 400 for an oversized body; keep that for
73
+ // its callers while the new route says 413
74
+ if (error instanceof MemoryStoreError && error.code === "too-large")
75
+ return json(res, 400, { error: error.message });
76
+ return replyMemoryError(res, error);
77
+ }
78
+ }
79
+ m = path.match(/^\/api\/bots\/([\w-]+)\/memory\/file$/);
80
+ if (m && method === "GET") {
81
+ if (!deps.bot(m[1]))
82
+ return json(res, 404, { error: "no such bot" });
83
+ try {
84
+ return json(res, 200, readMemoryDoc(m[1], url.searchParams.get("path") ?? MEMORY_INDEX));
85
+ }
86
+ catch (error) {
87
+ return replyMemoryError(res, error);
88
+ }
89
+ }
90
+ if (m && method === "PUT") {
91
+ if (!deps.bot(m[1]))
92
+ return json(res, 404, { error: "no such bot" });
93
+ const parsed = z
94
+ .object({ path: z.string().default(MEMORY_INDEX), text: z.string(), expectedHash: z.string().optional() })
95
+ .safeParse(await readBody(req));
96
+ if (!parsed.success)
97
+ return json(res, 400, { error: "text must be a string; path and expectedHash are optional strings" });
98
+ try {
99
+ const { doc, entry } = journalMemoryWrite(m[1], parsed.data.path, parsed.data.text, {
100
+ actor: "person",
101
+ via: "ui",
102
+ expectedHash: parsed.data.expectedHash,
103
+ });
104
+ return json(res, 200, { ok: true, ...doc, entry: entry ? journalEntryForClient(m[1], entry) : null, overview: memoryOverview(m[1]) });
105
+ }
106
+ catch (error) {
107
+ return replyMemoryError(res, error);
108
+ }
109
+ }
110
+ if (m && method === "DELETE") {
111
+ if (!deps.bot(m[1]))
112
+ return json(res, 404, { error: "no such bot" });
113
+ const file = url.searchParams.get("path") ?? "";
114
+ try {
115
+ const entry = journalMemoryDelete(m[1], file, { actor: "person", via: "ui" });
116
+ return json(res, 200, { ok: true, path: file, entry: entry ? journalEntryForClient(m[1], entry) : null, overview: memoryOverview(m[1]) });
117
+ }
118
+ catch (error) {
119
+ return replyMemoryError(res, error);
120
+ }
121
+ }
122
+ m = path.match(/^\/api\/bots\/([\w-]+)\/memory\/journal$/);
123
+ if (m && method === "GET") {
124
+ if (!deps.bot(m[1]))
125
+ return json(res, 404, { error: "no such bot" });
126
+ // the row a save queued a moment ago may not have reached disk yet
127
+ await flushMemoryJournal(m[1]);
128
+ const limit = Math.min(200, Math.max(1, Number(url.searchParams.get("limit") ?? 50) || 50));
129
+ const botId = m[1];
130
+ return json(res, 200, { entries: readMemoryJournal(botId, limit).map((entry) => journalEntryForClient(botId, entry)) });
131
+ }
132
+ m = path.match(/^\/api\/bots\/([\w-]+)\/memory\/journal\/([\w-]+)\/revert$/);
133
+ if (m && method === "POST") {
134
+ if (!deps.bot(m[1]))
135
+ return json(res, 404, { error: "no such bot" });
136
+ await flushMemoryJournal(m[1]);
137
+ const result = revertMemoryChange(m[1], m[2]);
138
+ if (!result.ok)
139
+ return json(res, result.status, { error: result.error });
140
+ return json(res, 200, { ok: true, ...result.doc, entry: result.entry ? journalEntryForClient(m[1], result.entry) : null, overview: memoryOverview(m[1]) });
141
+ }
142
+ // What upkeep learned about the person is shared through About me.
143
+ if (method === "GET" && path === "/api/profile/learned") {
144
+ return json(res, 200, { learned: listLearnedFacts() });
145
+ }
146
+ m = path.match(/^\/api\/profile\/learned\/([\w-]+)\/remove$/);
147
+ if (m && method === "POST") {
148
+ const removed = removeLearned(m[1], deps.aboutMe(), deps.saveAboutMe);
149
+ if (!removed)
150
+ return json(res, 404, { error: "That fact was already removed." });
151
+ return json(res, 200, { ok: true, aboutMe: deps.aboutMe(), learned: listLearnedFacts() });
152
+ }
153
+ m = path.match(/^\/api\/bots\/([\w-]+)\/memory\/upkeep$/);
154
+ if (m && method === "GET") {
155
+ const bot = deps.bot(m[1]);
156
+ if (!bot)
157
+ return json(res, 404, { error: "no such bot" });
158
+ return json(res, 200, { enabled: upkeepEnabled(bot), ...deps.upkeep.status(bot.id) });
159
+ }
160
+ m = path.match(/^\/api\/bots\/([\w-]+)\/memory\/tidy$/);
161
+ if (m && method === "POST") {
162
+ const bot = deps.bot(m[1]);
163
+ if (!bot)
164
+ return json(res, 404, { error: "no such bot" });
165
+ if (!upkeepEnabled(bot))
166
+ return json(res, 409, { error: "Switch on Memory upkeep for this bot first." });
167
+ const report = await deps.upkeep.tidy(bot.id);
168
+ await flushMemoryJournal(bot.id);
169
+ return json(res, 200, { report, overview: memoryOverview(bot.id) });
170
+ }
171
+ m = path.match(/^\/api\/bots\/([\w-]+)\/memory\/open$/);
172
+ if (m && method === "POST") {
173
+ if (!deps.bot(m[1]))
174
+ return json(res, 404, { error: "no such bot" });
175
+ const parsed = z.object({ target: z.enum(["obsidian", "folder"]) }).safeParse(await readBody(req));
176
+ if (!parsed.success)
177
+ return json(res, 400, { error: "target must be obsidian or folder" });
178
+ // The folder is on this machine's disk; opening it only makes sense
179
+ // from this machine. A paired phone or a remote browser gets the path
180
+ // to open by hand instead.
181
+ const workspacePath = memoryOverview(m[1]).workspacePath;
182
+ if (auth.kind !== "loopback") {
183
+ return json(res, 403, { error: `This only works on the computer running OpenMausBot. The memory folder there is ${workspacePath}`, workspacePath });
184
+ }
185
+ const opened = await openMemoryLocation(m[1], parsed.data.target);
186
+ if (!opened.ok)
187
+ return json(res, 500, { error: opened.error, workspacePath: opened.workspacePath });
188
+ return json(res, 200, opened);
189
+ }
190
+ m = path.match(/^\/api\/bots\/([\w-]+)\/memory\/topics\/([^/]+)$/);
191
+ if (m && method === "GET") {
192
+ if (!deps.bot(m[1]))
193
+ return json(res, 404, { error: "no such bot" });
194
+ // Decode before validating: a UI-sent name arrives percent-encoded
195
+ // ("my notes.md" → "my%20notes.md"), and an encoded traversal
196
+ // ("..%2F..") must be judged by what it decodes TO, not slip through
197
+ // as an opaque token. The name gate then rejects anything that is not
198
+ // a single plain-markdown path segment.
199
+ let name;
200
+ try {
201
+ name = decodeURIComponent(m[2]);
202
+ }
203
+ catch {
204
+ return json(res, 400, { error: "invalid topic name" });
205
+ }
206
+ if (!isMemoryTopicName(name))
207
+ return json(res, 400, { error: "invalid topic name" });
208
+ const text = readMemoryTopic(m[1], name);
209
+ if (text === null)
210
+ return json(res, 404, { error: "no such topic file" });
211
+ return json(res, 200, { name, text });
212
+ }
213
+ return PASS;
214
+ };
215
+ }
@@ -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
+ }
@@ -76,11 +76,11 @@ const targetBotSchema = z.object({
76
76
  }).strict();
77
77
  const routineProposalSchema = z.discriminatedUnion("action", [
78
78
  z.object({ action: z.literal("create"), routine: routineToolDefinitionSchema, forBot: targetBotSchema.optional() }).strict(),
79
- z.object({ action: z.literal("update"), routineId: z.string().max(128), changes: routineToolChangesSchema }).strict(),
80
- z.object({ action: z.literal("pause"), routineId: z.string().max(128) }).strict(),
81
- z.object({ action: z.literal("resume"), routineId: z.string().max(128) }).strict(),
82
- z.object({ action: z.literal("run_now"), routineId: z.string().max(128) }).strict(),
83
- z.object({ action: z.literal("delete"), routineId: z.string().max(128) }).strict(),
79
+ z.object({ action: z.literal("update"), routineId: z.string().max(128), changes: routineToolChangesSchema, forBot: targetBotSchema.optional() }).strict(),
80
+ z.object({ action: z.literal("pause"), routineId: z.string().max(128), forBot: targetBotSchema.optional() }).strict(),
81
+ z.object({ action: z.literal("resume"), routineId: z.string().max(128), forBot: targetBotSchema.optional() }).strict(),
82
+ z.object({ action: z.literal("run_now"), routineId: z.string().max(128), forBot: targetBotSchema.optional() }).strict(),
83
+ z.object({ action: z.literal("delete"), routineId: z.string().max(128), forBot: targetBotSchema.optional() }).strict(),
84
84
  ]);
85
85
  const storedWeekdaysSchema = z.array(z.number().int().min(0).max(6)).min(1).max(7).refine((weekdays) => new Set(weekdays).size === weekdays.length, "Stored routine weekdays must be unique");
86
86
  const storedIntervalWindowSchema = z.object({
@@ -190,6 +190,7 @@ const storedChangesSchema = storedDefinitionSchema
190
190
  const storedManageBase = {
191
191
  routineId: z.string().regex(ROUTINE_ID),
192
192
  expectedUpdatedAt: z.number().int().nonnegative(),
193
+ forBot: targetBotSchema.optional(),
193
194
  };
194
195
  const storedOperationSchema = z.discriminatedUnion("action", [
195
196
  z.object({ action: z.literal("create"), routine: storedDefinitionSchema, forBot: targetBotSchema.optional() }).strict(),
@@ -441,7 +442,11 @@ function normalizedOperation(manager, botId, validated, now) {
441
442
  return operation;
442
443
  }
443
444
  const id = routineId(validated.routineId);
444
- const current = ownedRoutine(manager, id, botId);
445
+ // A targeted action manages the named bot's routine: ownership and every
446
+ // run's permissions stay with that bot, exactly as a routine created
447
+ // through the for_bot_id path keeps them.
448
+ const ownerBotId = validated.forBot?.botId ?? botId;
449
+ const current = ownedRoutine(manager, id, ownerBotId);
445
450
  if (!current)
446
451
  throw new RoutineRequestError("That routine does not exist", 404);
447
452
  if (validated.action === "update") {
@@ -450,12 +455,18 @@ function normalizedOperation(manager, botId, validated, now) {
450
455
  routineId: id,
451
456
  expectedUpdatedAt: current.updatedAt,
452
457
  changes: normalizeChanges(validated.changes, now),
458
+ ...(validated.forBot ? { forBot: validated.forBot } : {}),
453
459
  };
454
460
  }
455
461
  if (validated.action === "resume" && nextOccurrence(current.schedule, now) === null) {
456
462
  throw new RoutineRequestError(noFutureResumeMessage(current.schedule), 409);
457
463
  }
458
- return { action: validated.action, routineId: id, expectedUpdatedAt: current.updatedAt };
464
+ return {
465
+ action: validated.action,
466
+ routineId: id,
467
+ expectedUpdatedAt: current.updatedAt,
468
+ ...(validated.forBot ? { forBot: validated.forBot } : {}),
469
+ };
459
470
  }
460
471
  function asSchedule(schedule, now) {
461
472
  if (schedule.type === "cron")
@@ -651,7 +662,7 @@ function cardCopy(operation, manager, timeZone, now) {
651
662
  const actionCopy = ACTION_COPY[operation.action];
652
663
  const actionLabel = actionCopy.title;
653
664
  const name = redactSecretsInText(definition?.name ?? "routine");
654
- const forBot = operation.action === "create" ? operation.forBot : undefined;
665
+ const forBot = operation.forBot;
655
666
  const forSuffix = forBot ? ` for @${redactSecretsInText(forBot.name)}` : "";
656
667
  const title = `${actionLabel} “${name}”${forSuffix}?`;
657
668
  if (!definition) {
@@ -666,7 +677,7 @@ function cardCopy(operation, manager, timeZone, now) {
666
677
  const nextRunAt = nextForOperation(operation, manager, now);
667
678
  const scheduleTimeZone = definition.schedule.type === "cron" ? definition.schedule.timeZone : timeZone;
668
679
  const when = operation.action === "run_now" ? "Now" : scheduleText(definition.schedule, timeZone);
669
- const destination = definition.runOn === "cloud" ? "Box-hosted agent" : "Bot’s current model and configured computer";
680
+ const destination = definition.runOn === "cloud" ? "Boat-hosted agent" : "Bot’s current model and configured computer";
670
681
  const current = operation.action === "create"
671
682
  ? null
672
683
  : manager.listRoutines().find((routine) => routine.id === operation.routineId) ?? null;
@@ -838,7 +849,7 @@ function revalidateOperation(operation, manager, botId, now) {
838
849
  }
839
850
  const current = operation.action === "create"
840
851
  ? null
841
- : verifyManageSnapshot(operation, manager, botId);
852
+ : verifyManageSnapshot(operation, manager, operation.forBot?.botId ?? botId);
842
853
  const schedule = operation.action === "create"
843
854
  ? operation.routine.schedule
844
855
  : operation.action === "update"
@@ -923,7 +934,7 @@ export class RoutineRequestService {
923
934
  throw new RoutineRequestError(schemaIssue(parsedProposal.error, "Invalid routine proposal"));
924
935
  }
925
936
  const operation = normalizedOperation(this.routines, botId, parsedProposal.data, at);
926
- if (operation.action === "create" && operation.forBot && this.validateTarget) {
937
+ if (operation.forBot && this.validateTarget) {
927
938
  const refusal = this.validateTarget(botId, operation.forBot);
928
939
  if (refusal)
929
940
  throw new RoutineRequestError(refusal, 403);
@@ -1130,7 +1141,7 @@ export class RoutineRequestService {
1130
1141
  return { claimed: true, state: "denied" };
1131
1142
  }
1132
1143
  revalidateOperation(payload.operation, this.routines, payload.botId, this.now());
1133
- if (payload.operation.action === "create" && payload.operation.forBot && this.validateTarget) {
1144
+ if (payload.operation.forBot && this.validateTarget) {
1134
1145
  const refusal = this.validateTarget(payload.botId, payload.operation.forBot);
1135
1146
  if (refusal)
1136
1147
  throw new RoutineRequestError(refusal, 404, { terminal: true });
@@ -1229,7 +1240,7 @@ export class RoutineRequestService {
1229
1240
  fingerprint,
1230
1241
  }).id;
1231
1242
  case "update": {
1232
- const current = verifyManageSnapshot(operation, this.routines, payload.botId);
1243
+ const current = verifyManageSnapshot(operation, this.routines, operation.forBot?.botId ?? payload.botId);
1233
1244
  const updated = this.routines.update(operation.routineId, updateFromChanges(operation.changes, confirmationAt, current.schedule), {
1234
1245
  requestId: payload.requestId,
1235
1246
  messageId,
@@ -1245,7 +1256,7 @@ export class RoutineRequestService {
1245
1256
  }
1246
1257
  case "pause":
1247
1258
  case "resume": {
1248
- verifyManageSnapshot(operation, this.routines, payload.botId);
1259
+ verifyManageSnapshot(operation, this.routines, operation.forBot?.botId ?? payload.botId);
1249
1260
  const updated = this.routines.update(operation.routineId, { enabled: operation.action === "resume" }, {
1250
1261
  requestId: payload.requestId,
1251
1262
  messageId,
@@ -1260,7 +1271,7 @@ export class RoutineRequestService {
1260
1271
  return updated.id;
1261
1272
  }
1262
1273
  case "run_now": {
1263
- verifyManageSnapshot(operation, this.routines, payload.botId);
1274
+ verifyManageSnapshot(operation, this.routines, operation.forBot?.botId ?? payload.botId);
1264
1275
  const run = this.routines.runNow(operation.routineId, {
1265
1276
  requestId: payload.requestId,
1266
1277
  messageId,
@@ -1275,7 +1286,7 @@ export class RoutineRequestService {
1275
1286
  return run.id;
1276
1287
  }
1277
1288
  case "delete":
1278
- verifyManageSnapshot(operation, this.routines, payload.botId);
1289
+ verifyManageSnapshot(operation, this.routines, operation.forBot?.botId ?? payload.botId);
1279
1290
  if (!this.routines.remove(operation.routineId, {
1280
1291
  requestId: payload.requestId,
1281
1292
  messageId,
@@ -1284,12 +1284,16 @@ export class RoutineManager {
1284
1284
  continue;
1285
1285
  }
1286
1286
  // A webhook is an incoming message, so make its task the bot's live
1287
- // chat immediately. Scheduled work remains detached and unobtrusive.
1288
- const task = run.target === "room-goal"
1289
- ? run.groupId
1290
- ? this.options.createGoalTask?.(run.groupId, run.routineName) ?? null
1291
- : null
1292
- : this.options.createTask(run.botId, run.routineName, run.triggerSource === "webhook");
1287
+ // chat immediately. Scheduled work stays in its own task unless the
1288
+ // workspace has asked for runs to join the conversation they report to.
1289
+ const joined = run.target === "room-goal" ? null : this.options.joinConversation?.(run) || null;
1290
+ const task = joined
1291
+ ? { threadId: joined }
1292
+ : run.target === "room-goal"
1293
+ ? run.groupId
1294
+ ? this.options.createGoalTask?.(run.groupId, run.routineName) ?? null
1295
+ : null
1296
+ : this.options.createTask(run.botId, run.routineName, run.triggerSource === "webhook");
1293
1297
  if (!task) {
1294
1298
  this.failRun(run, run.target === "room-goal"
1295
1299
  ? "Could not create a room task for this goal"
@@ -15,41 +15,120 @@ export const sharedComputerOperation = z.object({
15
15
  arguments: z.record(z.string(), z.unknown()).optional(),
16
16
  }).strict();
17
17
  const failure = (message, status = 409) => Object.assign(new Error(message), { status });
18
+ /** Bounds memory, not anyone's share: a desktop session holds one entry. */
19
+ const TOTAL = 100;
20
+ /** How long a lent computer that stopped polling (asleep, app closed) stays
21
+ * known, so a status reader can tell "lent but offline" from "not lent". */
22
+ const RETAIN_MS = 14 * 24 * 60 * 60_000;
23
+ const OFFLINE = "This shared computer is offline. Keep its desktop app open; do not substitute files on the server.";
24
+ /** The scopes the desktop registered, checked by the server as well. The
25
+ * desktop's own grant is the authority and checks every operation again;
26
+ * this only means the server never even queues work outside what the desktop
27
+ * said it lends. */
28
+ export function sharedComputerScopeRefusal(registration, operation) {
29
+ switch (operation.action) {
30
+ case "run_command":
31
+ return registration.terminal ? null : "Terminal access is not lent to this server.";
32
+ case "computer_tools":
33
+ case "computer_call":
34
+ return registration.computer ? null : "Apps and screen are not lent to this server.";
35
+ case "list_files":
36
+ case "read_file":
37
+ case "write_file": {
38
+ const lent = registration.folders.find(entry => entry.id === operation.folder_id);
39
+ if (!lent)
40
+ return "This folder has not been shared with this server.";
41
+ return operation.action === "write_file" && !lent.write ? "This folder is read-only." : null;
42
+ }
43
+ }
44
+ }
45
+ /** The person a bot turn acts for, from the harness's own record of which
46
+ * message started it: that request must name this exact turn generation (a
47
+ * continuation of the same request adds its generation), must not have been
48
+ * stopped, and its message must come from a person. Anything else is nobody. */
49
+ export function provenRequestPerson(request, generation, personOf) {
50
+ if (!request?.messageId || request.stopped || !request.generations.has(generation))
51
+ return null;
52
+ return personOf(request.messageId) ?? null;
53
+ }
18
54
  /** In-memory rendezvous only. Authority over local resources stays on the
19
- * desktop. Restart/disconnect NEVER replays an operation with an unknown outcome. */
55
+ * desktop. Restart/disconnect NEVER replays an operation with an unknown outcome.
56
+ *
57
+ * Partitioned by owner: an entry is keyed by the registering session and the
58
+ * desktop's id, so another session can never take over an id, and a bot turn
59
+ * sees only the computers of the person it acts for. A turn that acts for
60
+ * nobody provable (principal null) sees none. */
20
61
  export class SharedComputers {
21
62
  computers = new Map();
22
63
  sessionLive;
23
64
  constructor(sessionLive) { this.sessionLive = sessionLive; }
65
+ static key(session, id) { return `${session}\n${id}`; }
24
66
  register(registration, owner, secret) {
25
67
  if (!/^[a-f0-9]{64}$/.test(secret))
26
68
  throw failure("Invalid computer credential", 400);
27
- const old = this.computers.get(registration.id);
69
+ const key = SharedComputers.key(owner.session, registration.id);
70
+ const old = this.computers.get(key);
28
71
  if (old) {
29
- this.authorize(registration.id, owner, secret);
72
+ this.authorize(registration.id, owner.session, secret);
30
73
  old.registration = registration;
31
74
  old.seen = Date.now();
75
+ return;
76
+ }
77
+ this.reap();
78
+ // A paired desktop runs one grant per server, so a new id from the same
79
+ // session is a new save: its earlier entry is stale and goes. No other
80
+ // session can reach, replace or squat this key.
81
+ for (const [stale, entry] of this.computers) {
82
+ if (entry.owner.session !== owner.session)
83
+ continue;
84
+ this.computers.delete(stale);
85
+ entry.wake?.();
86
+ for (const job of entry.jobs.values())
87
+ this.finish(entry, job, failure("This computer's access changed. An in-flight action may have completed; inspect before retrying."));
32
88
  }
33
- else {
34
- // Reap dead devices before applying the bounded registration limit.
35
- for (const [id, entry] of this.computers)
36
- if (!this.online(entry) && entry.jobs.size === 0)
37
- this.computers.delete(id);
38
- if (this.computers.size >= 20)
39
- throw failure("Too many shared computers");
40
- this.computers.set(registration.id, { registration, owner, secret, seen: Date.now(), jobs: new Map() });
89
+ if (this.computers.size >= TOTAL)
90
+ throw failure("Too many shared computers");
91
+ this.computers.set(key, { registration, owner: { ...owner }, secret, seen: Date.now(), jobs: new Map() });
92
+ }
93
+ /** Forget entries whose session ended or that have been silent too long. */
94
+ reap() {
95
+ for (const [key, entry] of this.computers) {
96
+ if (entry.jobs.size === 0 && (!this.sessionLive(entry.owner.session) || Date.now() - entry.seen > RETAIN_MS))
97
+ this.computers.delete(key);
41
98
  }
42
99
  }
43
- online(entry) { return this.sessionLive(entry.owner) && Date.now() - entry.seen < 40_000; }
44
- authorize(id, owner, secret) {
45
- const entry = this.computers.get(id);
46
- if (!entry || entry.owner !== owner || !this.sessionLive(owner) || !/^[a-f0-9]{64}$/.test(secret) || !timingSafeEqual(Buffer.from(secret), Buffer.from(entry.secret)))
100
+ online(entry) { return this.sessionLive(entry.owner.session) && Date.now() - entry.seen < 40_000; }
101
+ authorize(id, session, secret) {
102
+ const entry = this.computers.get(SharedComputers.key(session, id));
103
+ if (!entry || !this.sessionLive(session) || !/^[a-f0-9]{64}$/.test(secret) || !timingSafeEqual(Buffer.from(secret), Buffer.from(entry.secret)))
47
104
  throw failure("Shared computer is disconnected or not authorized", 403);
48
105
  return entry;
49
106
  }
50
- list() { return [...this.computers.values()].filter(entry => this.online(entry)).map(entry => entry.registration); }
51
- async poll(id, owner, secret) {
52
- const entry = this.authorize(id, owner, secret);
107
+ /** The online computers one person lent. */
108
+ list(principal) {
109
+ if (!principal)
110
+ return [];
111
+ return [...this.computers.values()].filter(entry => entry.owner.person === principal && this.online(entry)).map(entry => entry.registration);
112
+ }
113
+ /** What one person has lent, online or not: the documented status a
114
+ * reader (the Cloud UI, the next step's placement) uses to tell "your Mac
115
+ * is lent and awake" from "lent but asleep" and "not lent". No secret, no
116
+ * session. Resets when this server restarts; a lending desktop registers
117
+ * again within seconds of being online. */
118
+ status(principal) {
119
+ if (!principal)
120
+ return [];
121
+ this.reap();
122
+ return [...this.computers.values()].filter(entry => entry.owner.person === principal).map(entry => ({
123
+ id: entry.registration.id, name: entry.registration.name, online: this.online(entry), busy: entry.jobs.size > 0, lastSeenAt: entry.seen,
124
+ scopes: {
125
+ folders: entry.registration.folders.map(({ id, name, write }) => ({ id, name, write })),
126
+ terminal: entry.registration.terminal, screen: entry.registration.computer,
127
+ },
128
+ }));
129
+ }
130
+ async poll(id, session, secret) {
131
+ const entry = this.authorize(id, session, secret);
53
132
  if (entry.wake)
54
133
  throw failure("A computer poll is already running");
55
134
  entry.seen = Date.now();
@@ -73,34 +152,41 @@ export class SharedComputers {
73
152
  const timer = setTimeout(() => { entry.wake = undefined; resolve(); }, 20_000);
74
153
  entry.wake = () => { clearTimeout(timer); entry.wake = undefined; resolve(); };
75
154
  });
76
- this.authorize(id, owner, secret);
155
+ this.authorize(id, session, secret);
77
156
  entry.seen = Date.now();
78
157
  return next();
79
158
  }
80
- liveJob(id, owner, secret, jobId) {
81
- const entry = this.authorize(id, owner, secret);
159
+ liveJob(id, session, secret, jobId) {
160
+ const entry = this.authorize(id, session, secret);
82
161
  entry.seen = Date.now();
83
162
  const job = entry.jobs.get(jobId);
84
163
  return job?.sent === true && job.active();
85
164
  }
86
- complete(id, owner, secret, jobId, result) {
87
- const entry = this.authorize(id, owner, secret);
165
+ complete(id, session, secret, jobId, result) {
166
+ const entry = this.authorize(id, session, secret);
88
167
  const job = entry.jobs.get(jobId);
89
168
  if (!job || !job.sent)
90
169
  throw failure("This computer operation expired; it will not be replayed");
91
170
  this.finish(entry, job, job.active() ? null : failure("The requesting turn ended"), result);
92
171
  }
93
- disconnect(id, owner, secret) {
94
- const entry = this.authorize(id, owner, secret);
95
- this.computers.delete(id);
172
+ disconnect(id, session, secret) {
173
+ const entry = this.authorize(id, session, secret);
174
+ this.computers.delete(SharedComputers.key(session, id));
96
175
  entry.wake?.();
97
176
  for (const job of entry.jobs.values())
98
177
  this.finish(entry, job, failure("Computer disconnected. An in-flight action may have completed; inspect before retrying."));
99
178
  }
100
- request(operation, active) {
101
- const entry = this.computers.get(operation.computer_id);
102
- if (!entry || !this.online(entry))
103
- return Promise.reject(failure("This shared computer is offline. Keep its desktop app open; do not substitute files on the server."));
179
+ /** One bot operation for the person the turn acts for. Another person's
180
+ * computer answers exactly like one that does not exist. */
181
+ request(operation, principal, active) {
182
+ if (!principal)
183
+ return Promise.reject(failure("This conversation cannot use a shared computer: only a conversation started by the person who shared it can.", 403));
184
+ const entry = [...this.computers.values()].find(candidate => candidate.registration.id === operation.computer_id && candidate.owner.person === principal && this.online(candidate));
185
+ if (!entry)
186
+ return Promise.reject(failure(OFFLINE));
187
+ const refusal = sharedComputerScopeRefusal(entry.registration, operation);
188
+ if (refusal)
189
+ return Promise.reject(failure(refusal, 403));
104
190
  if (entry.jobs.size >= 1)
105
191
  return Promise.reject(failure("This shared computer is busy. Wait for its current action to finish."));
106
192
  if (!active())