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
@@ -0,0 +1,238 @@
1
+ import { deciderCredential } from "../included-services.js";
2
+ import { jevBackend } from "./jev.js";
3
+ import { appendDeciderLog, stateHash } from "./log.js";
4
+ import { KEY_CHECK_QUESTION, KEY_CHECK_STATE, relayAccepts, relaySeam } from "./relay.js";
5
+ export { DECIDER_JOBS } from "./types.js";
6
+ /** Used when a caller passes no timeout. Room routing passes its own. */
7
+ export const DEFAULT_DECIDER_TIMEOUT_MS = 1_500;
8
+ /** The Settings key check is one tiny call a person is waiting on. */
9
+ export const KEY_CHECK_TIMEOUT_MS = 10_000;
10
+ const MIN_TIMEOUT_MS = 50;
11
+ const MAX_TIMEOUT_MS = 30_000;
12
+ /** A request the switches never let through. */
13
+ const offBackend = {
14
+ id: "off",
15
+ decide: async () => ({ ok: false, reason: "disabled" }),
16
+ };
17
+ const BACKENDS = { jev: jevBackend };
18
+ export function deciderProvider(cfg) {
19
+ const provider = cfg.decider?.provider ?? "jev";
20
+ return provider in BACKENDS ? provider : "off";
21
+ }
22
+ /** The key a call uses and the one base URL it goes to: the person's own
23
+ * (a draft being tested, else the config value or the desktop's encrypted
24
+ * store handed over as env) with `decider.baseUrl` or Jev's own, else Cloud
25
+ * Pro's included token with the Admin's relay, but only where the relay
26
+ * accepts that seam: any other job has only an own key. Resolved on every
27
+ * call. */
28
+ function deciderAccount(cfg, seam, draft) {
29
+ const account = deciderCredential(draft?.trim() || cfg.decider?.key, cfg.decider?.baseUrl);
30
+ return account?.included && seam && !relaySeam(seam) ? null : account;
31
+ }
32
+ /** A key is on file, or Cloud Pro includes decisions here. */
33
+ export function deciderConfigured(cfg) {
34
+ return Boolean(deciderAccount(cfg));
35
+ }
36
+ /** Cloud Pro includes decisions on this machine, whatever key is saved:
37
+ * what clearing an own key falls back to. */
38
+ export function deciderIncludedHere() {
39
+ return deciderCredential(undefined, undefined)?.included === true;
40
+ }
41
+ /** The master switch as it takes effect: off while there is no key. With an
42
+ * own key it is on once switched on (saving the key does that); with Cloud
43
+ * Pro's included decisions it is on until someone switches it off. */
44
+ export function deciderEnabled(cfg) {
45
+ const account = deciderAccount(cfg);
46
+ if (!account || deciderProvider(cfg) === "off")
47
+ return false;
48
+ return account.included ? cfg.decider?.enabled !== false : cfg.decider?.enabled === true;
49
+ }
50
+ /** One job's own switch. Absent means on: turning the decider on turns on
51
+ * what it decides unless a job was switched off by hand. */
52
+ export function deciderJobOn(cfg, job) {
53
+ return cfg.decider?.jobs?.[job] !== false;
54
+ }
55
+ /** Whether a job would be asked right now. Callers check this before they
56
+ * spend any effort building a request. */
57
+ export function deciderReady(cfg, job) {
58
+ return deciderEnabled(cfg) && deciderJobOn(cfg, job) && Boolean(deciderAccount(cfg, job));
59
+ }
60
+ /** What Settings needs. Never the key, and not the base URL either: that is
61
+ * an operator setting with no UI. `included`: the decisions are Cloud Pro's,
62
+ * not a saved key. */
63
+ export function describeDecider(cfg) {
64
+ const account = deciderAccount(cfg);
65
+ return {
66
+ provider: "jev",
67
+ configured: Boolean(account),
68
+ ...(account?.included ? { included: true } : {}),
69
+ enabled: deciderEnabled(cfg),
70
+ jobs: { roomRouting: deciderJobOn(cfg, "roomRouting") },
71
+ };
72
+ }
73
+ /** The config write a Settings save becomes.
74
+ *
75
+ * Owner rule: the switch is off while no key is saved, and saving a key is
76
+ * the "turn it on" — it switches the decider and its room job on, with no
77
+ * extra step. Clearing the key switches it off, unless Cloud Pro includes
78
+ * decisions here (`included`): then it falls back to them as it stands.
79
+ * Explicit values in the same patch win. Jobs merge per job, because
80
+ * saveConfig replaces a section's nested objects whole. Returns an error for
81
+ * a switch-on with no key and nothing included. */
82
+ export function deciderSavePatch(patch, current, included = false) {
83
+ const next = { ...patch };
84
+ const savingKey = typeof patch.key === "string" && Boolean(patch.key.trim());
85
+ const clearingKey = typeof patch.key === "string" && !patch.key.trim();
86
+ if (typeof patch.key === "string")
87
+ next.key = patch.key.trim();
88
+ if (savingKey) {
89
+ next.enabled = patch.enabled ?? true;
90
+ next.jobs = { ...current?.jobs, roomRouting: true, ...patch.jobs };
91
+ }
92
+ else if (clearingKey) {
93
+ if (!included)
94
+ next.enabled = false;
95
+ }
96
+ else if (patch.jobs) {
97
+ next.jobs = { ...current?.jobs, ...patch.jobs };
98
+ }
99
+ if (next.enabled === true && !savingKey && !current?.key?.trim() && !included) {
100
+ return { ok: false, error: "Save a Jev API key before turning on fast decisions." };
101
+ }
102
+ return { ok: true, patch: next };
103
+ }
104
+ export function createDecider(deps) {
105
+ const fetchImpl = deps.fetch ?? fetch;
106
+ const now = deps.now ?? (() => performance.now());
107
+ function gate(cfg, seam) {
108
+ if (deciderProvider(cfg) === "off")
109
+ return "disabled";
110
+ if (seam === "keyCheck")
111
+ return null;
112
+ if (!deciderConfigured(cfg))
113
+ return cfg.decider?.enabled === true ? "no_key" : "disabled";
114
+ if (!deciderEnabled(cfg))
115
+ return "disabled";
116
+ if (!deciderJobOn(cfg, seam))
117
+ return "job_off";
118
+ // Only the included token, and the relay does not take this job.
119
+ if (!deciderAccount(cfg, seam))
120
+ return "no_key";
121
+ return null;
122
+ }
123
+ async function call(seam, backend, request, options) {
124
+ const provider = backend.id === "off" ? undefined : backend.id;
125
+ if (options.signal?.aborted)
126
+ return { ok: false, ...(provider ? { provider } : {}), reason: "cancelled" };
127
+ const timeoutMs = Math.min(MAX_TIMEOUT_MS, Math.max(MIN_TIMEOUT_MS, options.timeoutMs ?? DEFAULT_DECIDER_TIMEOUT_MS));
128
+ const controller = new AbortController();
129
+ const onAbort = () => controller.abort();
130
+ options.signal?.addEventListener("abort", onAbort, { once: true });
131
+ let timer;
132
+ // The race, not only the abort, bounds the call: a body that stalls
133
+ // after its headers must not hold a room turn past the budget.
134
+ const timedOut = new Promise((resolve) => {
135
+ timer = setTimeout(() => {
136
+ controller.abort();
137
+ resolve({ ok: false, reason: "timeout" });
138
+ }, timeoutMs);
139
+ });
140
+ const started = now();
141
+ let outcome;
142
+ try {
143
+ const decided = Promise.resolve()
144
+ .then(() => backend.decide({ ...request, signal: controller.signal, fetch: fetchImpl }))
145
+ .catch(() => ({ ok: false, reason: "malformed" }));
146
+ outcome = await Promise.race([decided, timedOut]);
147
+ }
148
+ finally {
149
+ clearTimeout(timer);
150
+ options.signal?.removeEventListener("abort", onAbort);
151
+ }
152
+ const latencyMs = Math.max(0, Math.round(now() - started));
153
+ if (!outcome.ok && outcome.reason !== "timeout" && options.signal?.aborted)
154
+ outcome = { ok: false, reason: "cancelled" };
155
+ if (provider && deps.dataDir) {
156
+ const first = outcome.ok ? Object.values(outcome.answers).find((answer) => answer.type === "choice") : undefined;
157
+ appendDeciderLog(deps.dataDir, {
158
+ at: new Date().toISOString(),
159
+ seam,
160
+ provider,
161
+ ok: outcome.ok,
162
+ ...(!outcome.ok ? { reason: outcome.reason, ...(outcome.status ? { status: outcome.status } : {}) } : {}),
163
+ ...(first ? { choice: first.choice } : {}),
164
+ pTop: first?.pTop ?? null,
165
+ margin: first?.margin ?? null,
166
+ latencyMs,
167
+ ...(outcome.ok && outcome.inputTokens !== undefined ? { inputTokens: outcome.inputTokens } : {}),
168
+ stateHash: stateHash(request.state),
169
+ });
170
+ }
171
+ if (!outcome.ok)
172
+ return { ok: false, ...(provider ? { provider } : {}), reason: outcome.reason, ...(outcome.status ? { status: outcome.status } : {}), latencyMs };
173
+ return {
174
+ ok: true,
175
+ provider: provider,
176
+ answers: outcome.answers,
177
+ latencyMs,
178
+ ...(outcome.inputTokens !== undefined ? { inputTokens: outcome.inputTokens } : {}),
179
+ ...(outcome.model ? { model: outcome.model } : {}),
180
+ };
181
+ }
182
+ async function ask(seam, state, questions, options = {}) {
183
+ try {
184
+ const cfg = deps.config();
185
+ const refused = gate(cfg, seam);
186
+ if (refused)
187
+ return { ok: false, reason: refused };
188
+ const provider = deciderProvider(cfg);
189
+ const backend = provider === "off" ? offBackend : BACKENDS[provider];
190
+ const account = deciderAccount(cfg, seam);
191
+ // Through the included token, only the exact requests the relay takes.
192
+ if (account.included && !relayAccepts(seam, state, questions))
193
+ return { ok: false, reason: "misconfigured" };
194
+ return await call(seam, backend, { key: account.token, baseUrl: account.api, state, questions }, options);
195
+ }
196
+ catch {
197
+ return { ok: false, reason: "malformed" };
198
+ }
199
+ }
200
+ const single = (result) => result.ok ? { ...result, answers: result.answers.answer } : result;
201
+ return {
202
+ ask,
203
+ async choose(seam, state, question, options) {
204
+ const asked = { answer: { type: "choice", instructions: question.instructions, options: question.options } };
205
+ return single(await ask(seam, state, asked, options));
206
+ },
207
+ async score(seam, state, question, options) {
208
+ return single(await ask(seam, state, { answer: { type: "score", instructions: question.instructions, levels: question.levels } }, options));
209
+ },
210
+ async yesNo(seam, state, instructions, options) {
211
+ return single(await ask(seam, state, { answer: { type: "yesno", instructions } }, options));
212
+ },
213
+ async testKey(input = {}, options = {}) {
214
+ try {
215
+ const cfg = deps.config();
216
+ // A draft or saved own key goes to Jev (or decider.baseUrl); with
217
+ // neither, Cloud Pro's included token goes to its relay.
218
+ const account = deciderAccount(cfg, "keyCheck", input.key);
219
+ if (!account)
220
+ return { ok: false, reason: "no_key" };
221
+ const provider = deciderProvider(cfg);
222
+ const backend = provider === "off" ? offBackend : BACKENDS[provider];
223
+ const questions = { answer: { type: "yesno", instructions: KEY_CHECK_QUESTION } };
224
+ if (account.included && !relayAccepts("keyCheck", KEY_CHECK_STATE, questions))
225
+ return { ok: false, reason: "misconfigured" };
226
+ return single(await call("keyCheck", backend, {
227
+ key: account.token,
228
+ baseUrl: account.api,
229
+ state: KEY_CHECK_STATE,
230
+ questions,
231
+ }, { timeoutMs: KEY_CHECK_TIMEOUT_MS, ...options }));
232
+ }
233
+ catch {
234
+ return { ok: false, reason: "malformed" };
235
+ }
236
+ },
237
+ };
238
+ }
@@ -0,0 +1,183 @@
1
+ export const JEV_DEFAULT_BASE_URL = "https://api.typesafe.ai";
2
+ export const JEV_MODEL = "jev-latest";
3
+ export const JEV_MAX_OPTIONS = 255;
4
+ export const JEV_MIN_LEVELS = 2;
5
+ export const JEV_MAX_LEVELS = 10;
6
+ function isLoopback(hostname) {
7
+ const host = hostname.replace(/^\[|\]$/g, "");
8
+ return host === "localhost" || host === "127.0.0.1" || host === "::1" || host.startsWith("127.");
9
+ }
10
+ /** The endpoint for a base URL, or null when the key must not be sent there:
11
+ * https anywhere, plain http only to this machine (a local Jev-compatible
12
+ * server or a test double). */
13
+ export function jevEndpoint(baseUrl) {
14
+ const root = (baseUrl?.trim() || JEV_DEFAULT_BASE_URL).replace(/\/+$/, "");
15
+ let url;
16
+ try {
17
+ url = new URL(`${root}/v1/systemone`);
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ if (url.username || url.password)
23
+ return null;
24
+ if (url.protocol === "https:")
25
+ return url;
26
+ return url.protocol === "http:" && isLoopback(url.hostname) ? url : null;
27
+ }
28
+ /** Questions within the API's limits: 2–255 options, 2–10 levels. */
29
+ export function questionsFit(questions) {
30
+ const entries = Object.entries(questions);
31
+ if (!entries.length)
32
+ return false;
33
+ return entries.every(([, question]) => {
34
+ if (question.type === "choice") {
35
+ const count = Object.keys(question.options).length;
36
+ return count >= 2 && count <= JEV_MAX_OPTIONS;
37
+ }
38
+ if (question.type === "score")
39
+ return question.levels.length >= JEV_MIN_LEVELS && question.levels.length <= JEV_MAX_LEVELS;
40
+ return true;
41
+ });
42
+ }
43
+ export function jevRequestBody(state, questions) {
44
+ const wire = {};
45
+ for (const [id, question] of Object.entries(questions)) {
46
+ if (question.type === "choice") {
47
+ wire[id] = { type: "choice", instructions: question.instructions, criteria: { ...question.options } };
48
+ }
49
+ else if (question.type === "score") {
50
+ wire[id] = { type: "score", instructions: question.instructions, criteria: [...question.levels] };
51
+ }
52
+ else {
53
+ const criteria = question.criteria?.yes || question.criteria?.no
54
+ ? { ...(question.criteria.yes ? { true: question.criteria.yes } : {}), ...(question.criteria.no ? { false: question.criteria.no } : {}) }
55
+ : undefined;
56
+ wire[id] = { type: "noul", instructions: question.instructions, ...(criteria ? { criteria } : {}) };
57
+ }
58
+ }
59
+ return { state, model: JEV_MODEL, questions: wire };
60
+ }
61
+ const isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
62
+ const isProbability = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
63
+ /** Jev rounds to two places, so a map can sum to 0.98 or 1.02. */
64
+ const SUM_TOLERANCE = 0.1;
65
+ const EPSILON = 1e-9;
66
+ function parseChoice(raw, question) {
67
+ const offered = new Set(Object.keys(question.options));
68
+ const choice = raw.choice;
69
+ if (typeof choice !== "string" || !offered.has(choice))
70
+ return null;
71
+ if (!isRecord(raw.probabilities))
72
+ return null;
73
+ const probabilities = {};
74
+ let sum = 0;
75
+ for (const [key, value] of Object.entries(raw.probabilities)) {
76
+ if (!offered.has(key) || !isProbability(value))
77
+ return null;
78
+ probabilities[key] = value;
79
+ sum += value;
80
+ }
81
+ if (!(Math.abs(sum - 1) <= SUM_TOLERANCE))
82
+ return null;
83
+ const pTop = probabilities[choice];
84
+ if (pTop === undefined)
85
+ return null;
86
+ const others = Object.entries(probabilities).filter(([key]) => key !== choice).map(([, value]) => value);
87
+ const runnerUp = others.length ? Math.max(...others) : 0;
88
+ // Ties break to the first key on the vendor side; a choice that is not
89
+ // the most likely option means the answer is not what it claims to be.
90
+ if (runnerUp > pTop + EPSILON)
91
+ return null;
92
+ return { type: "choice", choice, pTop, margin: pTop - runnerUp, probabilities };
93
+ }
94
+ function parseScore(raw, question) {
95
+ const levels = question.levels.length;
96
+ const score = raw.score;
97
+ if (typeof score !== "number" || !Number.isFinite(score) || score < -EPSILON || score > levels - 1 + EPSILON)
98
+ return null;
99
+ if (!isRecord(raw.probabilities))
100
+ return null;
101
+ const probabilities = Array.from({ length: levels }, () => 0);
102
+ for (const [key, value] of Object.entries(raw.probabilities)) {
103
+ const index = Number(key);
104
+ if (!Number.isInteger(index) || index < 0 || index >= levels || String(index) !== key || !isProbability(value))
105
+ return null;
106
+ probabilities[index] = value;
107
+ }
108
+ const sum = probabilities.reduce((total, value) => total + value, 0);
109
+ if (!(Math.abs(sum - 1) <= SUM_TOLERANCE))
110
+ return null;
111
+ const level = probabilities.indexOf(Math.max(...probabilities));
112
+ return { type: "score", score, level, probabilities };
113
+ }
114
+ function parseYesNo(raw) {
115
+ return isProbability(raw.noul) ? { type: "yesno", p: raw.noul } : null;
116
+ }
117
+ const WIRE_TYPE = { choice: "choice", score: "score", yesno: "noul" };
118
+ /** Strictly decode one response body against the questions that were asked. */
119
+ export function parseJevResponse(body, questions) {
120
+ if (!isRecord(body) || !isRecord(body.answers))
121
+ return { ok: false };
122
+ const answers = {};
123
+ for (const [id, question] of Object.entries(questions)) {
124
+ const raw = body.answers[id];
125
+ if (!isRecord(raw))
126
+ return { ok: false };
127
+ if (raw.type !== undefined && raw.type !== WIRE_TYPE[question.type])
128
+ return { ok: false };
129
+ const parsed = question.type === "choice" ? parseChoice(raw, question)
130
+ : question.type === "score" ? parseScore(raw, question)
131
+ : parseYesNo(raw);
132
+ if (!parsed)
133
+ return { ok: false };
134
+ answers[id] = parsed;
135
+ }
136
+ const usage = isRecord(body.usage) ? body.usage.input_tokens : undefined;
137
+ const inputTokens = typeof usage === "number" && Number.isFinite(usage) && usage >= 0 ? usage : undefined;
138
+ const model = typeof body.model === "string" && body.model.length <= 80 ? body.model : undefined;
139
+ return { ok: true, answers, ...(inputTokens !== undefined ? { inputTokens } : {}), ...(model ? { model } : {}) };
140
+ }
141
+ function refusal(status) {
142
+ if (status === 401 || status === 403)
143
+ return { ok: false, reason: "rejected", status };
144
+ if (status === 429)
145
+ return { ok: false, reason: "rate_limited", status };
146
+ if (status === 529 || status === 503)
147
+ return { ok: false, reason: "overloaded", status };
148
+ return { ok: false, reason: "http_error", status };
149
+ }
150
+ async function decide(request) {
151
+ const endpoint = jevEndpoint(request.baseUrl);
152
+ if (!endpoint || !questionsFit(request.questions))
153
+ return { ok: false, reason: "misconfigured" };
154
+ let response;
155
+ try {
156
+ response = await request.fetch(endpoint, {
157
+ method: "POST",
158
+ headers: { authorization: `Bearer ${request.key}`, "content-type": "application/json", accept: "application/json" },
159
+ body: JSON.stringify(jevRequestBody(request.state, request.questions)),
160
+ signal: request.signal,
161
+ // Never replay the key to wherever a redirect points.
162
+ redirect: "error",
163
+ });
164
+ }
165
+ catch {
166
+ // The caller tells a timeout or a Stop from a dead network by its own signals.
167
+ return { ok: false, reason: "unreachable" };
168
+ }
169
+ if (!response.ok) {
170
+ await response.body?.cancel().catch(() => undefined);
171
+ return refusal(response.status);
172
+ }
173
+ let body;
174
+ try {
175
+ body = await response.json();
176
+ }
177
+ catch {
178
+ return { ok: false, reason: "malformed" };
179
+ }
180
+ const parsed = parseJevResponse(body, request.questions);
181
+ return parsed.ok ? parsed : { ok: false, reason: "malformed" };
182
+ }
183
+ export const jevBackend = { id: "jev", decide };
@@ -0,0 +1,62 @@
1
+ // The decision model's own log: one row per call that reached a backend,
2
+ // one file per UTC month under <data>/decider-log/YYYY-MM.ndjson.
3
+ //
4
+ // Not the authorization decision log (server/decision-log.ts, <data>/
5
+ // decisions/), which records approvals. This one records what a fast
6
+ // classifier picked and how sure it was, so its accuracy and cost can be
7
+ // measured later. A row never holds the message text, the options' text or
8
+ // the key: only the seam, the chosen option key (a bot id for room routing),
9
+ // the probabilities and a short hash of the state.
10
+ //
11
+ // Same discipline as the other logs: 0600, fire-and-forget, serialized per
12
+ // directory, and months older than the shared retention window pruned.
13
+ import { createHash } from "node:crypto";
14
+ import { appendFile, mkdir } from "node:fs/promises";
15
+ import { join } from "node:path";
16
+ import { boundRetentionDays, pruneMonthFiles } from "../decision-log.js";
17
+ export const DECIDER_LOG_DIR = "decider-log";
18
+ const PRUNE_EVERY_MS = 60 * 60_000;
19
+ const writeQueues = new Map();
20
+ const lastPrune = new Map();
21
+ export function stateHash(state) {
22
+ let text;
23
+ try {
24
+ text = JSON.stringify(state) ?? "";
25
+ }
26
+ catch {
27
+ text = "";
28
+ }
29
+ return createHash("sha256").update(text).digest("hex").slice(0, 16);
30
+ }
31
+ function monthKey(at) {
32
+ return `${at.getUTCFullYear()}-${String(at.getUTCMonth() + 1).padStart(2, "0")}`;
33
+ }
34
+ export function deciderLogFileFor(dataDir, at) {
35
+ return join(dataDir, DECIDER_LOG_DIR, `${monthKey(at)}.ndjson`);
36
+ }
37
+ async function write(dataDir, row) {
38
+ const at = new Date(row.at);
39
+ await mkdir(join(dataDir, DECIDER_LOG_DIR), { recursive: true, mode: 0o700 });
40
+ await appendFile(deciderLogFileFor(dataDir, at), JSON.stringify(row) + "\n", { mode: 0o600 });
41
+ if (at.getTime() - (lastPrune.get(dataDir) ?? 0) >= PRUNE_EVERY_MS) {
42
+ lastPrune.set(dataDir, at.getTime());
43
+ await pruneMonthFiles(join(dataDir, DECIDER_LOG_DIR), boundRetentionDays(), at);
44
+ }
45
+ }
46
+ /** Append one row. Never throws and never waits: a full disk must not turn
47
+ * into a slower or failed room turn. */
48
+ export function appendDeciderLog(dataDir, row) {
49
+ const previous = writeQueues.get(dataDir) ?? Promise.resolve();
50
+ const queued = previous.then(() => write(dataDir, row)).catch(() => {
51
+ /* logging must never take down a decision */
52
+ });
53
+ writeQueues.set(dataDir, queued);
54
+ void queued.finally(() => {
55
+ if (writeQueues.get(dataDir) === queued)
56
+ writeQueues.delete(dataDir);
57
+ });
58
+ }
59
+ /** Test seam: wait until every queued row for this directory is on disk. */
60
+ export async function flushDeciderLog(dataDir) {
61
+ await writeQueues.get(dataDir);
62
+ }
@@ -0,0 +1,52 @@
1
+ // What Cloud Pro's decision relay accepts through the included token
2
+ // (docs/cloud-pro.md, "Included Boat computers, voice and decisions"; the
3
+ // Admin checks the same): room routing's one request and the Settings key
4
+ // check, within its size caps. Anything else (another job, another question,
5
+ // a larger state) is never sent through the included token: it goes only
6
+ // with the person's own key, until the relay accepts it too.
7
+ import { jevRequestBody } from "./jev.js";
8
+ import { ROOM_ROUTING_INSTRUCTIONS, ROOM_ROUTING_STATE_KEYS } from "./room-routing.js";
9
+ /** The Settings key check's fixed request. */
10
+ export const KEY_CHECK_STATE = { purpose: "OpenMausBot is checking that a decision-model key works." };
11
+ export const KEY_CHECK_QUESTION = "Is this a connection check?";
12
+ /** The relay's caps: the whole request body, and the state as JSON. Both
13
+ * measured in UTF-8 bytes, which is never less than characters. */
14
+ export const RELAY_MAX_BODY_BYTES = 64 * 1024;
15
+ export const RELAY_MAX_STATE_BYTES = 24_000;
16
+ /** Where a request may use the included token. A job added later is not here
17
+ * until the relay accepts it. */
18
+ const RELAY_SEAMS = new Set(["roomRouting", "keyCheck"]);
19
+ const ROOM_STATE_KEYS = new Set(ROOM_ROUTING_STATE_KEYS);
20
+ const ROOM_STATE_REQUIRED = ROOM_ROUTING_STATE_KEYS.filter((key) => key !== "recent_messages");
21
+ export function relaySeam(seam) {
22
+ return RELAY_SEAMS.has(seam);
23
+ }
24
+ const isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
25
+ function shapeFits(seam, state, questions) {
26
+ const ids = Object.keys(questions);
27
+ const question = questions.answer;
28
+ if (ids.length !== 1 || !question)
29
+ return false;
30
+ if (seam === "keyCheck") {
31
+ return question.type === "yesno" && question.instructions === KEY_CHECK_QUESTION && !question.criteria &&
32
+ JSON.stringify(state) === JSON.stringify(KEY_CHECK_STATE);
33
+ }
34
+ if (seam === "roomRouting") {
35
+ return question.type === "choice" && question.instructions === ROOM_ROUTING_INSTRUCTIONS && isRecord(state) &&
36
+ Object.keys(state).every((key) => ROOM_STATE_KEYS.has(key)) && ROOM_STATE_REQUIRED.every((key) => key in state);
37
+ }
38
+ // Any other seam: the relay takes nothing from it.
39
+ return false;
40
+ }
41
+ /** Whether this exact request may go through the included token. */
42
+ export function relayAccepts(seam, state, questions) {
43
+ try {
44
+ if (!shapeFits(seam, state, questions))
45
+ return false;
46
+ return Buffer.byteLength(JSON.stringify(state)) <= RELAY_MAX_STATE_BYTES &&
47
+ Buffer.byteLength(JSON.stringify(jevRequestBody(state, questions))) <= RELAY_MAX_BODY_BYTES;
48
+ }
49
+ catch {
50
+ return false;
51
+ }
52
+ }
@@ -0,0 +1,88 @@
1
+ /** Turn starts take seconds anyway, and people far from the US West Coast
2
+ * see 400–700 ms round trips; 1.5 s keeps a slow answer usable. */
3
+ export const ROOM_ROUTING_TIMEOUT_MS = 1_500;
4
+ /** Bench calibration: answers at >= 0.6 were right 92–99% of the time. */
5
+ export const ROOM_ROUTING_MIN_PROBABILITY = 0.6;
6
+ export const EVERYONE_OPTION = "__everyone__";
7
+ const EVERYONE_MEANING = "Several members: the message explicitly needs answers or work from more than one member of the room, for example it asks everyone or asks each member for their part.";
8
+ /** Fixed: Cloud Pro's decision relay accepts room routing only with these
9
+ * exact instructions (relay.ts). */
10
+ export const ROOM_ROUTING_INSTRUCTIONS = "Which bot in this room should answer `new_message`? Choose __everyone__ only when the message needs several members to answer.";
11
+ /** Every key the state may carry; `recent_messages` is left out when there
12
+ * are none. Cloud Pro's relay accepts no other. */
13
+ export const ROOM_ROUTING_STATE_KEYS = ["room", "humans_in_room", "bots_in_room", "recent_messages", "new_message"];
14
+ const NAME_MAX = 80;
15
+ const TITLE_MAX = 120;
16
+ const DESCRIPTION_MAX = 400;
17
+ const LINE_MAX = 500;
18
+ /** The room context window bounds how many lines; this bounds their size,
19
+ * because large irrelevant state makes the classifier worse, not better. */
20
+ const RECENT_BUDGET_CHARS = 6_000;
21
+ function clip(value, max) {
22
+ const flat = value.replace(/\s+/g, " ").trim();
23
+ return flat.length > max ? `${flat.slice(0, max - 1)}…` : flat;
24
+ }
25
+ /** What one bot is, as an option: "Maya, Product Designer. Owns UI…". */
26
+ export function memberOption(member) {
27
+ const name = clip(member.name, NAME_MAX) || "Unnamed bot";
28
+ const title = member.title ? clip(member.title, TITLE_MAX) : "";
29
+ const description = member.description ? clip(member.description, DESCRIPTION_MAX) : "";
30
+ return `${name}${title ? `, ${title}` : ""} bot.${description ? ` ${description}` : ""}`;
31
+ }
32
+ /** The newest lines that fit the budget, oldest first, each clipped. */
33
+ function recentWithinBudget(recent) {
34
+ const kept = [];
35
+ let used = 0;
36
+ for (let index = recent.length - 1; index >= 0; index--) {
37
+ const line = recent[index];
38
+ const text = clip(line.text, LINE_MAX);
39
+ if (!text)
40
+ continue;
41
+ const size = text.length + line.from.length;
42
+ if (used + size > RECENT_BUDGET_CHARS)
43
+ break;
44
+ used += size;
45
+ kept.unshift({ from: clip(line.from, NAME_MAX), text });
46
+ }
47
+ return kept;
48
+ }
49
+ export function roomRoutingRequest(input) {
50
+ const options = {};
51
+ for (const member of input.members)
52
+ options[member.id] = memberOption(member);
53
+ options[EVERYONE_OPTION] = EVERYONE_MEANING;
54
+ const recent = recentWithinBudget(input.recent);
55
+ const state = {
56
+ room: clip(input.room, NAME_MAX),
57
+ humans_in_room: input.humans.map((human) => clip(human, NAME_MAX)),
58
+ bots_in_room: input.members.map((member) => clip(member.name, NAME_MAX)),
59
+ ...(recent.length ? { recent_messages: recent } : {}),
60
+ new_message: { from: clip(input.message.from, NAME_MAX), text: input.message.text.trim().slice(0, 8_000) },
61
+ };
62
+ return { state, question: { instructions: ROOM_ROUTING_INSTRUCTIONS, options } };
63
+ }
64
+ /** Ask once and turn the answer into a route. Never throws. */
65
+ export async function decideRoomResponder(decider, input, options = {}) {
66
+ try {
67
+ if (input.members.length < 2)
68
+ return { kind: "fallback", reason: "no_choice" };
69
+ const { state, question } = roomRoutingRequest(input);
70
+ const result = await decider.choose("roomRouting", state, question, {
71
+ timeoutMs: options.timeoutMs ?? ROOM_ROUTING_TIMEOUT_MS,
72
+ signal: options.signal,
73
+ });
74
+ if (!result.ok)
75
+ return { kind: "fallback", reason: result.reason };
76
+ const { choice, pTop } = result.answers;
77
+ if (pTop < ROOM_ROUTING_MIN_PROBABILITY)
78
+ return { kind: "fallback", reason: "low_confidence" };
79
+ if (choice === EVERYONE_OPTION)
80
+ return { kind: "everyone", probability: pTop };
81
+ if (!input.members.some((member) => member.id === choice))
82
+ return { kind: "fallback", reason: "malformed" };
83
+ return { kind: "member", botId: choice, probability: pTop };
84
+ }
85
+ catch {
86
+ return { kind: "fallback", reason: "malformed" };
87
+ }
88
+ }
@@ -0,0 +1,5 @@
1
+ // The decision model's vocabulary, shared by every backend and caller. A
2
+ // question is one of three typed shapes; an answer comes back typed to match.
3
+ // Nothing here names a vendor: `jev` is one backend, and another (a local
4
+ // Jev-compatible server, an LLM) plugs in behind the same types.
5
+ export const DECIDER_JOBS = ["roomRouting"];
@@ -0,0 +1,26 @@
1
+ import { desktopViewerUrl } from "./routes/desktop-viewer.js";
2
+ export const viewerTargetId = (target) => `local/${target.key.replace(":", "-")}`;
3
+ /** Native owner windows keep the existing direct viewer and cookie isolation. */
4
+ export function localVmViewerStatus(status, auth) {
5
+ return auth.kind === "loopback" ? status : {
6
+ ...status,
7
+ viewer_url: status.viewer_url ? desktopViewerUrl(viewerTargetId({ key: status.target_key })) : "",
8
+ };
9
+ }
10
+ export function localDesktopTarget(target, deps) {
11
+ return {
12
+ key: target.key,
13
+ async resolve() {
14
+ const status = await deps.status(target);
15
+ if (!status.managed || !status.imageMatches || status.network !== "loopback" || status.container !== "running"
16
+ || !status.viewer_url) {
17
+ throw Object.assign(new Error("Local VM unavailable"), { status: 409 });
18
+ }
19
+ return {
20
+ port: status.viewer_port ?? 0,
21
+ password: new URLSearchParams(new URL(status.viewer_url).hash.slice(1)).get("password"),
22
+ touch: () => deps.touch(target),
23
+ };
24
+ },
25
+ };
26
+ }