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
@@ -1,18 +1,33 @@
1
1
  // Entry point of the OMB Cloud Pro home image (deploy/fly/Dockerfile).
2
2
  //
3
- // Starts as root only to hand a fresh Fly volume (mounted root-owned at
4
- // /data) to the unprivileged `maus` user, then drops privileges for good
5
- // and runs two children: the OpenMausBot server on 127.0.0.1:8799 (and its
6
- // webhook receiver on :8800) and the Caddy edge on 0.0.0.0:8080. The edge is
7
- // the only listener the network can reach, and it always forwards with
8
- // X-Forwarded-*, so request-auth.ts never grants a remote request loopback
9
- // trust. If either child exits, both stop and the machine restarts.
3
+ // Starts as root, hands a fresh Fly volume (mounted root-owned at /data) to
4
+ // the unprivileged `maus` user, and stays a small root supervisor of two
5
+ // children that run as `maus`: the OpenMausBot server on 127.0.0.1:8799
6
+ // (and its webhook receiver on :8800) and the Caddy edge on 0.0.0.0:8080.
7
+ // The edge is the only listener the network can reach, and it always
8
+ // forwards with X-Forwarded-*, so request-auth.ts never grants a remote
9
+ // request loopback trust. If either child exits, both stop and the machine
10
+ // restarts; the one exception is the server asking to be started again
11
+ // after a restore.
12
+ //
13
+ // The machine's secrets (the signing secret, the relay tokens) arrive as
14
+ // this process's environment. No child's environment ever carries them:
15
+ // /proc/<pid>/environ keeps a process's starting environment, readable by
16
+ // anything running as the same user (an engine's shell). The server gets
17
+ // them over an inherited pipe it reads first thing and closes
18
+ // (cloud-secrets.ts), and an environment built from an allow-list, so a
19
+ // secret the platform adds later never reaches it either. This process stays
20
+ // root, so its own environment and memory are out of `maus`'s reach, and it
21
+ // runs and trusts only code `maus` cannot change: the image's, never the
22
+ // volume's (codeTrustProblem).
10
23
  import { spawn } from "node:child_process";
11
24
  import { chownSync, readFileSync, statSync } from "node:fs";
12
25
  import { dirname, join, posix, resolve } from "node:path";
13
26
  import { fileURLToPath, pathToFileURL } from "node:url";
14
- import { cloudHomeConfiguration, cloudHomeHost, prepareCloudHomeVolume, withoutIgnoredCloudKeys } from "./cloud-home.js";
27
+ import { CLOUD_HOME_RESTART_EXIT_CODE, CLOUD_SECRETS_FD_ENV, cloudHomeConfiguration, cloudHomeHost, cloudHomeSecrets, prepareCloudHomeVolume, withoutCloudSecrets, withoutIgnoredCloudKeys, } from "./cloud-home.js";
15
28
  const SERVICE_USER = "maus";
29
+ /** The descriptor the server reads its secrets from. */
30
+ const SECRETS_FD = 3;
16
31
  /** uid/gid from /etc/passwd; Node has no getpwnam. */
17
32
  export function passwdIds(passwd, name) {
18
33
  for (const line of passwd.split("\n")) {
@@ -22,22 +37,85 @@ export function passwdIds(passwd, name) {
22
37
  }
23
38
  return null;
24
39
  }
25
- /** The server child's environment: the operator's contract plus fixed
26
- * ports and paths (Linux paths inside the image, so POSIX joins on every
27
- * host that builds them, tests included), never a platform gateway's settings. The edge child gets
28
- * only what it needs to route. */
40
+ /** What the server child may be given from this process's environment, by
41
+ * name: the process basics, what the image sets, and the Cloud home's
42
+ * contract that is not secret. Anything else (a secret the platform adds
43
+ * later, a test's key, a platform gateway's settings) is left out. */
44
+ const SERVER_ENV_NAMES = new Set([
45
+ "PATH", "SHELL", "HOSTNAME", "LANG", "LANGUAGE", "TZ", "TERM", "TMPDIR", "NO_COLOR", "NODE_ENV", "NODE_EXTRA_CA_CERTS", "SSL_CERT_FILE", "SSL_CERT_DIR",
46
+ "AGENT_BROWSER_EXECUTABLE_PATH", "OMB_STATIC_DIR", "OMB_DATA_DIR",
47
+ "OMB_CLOUD_ROLE", "OMB_CLOUD_MACHINE_ID", "OMB_CLOUD_ADMIN_URL", "OMB_CLOUD_IMAGE", "OMB_PUBLIC_URL", "OMB_WEBHOOK_PUBLIC_URL",
48
+ "OMB_CLOUD_BOAT_URL", "OMB_CLOUD_VOICE_URL", "OMB_CLOUD_DECIDER_URL", "OMB_TTS_DEFAULT_VOICE",
49
+ ]);
50
+ export function serverEnvironmentAllowed(name) {
51
+ return SERVER_ENV_NAMES.has(name) || name.startsWith("LC_");
52
+ }
53
+ /** The server child's environment: the allowed part of the operator's
54
+ * contract (serverEnvironmentAllowed) plus fixed ports and paths (Linux
55
+ * paths inside the image, so POSIX joins on every host that builds them,
56
+ * tests included), never a platform gateway's settings and never a secret
57
+ * (`secrets`, handed over the pipe instead). `dropped` names what was left
58
+ * out, for the log. The edge child gets only what it needs to route. */
29
59
  export function cloudHomeChildEnvironments(config, env, home) {
60
+ const offered = withoutCloudSecrets(withoutIgnoredCloudKeys(env));
61
+ const allowed = Object.fromEntries(Object.entries(offered).filter(([name, value]) => value !== undefined && serverEnvironmentAllowed(name)));
62
+ const dropped = Object.keys(offered).filter((name) => !serverEnvironmentAllowed(name) && name !== "HOME").sort();
30
63
  const server = {
31
- ...withoutIgnoredCloudKeys(env), HOME: home, OMB_DATA_DIR: env.OMB_DATA_DIR || posix.join(home, ".openmausbot"),
64
+ ...allowed, HOME: home, OMB_DATA_DIR: env.OMB_DATA_DIR || posix.join(home, ".openmausbot"),
32
65
  OMB_PORT: "8799", OMB_WEBHOOK_PORT: "8800", OMB_PUBLIC_URL: config.publicOrigin,
33
66
  OMB_WEBHOOK_PUBLIC_URL: env.OMB_WEBHOOK_PUBLIC_URL || config.publicOrigin,
67
+ [CLOUD_SECRETS_FD_ENV]: String(SECRETS_FD),
34
68
  };
35
69
  const edge = {
36
70
  PATH: env.PATH ?? "/usr/local/bin:/usr/bin:/bin", HOME: "/tmp/omb-edge",
37
71
  XDG_DATA_HOME: "/tmp/omb-edge/data", XDG_CONFIG_HOME: "/tmp/omb-edge/config",
38
72
  OMB_CLOUD_PUBLIC_HOST: cloudHomeHost(config),
39
73
  };
40
- return { server, edge };
74
+ return { server, edge, secrets: cloudHomeSecrets(env), dropped };
75
+ }
76
+ /** Why the root supervisor must not run or trust `files`, or null: each
77
+ * must be root's and not writable by anyone else (nor any folder above it),
78
+ * and none may live on the volume `home`, which `maus` owns. The image makes its code
79
+ * root's (deploy/fly/Dockerfile); a file `maus` could rewrite would run as
80
+ * root at the next start, or be handed the secrets. */
81
+ export function codeTrustProblem(files, home, stat = statSync) {
82
+ const volume = posix.join(home, "/");
83
+ for (const file of files) {
84
+ if (file === home || file.startsWith(volume))
85
+ return `${file} is on the volume`;
86
+ for (let path = file;; path = dirname(path)) {
87
+ let owner;
88
+ try {
89
+ owner = stat(path);
90
+ }
91
+ catch {
92
+ return `${path} cannot be checked`;
93
+ }
94
+ if (owner.uid !== 0)
95
+ return `${path} is not root's`;
96
+ // A folder with the sticky bit (/tmp) lets nobody replace root's files.
97
+ if (owner.mode & 0o022 && !(path !== file && owner.mode & 0o1000))
98
+ return `${path} is writable by others than root`;
99
+ if (dirname(path) === path)
100
+ break;
101
+ }
102
+ }
103
+ return null;
104
+ }
105
+ /** Start the server with its secrets on an inherited pipe (never its
106
+ * environment), as `ids` when given. The pipe is written and closed at once;
107
+ * nothing else is ever sent on it. */
108
+ export function spawnWithSecrets(command, args, env, secrets, ids) {
109
+ const child = spawn(command, args, { env, stdio: ["inherit", "inherit", "inherit", "pipe"], ...(ids ? { uid: ids.uid, gid: ids.gid } : {}) });
110
+ const pipe = child.stdio[SECRETS_FD];
111
+ pipe?.on("error", () => { });
112
+ pipe?.end(JSON.stringify(secrets));
113
+ return child;
114
+ }
115
+ /** What the launcher does when the server child exits: start it again only
116
+ * when it asked to (a committed restore), and only a few times in a row. */
117
+ export function serverExitAction(code, stopping, restarts) {
118
+ return !stopping && code === CLOUD_HOME_RESTART_EXIT_CODE && restarts < 5 ? "restart" : "stop";
41
119
  }
42
120
  export function startCloudHome(env = process.env) {
43
121
  process.umask(0o077);
@@ -48,8 +126,11 @@ export function startCloudHome(env = process.env) {
48
126
  for (const warning of config.warnings)
49
127
  console.warn(`cloud home: ${warning}`);
50
128
  const home = env.HOME || "/data";
129
+ // Who the children run as: `maus` when this starts as root (the image),
130
+ // else whoever started it (a test, a dev machine).
131
+ let ids = null;
51
132
  if (process.getuid?.() === 0) {
52
- const ids = passwdIds(readFileSync("/etc/passwd", "utf8"), SERVICE_USER);
133
+ ids = passwdIds(readFileSync("/etc/passwd", "utf8"), SERVICE_USER);
53
134
  if (!ids)
54
135
  throw new Error(`The ${SERVICE_USER} user is missing from this image.`);
55
136
  // A new volume is a root-owned mount point. Only the mount point itself
@@ -58,14 +139,32 @@ export function startCloudHome(env = process.env) {
58
139
  if (stat.uid !== ids.uid || stat.gid !== ids.gid)
59
140
  chownSync(home, ids.uid, ids.gid);
60
141
  process.setgroups?.([]);
61
- process.setgid(ids.gid);
62
- process.setuid(ids.uid);
142
+ // The volume is prepared as `maus`, so what it creates is theirs.
143
+ process.setegid(ids.gid);
144
+ process.seteuid(ids.uid);
145
+ try {
146
+ prepareCloudHomeVolume(home, config.machineId);
147
+ }
148
+ finally {
149
+ process.seteuid(0);
150
+ process.setegid(0);
151
+ }
152
+ }
153
+ else {
154
+ prepareCloudHomeVolume(home, config.machineId);
63
155
  }
64
- if (process.getuid?.() === 0)
65
- throw new Error("The Cloud home must run as an unprivileged user.");
66
- prepareCloudHomeVolume(home, config.machineId);
67
156
  const here = dirname(fileURLToPath(import.meta.url));
68
- const { server, edge } = cloudHomeChildEnvironments(config, env, home);
157
+ const edgeBin = env.OMB_CLOUD_EDGE_BIN || "/usr/local/bin/caddy";
158
+ const edgeConfig = env.OMB_CLOUD_EDGE_CONFIG || "/app/cloud/Caddyfile";
159
+ if (ids) {
160
+ const problem = codeTrustProblem([process.execPath, fileURLToPath(import.meta.url), join(here, "index.js"), edgeBin, edgeConfig], home);
161
+ if (problem)
162
+ throw new Error(`This image's code is not safe to run as root: ${problem}. Rebuild it from deploy/fly/Dockerfile.`);
163
+ }
164
+ const { server, edge, secrets, dropped } = cloudHomeChildEnvironments(config, env, home);
165
+ // Names only, never values: what the server does not get from here.
166
+ if (dropped.length)
167
+ console.log(`cloud home: not passed to the server: ${dropped.join(", ")}`);
69
168
  const children = [];
70
169
  let stopping = false;
71
170
  const stop = (failed) => {
@@ -81,14 +180,25 @@ export function startCloudHome(env = process.env) {
81
180
  child.kill("SIGKILL"); }, 20_000);
82
181
  force.unref();
83
182
  };
84
- const run = (command, args, childEnv) => {
85
- const child = spawn(command, args, { env: childEnv, stdio: "inherit" });
183
+ const watch = (child, again) => {
86
184
  children.push(child);
87
185
  child.once("error", () => stop(true));
88
- child.once("exit", () => stop(true));
186
+ child.once("exit", (code) => {
187
+ children.splice(children.indexOf(child), 1);
188
+ if (!again?.(code))
189
+ stop(true);
190
+ });
89
191
  };
90
- run(process.execPath, [join(here, "index.js")], server);
91
- run(env.OMB_CLOUD_EDGE_BIN || "/usr/local/bin/caddy", ["run", "--config", env.OMB_CLOUD_EDGE_CONFIG || "/app/cloud/Caddyfile", "--adapter", "caddyfile"], edge);
192
+ let restarts = 0;
193
+ const runServer = () => watch(spawnWithSecrets(process.execPath, [join(here, "index.js")], server, secrets, ids), (code) => {
194
+ if (serverExitAction(code, stopping, restarts) !== "restart")
195
+ return false;
196
+ restarts++;
197
+ runServer();
198
+ return true;
199
+ });
200
+ runServer();
201
+ watch(spawn(edgeBin, ["run", "--config", edgeConfig, "--adapter", "caddyfile"], { env: edge, stdio: "inherit", ...(ids ? { uid: ids.uid, gid: ids.gid } : {}) }));
92
202
  process.once("SIGTERM", () => stop(false));
93
203
  process.once("SIGINT", () => stop(false));
94
204
  }
@@ -1,5 +1,5 @@
1
- // OMB Cloud Pro home machine: the boot contract, the Admin's signed pairing
2
- // request, and the volume the machine lives on. docs/cloud-pro.md is the
1
+ // OMB Cloud Pro home machine: the boot contract, the places it offers, the
2
+ // Admin's signed pairing request, and the volume the machine lives on. docs/cloud-pro.md is the
3
3
  // contract of record (and openmaus-cloud docs/consumer-cloud.md its Admin
4
4
  // half); keep them in step.
5
5
  //
@@ -12,7 +12,10 @@
12
12
  // the machine's bootstrap secret (HMAC-SHA256 over the method, path, a
13
13
  // timestamp, a nonce and the body's hash); each valid request opens one
14
14
  // ordinary pairing window (sessions.ts: single use, at most ten minutes),
15
- // which the Admin hands to the person's signed-in desktop app.
15
+ // which the Admin hands to the person's signed-in desktop app. A request for
16
+ // a browser sign-in (the Cloud page's "Use in your browser") names the
17
+ // account that owns this Cloud and opens a window only this machine's web
18
+ // page redeems, for at most two minutes, after showing whose Cloud it is.
16
19
  //
17
20
  // Cloud Pro includes no AI. The person signs in on the machine with their own
18
21
  // Claude or ChatGPT account, or an API key, exactly as on any server; nothing
@@ -33,12 +36,20 @@ export const CLOUD_IGNORED_KEYS = ["OMB_HOSTED_MODEL_URL", "OMB_HOSTED_MODEL_TOK
33
36
  export const CLOUD_PAIRING_PATH = "/api/cloud/pairing";
34
37
  export const CLOUD_PAIRING_DEFAULT_TTL_S = 300;
35
38
  export const CLOUD_PAIRING_MAX_TTL_S = 600;
39
+ /** A browser sign-in's owner: printable ASCII with no space, `<` or `>`, and exactly one `@`. */
40
+ const OWNER_EMAIL = /^[!-;=?A-~]{1,64}@[!-;=?A-~]{1,189}$/;
41
+ /** A browser sign-in is redeemed the moment its tab loads. */
42
+ export const CLOUD_BROWSER_SIGN_IN_MAX_TTL_S = 120;
36
43
  /** How far a signed request's timestamp may be from this machine's clock. */
37
44
  export const CLOUD_PAIRING_SKEW_S = 300;
38
45
  /** How long a used nonce is refused. Longer than the whole accepted window. */
39
46
  export const CLOUD_PAIRING_NONCE_MS = 10 * 60_000;
40
47
  const MAX_NONCES = 10_000;
41
48
  export const CLOUD_HOME_MARKER = ".omb-cloud-home.json";
49
+ /** The server exits with this after a restore commits (Move to Cloud): the
50
+ * launcher then starts it again, and startup installs the restore. Any other
51
+ * exit stops the machine for Fly to restart. */
52
+ export const CLOUD_HOME_RESTART_EXIT_CODE = 75;
42
53
  export function cloudHomeConfigured(env = process.env) {
43
54
  return CLOUD_HOME_KEYS.some((key) => env[key] !== undefined);
44
55
  }
@@ -85,6 +96,9 @@ export function cloudHomeConfiguration(env = process.env) {
85
96
  warnings.push(`ignoring ${ignored.join(", ")}: Cloud Pro includes no AI; people sign in with their own Claude or ChatGPT account, or an API key`);
86
97
  return { machineId, adminOrigin, publicOrigin, bootstrapSecret, warnings };
87
98
  }
99
+ // The secrets and their pipe live in cloud-secrets.ts, which the server
100
+ // reads before anything else (cloud-secrets-boot.ts).
101
+ export { CLOUD_HOME_SECRET_KEYS, CLOUD_SECRETS_FD_ENV, cloudHomeSecrets, takeCloudSecrets, withoutCloudSecrets } from "./cloud-secrets.js";
88
102
  /** The environment without a platform gateway's settings (CLOUD_IGNORED_KEYS). */
89
103
  export function withoutIgnoredCloudKeys(env) {
90
104
  const kept = { ...env };
@@ -92,6 +106,37 @@ export function withoutIgnoredCloudKeys(env) {
92
106
  delete kept[key];
93
107
  return kept;
94
108
  }
109
+ // The places a Cloud home offers live in shared/cloud-home.ts, so the app
110
+ // lists exactly what the server accepts.
111
+ export { cloudHomeOffersPlace } from "../shared/cloud-home.js";
112
+ /** Why a Cloud home refuses a place it never offers (no "this computer" of
113
+ * the person's, no Local VM), in the words the person reads; undefined for a
114
+ * place it offers. A turn's error shows 160 characters, so each fits. */
115
+ export function cloudHomePlaceRefusal(place) {
116
+ if (place === "local")
117
+ return "This computer isn't a place on your OMB Cloud: its bots run in the cloud. Set Works on to Auto, Cloud or Browser, or lend your Mac under Settings → OMB Cloud.";
118
+ if (place === "vm")
119
+ return "Bots on your OMB Cloud can't use a Local VM: the cloud machine has no container runtime. Set Works on to Auto, Cloud or Browser.";
120
+ return undefined;
121
+ }
122
+ /** What a turn is told when Cloud is chosen but no Boat account is set up (no
123
+ * key of the person's and no included Boat). A Cloud home has no Local VM to
124
+ * suggest instead. */
125
+ export function boatNotConfiguredMessage(cloudHome) {
126
+ return `Cloud Boat is not configured — add a Boat API key or choose ${cloudHome ? "Browser" : "Local VM"}`;
127
+ }
128
+ /** The Cloud's setup checklist (docs/cloud-pro.md) has a "try something"
129
+ * step that is done once a bot's turn finishes on the machine itself. The
130
+ * server records when, once, in this Cloud's own onboarding record: that
131
+ * section never travels with Move to Cloud (workspace-backup-policy.ts), so a
132
+ * moved-in history of turns does not count. Null when there is nothing to
133
+ * record: not a Cloud home, already recorded, a failed or stopped turn, or a
134
+ * thread that is no bot's conversation or room. */
135
+ export function firstCloudTurnPatch(turn) {
136
+ if (!turn.cloudHome || turn.recorded || !turn.ok || !turn.known)
137
+ return null;
138
+ return { onboarding: { firstTurnAt: (turn.now ?? new Date()).toISOString() } };
139
+ }
95
140
  /** The Admin's side of the signature (openmaus-cloud cloudPairingSignature). */
96
141
  export function cloudPairingSignature(secret, timestamp, nonce, body) {
97
142
  const bodyHash = createHash("sha256").update(body).digest("base64url");
@@ -141,18 +186,32 @@ export function createCloudPairing(options) {
141
186
  }
142
187
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
143
188
  return { status: 400, body: { error: "invalid_body" } };
144
- const { label, ttlSeconds } = parsed;
189
+ const { label, ttlSeconds, purpose, owner } = parsed;
145
190
  // oxlint-disable-next-line no-control-regex
146
191
  if (label !== undefined && (typeof label !== "string" || label.length > 80 || /[\x00-\x1f\x7f]/.test(label)))
147
192
  return { status: 400, body: { error: "invalid_label" } };
148
193
  if (ttlSeconds !== undefined && (!Number.isSafeInteger(ttlSeconds) || ttlSeconds < 1))
149
194
  return { status: 400, body: { error: "invalid_ttl" } };
150
- const ttl = Math.min(ttlSeconds ?? CLOUD_PAIRING_DEFAULT_TTL_S, CLOUD_PAIRING_MAX_TTL_S);
195
+ if (purpose !== undefined && purpose !== "browser")
196
+ return { status: 400, body: { error: "invalid_purpose" } };
197
+ const browser = purpose === "browser";
198
+ // A browser sign-in names its Cloud's owner (the account's email), which the sign-in page shows before the
199
+ // person continues: one address in printable ASCII, as the Admin's email schemas allow, with no `<` or `>`.
200
+ if ((browser || owner !== undefined) && (typeof owner !== "string" || owner.length > 254 || !OWNER_EMAIL.test(owner))) {
201
+ return { status: 400, body: { error: "invalid_owner" } };
202
+ }
203
+ const ttl = Math.min(ttlSeconds ?? CLOUD_PAIRING_DEFAULT_TTL_S, browser ? CLOUD_BROWSER_SIGN_IN_MAX_TTL_S : CLOUD_PAIRING_MAX_TTL_S);
151
204
  const opened = sessions.openPairing({
152
205
  scopes: ["admin", "client"],
153
206
  label: typeof label === "string" && label.trim() ? label.trim() : "OMB Cloud",
154
207
  ttlMs: ttl * 1000,
208
+ browser,
209
+ ...(browser ? { owner: owner } : {}),
155
210
  });
211
+ // A browser sign-in has no code to type: only its credential redeems it, and saying `purpose` back tells the
212
+ // Admin this machine made one (a machine from before this ignores `purpose` and opens an ordinary window).
213
+ if (browser)
214
+ return { status: 200, body: { credential: opened.credential, expiresAt: opened.expiresAt, purpose: "browser" } };
156
215
  return { status: 200, body: { code: formatPairingCode(opened.code), credential: opened.credential, expiresAt: opened.expiresAt } };
157
216
  },
158
217
  };
@@ -0,0 +1,167 @@
1
+ // Who may use a Mac lent to an OMB Cloud home (docs/cloud-pro.md, "Let my
2
+ // Cloud use this Mac"). A Cloud home is one person's server, but not every
3
+ // turn on it is theirs: a guest the owner paired, a webhook's payload, or a
4
+ // line someone else slipped into a running turn must never reach the Mac.
5
+ //
6
+ // A turn may use the lent Mac only when the harness can prove it acts for the
7
+ // owner:
8
+ // - a conversation the owner started from one of their own devices (a live
9
+ // session with admin scope), or
10
+ // - a scheduled run of a routine whose instructions the owner wrote (created
11
+ // or edited from one of those devices), or a run the owner started by hand;
12
+ // and the conversation was opened by them and holds nobody else's words,
13
+ // ever: a resumed session carries everything said in it, so one line (or
14
+ // card answer) from a guest, a teammate bot or a local process, before or
15
+ // during the turn, takes that conversation out of lending. The owner starts a new one to lend again.
16
+ // Webhook runs, guests, rooms, a bot's delegated or peer turn, and anything
17
+ // unprovable never can.
18
+ // Everything here is pure; server/index.ts supplies the records.
19
+ import { createHash } from "node:crypto";
20
+ import { existsSync, lstatSync, readFileSync } from "node:fs";
21
+ import { z } from "zod";
22
+ import { writeFileAtomic } from "./atomic.js";
23
+ const personOf = (line) => line?.role === "user" && !line.peerAsk ? line.sender?.id : undefined;
24
+ export function cloudHomeTurnMayLend(turn) {
25
+ return cloudHomeLendingRefusal(turn) === null;
26
+ }
27
+ export function cloudHomeLendingRefusal(turn) {
28
+ const { request } = turn;
29
+ if (!request?.messageId || request.stopped || !request.generations.has(turn.generation))
30
+ return "unproven";
31
+ const index = turn.thread.findIndex(line => line.id === request.messageId);
32
+ if (index < 0)
33
+ return "unproven";
34
+ let owner;
35
+ if (request.automation === undefined) {
36
+ owner = turn.ownerPerson(personOf(turn.thread[index]));
37
+ }
38
+ else {
39
+ const run = turn.routineRun();
40
+ owner = request.automation !== "webhook" && Boolean(run) && run.triggerSource !== "webhook" &&
41
+ run.ownerAuthored && (run.triggerSource !== "manual" || run.ownerStarted);
42
+ }
43
+ if (!owner)
44
+ return "not-owner";
45
+ // A conversation someone else opened (and named) is theirs, whoever writes in it.
46
+ if (turn.starters?.some((person) => person !== undefined && !turn.ownerPerson(person)))
47
+ return "someone-else";
48
+ if (turn.reportsFromOthers)
49
+ return "someone-else";
50
+ // Nothing anyone else wrote (a guest, a teammate bot, a local process) may
51
+ // be anywhere in this conversation, before or after the request, as a line
52
+ // (sent, queued, steered or handed in) or as the answer to a card. A
53
+ // routine's own prompt line names nobody; its provenance was proven above.
54
+ const clean = ownerOnlyConversation(turn.thread, turn.ownerPerson, turn.cardAnswerer, request.automation !== undefined ? request.messageId : undefined);
55
+ return clean ? null : "someone-else";
56
+ }
57
+ /** A conversation only the owner has written in: every user line is theirs
58
+ * and every card someone answered was answered by them (`exempt`: one line,
59
+ * a routine's own prompt, that names nobody). The same test decides which of
60
+ * a bot's other conversations may feed a turn on a Cloud home (recall, the
61
+ * recent-work brief, memory capture, the daily log). */
62
+ export function ownerOnlyConversation(thread, ownerPerson, cardAnswerer, exempt) {
63
+ return thread.every(line => (line.role !== "user" || line.id === exempt || ownerPerson(personOf(line))) &&
64
+ (!answeredByPerson(line.card) || ownerPerson(cardAnswerer(line.card))));
65
+ }
66
+ /** A card someone answered: a person's verdict or words. The harness's own
67
+ * settlements (automatic approvals, a turn that ended) are marked dismissed
68
+ * or unavailable and carry nobody's words. */
69
+ function answeredByPerson(card) {
70
+ if (!card)
71
+ return false;
72
+ return (Boolean(card.answered) && card.answered !== "unavailable" && card.dismissed !== true) || Boolean(card.answeredText);
73
+ }
74
+ /** What a routine run does and when, reduced to what its author decides:
75
+ * the instructions, where and on what it runs, and its schedule. The
76
+ * scheduler never rewrites these (it does rewrite a routine's results thread
77
+ * on each run, so that is guarded by who edits the routine instead: see the
78
+ * routines PATCH route). An edit by anyone else changes the fingerprint. */
79
+ export function routineFingerprint(routine) {
80
+ const attachments = (routine.attachments ?? []).map(attachment => [attachment.id, attachment.path]);
81
+ return createHash("sha256").update(JSON.stringify([
82
+ routine.prompt ?? "", routine.target ?? null, routine.botId ?? "", routine.groupId ?? null, attachments,
83
+ routine.schedule ?? null, routine.runOn ?? null,
84
+ ])).digest("hex");
85
+ }
86
+ const authorsFile = z.object({
87
+ version: z.literal(1),
88
+ routines: z.record(z.string().max(128), z.string().regex(/^[a-f0-9]{64}$/)),
89
+ /** Whoever last wrote each routine (a person key), owner or not. */
90
+ writers: z.record(z.string().max(128), z.string().regex(/^p_[\w-]{22}$/)).optional(),
91
+ }).strict();
92
+ /** Which routines the owner wrote, as the fingerprint of what they wrote.
93
+ * Persisted beside the routines (owner-only). A missing or damaged file
94
+ * means none: it never grants anything by being absent. */
95
+ export function createCloudRoutineAuthors(file) {
96
+ let routines = {};
97
+ let writers = {};
98
+ try {
99
+ if (existsSync(file)) {
100
+ const stat = lstatSync(file);
101
+ if (stat.isFile() && !stat.isSymbolicLink() && stat.size <= 1_000_000) {
102
+ const parsed = authorsFile.parse(JSON.parse(readFileSync(file, "utf8")));
103
+ routines = parsed.routines;
104
+ writers = parsed.writers ?? {};
105
+ }
106
+ }
107
+ }
108
+ catch {
109
+ routines = {};
110
+ writers = {};
111
+ }
112
+ const save = () => writeFileAtomic(file, JSON.stringify({ version: 1, routines, writers }), { mode: 0o600 });
113
+ return {
114
+ /** The owner wrote this routine as it stands now. */
115
+ record(id, routine) { routines = { ...routines, [id]: routineFingerprint(routine) }; save(); },
116
+ /** The owner's authorship ends; whoever wrote it last is still named. */
117
+ forget(id) { if (Object.hasOwn(routines, id)) {
118
+ const next = { ...routines };
119
+ delete next[id];
120
+ routines = next;
121
+ save();
122
+ } },
123
+ /** Someone (a person key) created or edited this routine. */
124
+ wrote(id, person) { if (writers[id] !== person) {
125
+ writers = { ...writers, [id]: person };
126
+ save();
127
+ } },
128
+ /** Who last wrote this routine, when that is known. */
129
+ writer(id) { return Object.hasOwn(writers, id) ? writers[id] : undefined; },
130
+ /** The routine is gone: nothing about it is kept. */
131
+ remove(id) {
132
+ if (!Object.hasOwn(routines, id) && !Object.hasOwn(writers, id))
133
+ return;
134
+ const nextRoutines = { ...routines };
135
+ delete nextRoutines[id];
136
+ routines = nextRoutines;
137
+ const nextWriters = { ...writers };
138
+ delete nextWriters[id];
139
+ writers = nextWriters;
140
+ save();
141
+ },
142
+ /** Whether this routine, as it stands (or as a run snapshotted it), is
143
+ * exactly what the owner wrote. */
144
+ authored(id, routine) { return Object.hasOwn(routines, id) && routines[id] === routineFingerprint(routine); },
145
+ /** Whether the owner's fingerprint is on record for this routine at all,
146
+ * matching what it runs now or not. */
147
+ recorded(id) { return Object.hasOwn(routines, id); },
148
+ /** Everyone named as a writer (server/cloud-owner.ts). */
149
+ people() { return new Set(Object.values(writers)); },
150
+ /** Name `to` as the writer wherever `move` accepts the writer: a one-time
151
+ * migration (server/cloud-owner.ts). The routines that changed. */
152
+ reassign(move, to) {
153
+ const moved = [];
154
+ const next = { ...writers };
155
+ for (const [id, person] of Object.entries(writers))
156
+ if (person !== to && move(person)) {
157
+ next[id] = to;
158
+ moved.push(id);
159
+ }
160
+ if (moved.length) {
161
+ writers = next;
162
+ save();
163
+ }
164
+ return moved;
165
+ },
166
+ };
167
+ }