openmausbot 0.1.91 → 0.1.93

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 (164) hide show
  1. package/dist/assets/DesktopViewer-CKNR3SEW.js +5 -0
  2. package/dist/assets/{abnfDiagram-O67JEVCF-yP-w8pRq.js → abnfDiagram-O67JEVCF-Ckrm1I1X.js} +1 -1
  3. package/dist/assets/{arc-DsivfZEh.js → arc-BBdBuCgB.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-CILw7lzT.js → architectureDiagram-NJMV4G6O-CDdA3Hjs.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-DEQUV3HF.js → blockDiagram-BEXU5L5S-AUrsx1YR.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-lSXCZoKA.js → c4Diagram-YGBWAQC7-BEqQ0Unh.js} +1 -1
  7. package/dist/assets/channel-BU94VpxE.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-QCD5bgWP.js → chunk-2Q5K7J3B-CEEuvd4C.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-C5-iZJuL.js → chunk-3FUC2YCW-DyANCiHl.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-cxsawMpY.js → chunk-5VM5RSS4-Dx_Slpw9.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-DV8tV8H7.js → chunk-GTNCS2PH-Dl7h7iiD.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-CJFZyfbW.js → chunk-GWA4HPMP-BLx07SPb.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-BigbMXco.js → chunk-JWPE2WC7-BMxDj3n7.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-oVxTfpDy.js → chunk-NETBCI7D-CN66PT4H.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-CH4BiroW.js → chunk-WEXAMYUT-6HwhsDKw.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-KEDa6gax.js → chunk-XXDRQBXY-gWnLt7SN.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-BiPjfqVv.js → classDiagram-v2-NBCMYWYE-CMS5l2ea.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-Caxew3OY.js → cose-bilkent-JH36ORCC-CV7sQwIG.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-B_Pui369.js → cynefin-OW5HDTMX-CSyl-7vl.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-X64ydIhP.js → cynefinDiagram-VND7K2PF-BCLKL6az.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-TqYCI1E1.js → dagre-6A5THRUB-DiaZs6S1.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-TeOt3rAQ.js → diagram-22UHCM2B-BeTtmt3I.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-D70BLhDA.js → diagram-3UASUU5V-CsdBG-QQ.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-BbQDLwUC.js → diagram-ATOU4E4O-BczvdbR1.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-ChNJZ1u-.js → diagram-CDSNMT55-DHTTTMhi.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-DMBl2APa.js → diagram-MLGK6HIB-RTjHyd8z.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-CEqDchOp.js → diagram-MPIPVDR6-BDenHEH_.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-BVDKRp_-.js → ebnfDiagram-ZINNZB2B-BSiRSbtL.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-CRkofO9B.js → elk-276RUBZZ-BCAElss-.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-DruOMh5e.js → erDiagram-OPXOYQCR-C4_1ZPJe.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-8GMQeB7t.js → flowDiagram-KWPJA3E3-Bcb4Z75S.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-ORE9jcsM.js → ganttDiagram-FUAMR5RP-BiPOzxzb.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-BmsedsWh.js → gitGraphDiagram-X574FWY7-B1wtF3ZS.js} +1 -1
  34. package/dist/assets/index-Biji-bfL.css +1 -0
  35. package/dist/assets/{index-DKNEM8Q6.js → index-BkYwilkG.js} +1 -1
  36. package/dist/assets/index-DZlhoES0.js +597 -0
  37. package/dist/assets/{infoDiagram-VRGFBTTK-E_p7AooJ.js → infoDiagram-VRGFBTTK-A19No71U.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-Y-B2mKda.js → ishikawaDiagram-OU5B5YK6-BQsqJ_cV.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-eA8RELcK.js → journeyDiagram-ZHPQQLJL-DAO0pVan.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-DqIL2e8e.js → kanban-definition-PNTS6WVX-NepmKEe2.js} +1 -1
  41. package/dist/assets/{linear-yC_29R6e.js → linear-bq1PTDTW.js} +1 -1
  42. package/dist/assets/{mermaid.core-VhEMkSND.js → mermaid.core-s4LTbQle.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-oJ7kyQZy.js → mindmap-definition-NLK3R4M7-LtdbgHFH.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-BmfYcY0O.js → pegDiagram-GJSIUBJH-tdbaCxiK.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-DKvgP7w9.js → pieDiagram-5QR66LMP-DvkbpW8l.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-BEKL3W0L.js → quadrantDiagram-O4NWA36T-Do4KaH-E.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-BEsiqR5s.js → railroadDiagram-XR7U4H2S-BQWULOAs.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-DWBcWvAP.js → requirementDiagram-PLB6GJNP-w9kei1FU.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-rpZ2_O4s.js → sankeyDiagram-IPEJSGJF-D2gRy0e8.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-B99HQdJR.js → sequenceDiagram-PO4LG4MO-a2sO5_Eo.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-BjN0qT5T.js → sizeCapture-INFHLROL-CiPJmFEB.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-BbsVgGsC.js → stateDiagram-v2-GCMORJYK-DAkUUlL7.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-DG3gKkeR.js → swimlanes-2SLR337P-DANf2R8Y.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-CdHxG8aM.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-BTHOdtF_.js → timeline-definition-EJHVYXUP-BKpQmzrT.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-acLSLFwc.js → usecaseDiagram-POWQR4AR-BtI7qLN-.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-C5JzNX9G.js → vennDiagram-UO4OBE2U-gXJXrAG_.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-DL1-PrR_.js → wardleyDiagram-VNRHLVJA-GU8Po5-n.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-Bvr1477p.js → xychartDiagram-PMCCYNJV-BF4J30g6.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 +112 -14
  63. package/dist-server/companion/src/routes.js +24 -1
  64. package/dist-server/container-mcp.js +201 -16
  65. package/dist-server/drivers/agents-proxy.js +7 -4
  66. package/dist-server/enterprise/server/index.js +5 -1
  67. package/dist-server/index.js +10698 -4737
  68. package/dist-server/local-computer-proxy.js +133 -8
  69. package/dist-server/mcp-gate.js +16 -6
  70. package/dist-server/model-catalog/models-dev.snapshot.json +7299 -0
  71. package/dist-server/openmausbot.js +4656 -1258
  72. package/dist-server/pair-cli.js +4656 -1258
  73. package/dist-server/server/boat.js +5 -5
  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 +118 -26
  80. package/dist-server/server/cloud-home.js +39 -3
  81. package/dist-server/server/cloud-lending.js +74 -10
  82. package/dist-server/server/cloud-owner.js +196 -0
  83. package/dist-server/server/cloud-secrets-boot.js +6 -0
  84. package/dist-server/server/cloud-secrets.js +65 -0
  85. package/dist-server/server/computer-control.js +6 -0
  86. package/dist-server/server/config.js +187 -14
  87. package/dist-server/server/container-computer.js +46 -34
  88. package/dist-server/server/desktop-viewer-targets.js +26 -0
  89. package/dist-server/server/digest.js +12 -2
  90. package/dist-server/server/drivers/acp/core.js +339 -33
  91. package/dist-server/server/drivers/acp/cursor.js +13 -1
  92. package/dist-server/server/drivers/acp/opencode-go.js +614 -85
  93. package/dist-server/server/drivers/acp/quiet-status.js +257 -0
  94. package/dist-server/server/drivers/acp/qwen.js +18 -0
  95. package/dist-server/server/drivers/agents-catalog.js +6 -6
  96. package/dist-server/server/drivers/antigravity-acp.js +69 -11
  97. package/dist-server/server/drivers/antigravity-temp.js +360 -0
  98. package/dist-server/server/drivers/antigravity.js +21 -0
  99. package/dist-server/server/drivers/builtIn.js +2 -0
  100. package/dist-server/server/drivers/cerebras.js +108 -0
  101. package/dist-server/server/drivers/chat-mcp-tools.js +20 -3
  102. package/dist-server/server/drivers/chatgpt-plan-auth.js +481 -0
  103. package/dist-server/server/drivers/claude.js +329 -37
  104. package/dist-server/server/drivers/codex.js +228 -18
  105. package/dist-server/server/drivers/grok.js +2 -2
  106. package/dist-server/server/drivers/mistral.js +4 -3
  107. package/dist-server/server/drivers/openai-chat-protocol.js +4 -3
  108. package/dist-server/server/drivers/openai-chat.js +66 -4
  109. package/dist-server/server/drivers/openai-compat.js +73 -23
  110. package/dist-server/server/drivers/prompt-split.js +2 -0
  111. package/dist-server/server/engine-install.js +46 -12
  112. package/dist-server/server/env-path.js +21 -5
  113. package/dist-server/server/harness/registry.js +2 -2
  114. package/dist-server/server/index.js +1545 -238
  115. package/dist-server/server/lending-memory.js +479 -0
  116. package/dist-server/server/local-vm-idle.js +24 -6
  117. package/dist-server/server/local-vm-stop-reason.js +26 -0
  118. package/dist-server/server/managed-desktop.js +3 -0
  119. package/dist-server/server/mcp-bridge.js +5 -3
  120. package/dist-server/server/mcp-http.js +6 -3
  121. package/dist-server/server/mcp-oauth-discovery.js +133 -0
  122. package/dist-server/server/mcp-oauth-store.js +70 -0
  123. package/dist-server/server/mcp-oauth.js +550 -0
  124. package/dist-server/server/mcp-probe.js +14 -1
  125. package/dist-server/server/mcp-registry.js +43 -0
  126. package/dist-server/server/memory-upkeep.js +94 -84
  127. package/dist-server/server/model-catalog/catalog.js +374 -0
  128. package/dist-server/server/model-catalog/presets.js +176 -0
  129. package/dist-server/server/model-catalog/trim.js +219 -0
  130. package/dist-server/server/provider-key-check.js +5 -2
  131. package/dist-server/server/recent-work.js +1 -1
  132. package/dist-server/server/request-auth.js +23 -0
  133. package/dist-server/server/routes/antigravity-leftovers.js +45 -0
  134. package/dist-server/server/routes/bot-memory.js +31 -6
  135. package/dist-server/server/routes/desktop-viewer.js +238 -0
  136. package/dist-server/server/routine-requests.js +1 -1
  137. package/dist-server/server/routines.js +6 -3
  138. package/dist-server/server/sessions.js +56 -9
  139. package/dist-server/server/skills.js +25 -10
  140. package/dist-server/server/store.js +116 -122
  141. package/dist-server/server/thread-events.js +2 -0
  142. package/dist-server/server/thread-starters.js +33 -0
  143. package/dist-server/server/thread-title.js +46 -0
  144. package/dist-server/server/tts/fish.js +4 -5
  145. package/dist-server/server/tts/index.js +5 -3
  146. package/dist-server/server/vps-computer.js +80 -13
  147. package/dist-server/server/webhooks.js +18 -1
  148. package/dist-server/server/workspace-backup-policy.js +3 -0
  149. package/dist-server/server/workspace-backup.js +25 -0
  150. package/dist-server/server/workspace.js +66 -16
  151. package/dist-server/shared/approval-mode.js +1 -1
  152. package/dist-server/shared/redact.js +1 -0
  153. package/dist-server/shared/tool-name.js +22 -0
  154. package/dist-server/shared/workspace-backup-client.js +1 -0
  155. package/dist-server/src/lib/citations.js +177 -0
  156. package/dist-server/src/lib/composer-attachments.js +7 -1
  157. package/dist-server/vps-container-mcp.js +202 -16
  158. package/dist-server/workspace-backup.worker.js +87 -9
  159. package/enterprise/server/index.js +5 -1
  160. package/package.json +1 -1
  161. package/dist/assets/channel-DrfGMu7X.js +0 -1
  162. package/dist/assets/index-BlnCEcU6.js +0 -588
  163. package/dist/assets/index-DVxjOvID.css +0 -1
  164. package/dist/assets/swimlanesDiagram-TC7HE7FX-BCZhxl7e.js +0 -8
@@ -9,10 +9,10 @@
9
9
  // session with admin scope), or
10
10
  // - a scheduled run of a routine whose instructions the owner wrote (created
11
11
  // or edited from one of those devices), or a run the owner started by hand;
12
- // and the conversation holds nobody else's words, ever: a resumed session
13
- // carries everything said in it, so one line (or card answer) from a guest,
14
- // a teammate bot or a local process, before or during the turn, takes that
15
- // conversation out of lending. The owner starts a new one to lend again.
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
16
  // Webhook runs, guests, rooms, a bot's delegated or peer turn, and anything
17
17
  // unprovable never can.
18
18
  // Everything here is pure; server/index.ts supplies the records.
@@ -42,14 +42,27 @@ export function cloudHomeLendingRefusal(turn) {
42
42
  }
43
43
  if (!owner)
44
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";
45
50
  // Nothing anyone else wrote (a guest, a teammate bot, a local process) may
46
51
  // be anywhere in this conversation, before or after the request, as a line
47
52
  // (sent, queued, steered or handed in) or as the answer to a card. A
48
53
  // routine's own prompt line names nobody; its provenance was proven above.
49
- const clean = turn.thread.every((line, at) => (line.role !== "user" || (at === index && request.automation !== undefined) || turn.ownerPerson(personOf(line))) &&
50
- (!answeredByPerson(line.card) || turn.ownerPerson(turn.cardAnswerer(line.card))));
54
+ const clean = ownerOnlyConversation(turn.thread, turn.ownerPerson, turn.cardAnswerer, request.automation !== undefined ? request.messageId : undefined);
51
55
  return clean ? null : "someone-else";
52
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
+ }
53
66
  /** A card someone answered: a person's verdict or words. The harness's own
54
67
  * settlements (automatic approvals, a turn that ended) are marked dismissed
55
68
  * or unavailable and carry nobody's words. */
@@ -70,34 +83,85 @@ export function routineFingerprint(routine) {
70
83
  routine.schedule ?? null, routine.runOn ?? null,
71
84
  ])).digest("hex");
72
85
  }
73
- const authorsFile = z.object({ version: z.literal(1), routines: z.record(z.string().max(128), z.string().regex(/^[a-f0-9]{64}$/)) }).strict();
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();
74
92
  /** Which routines the owner wrote, as the fingerprint of what they wrote.
75
93
  * Persisted beside the routines (owner-only). A missing or damaged file
76
94
  * means none: it never grants anything by being absent. */
77
95
  export function createCloudRoutineAuthors(file) {
78
96
  let routines = {};
97
+ let writers = {};
79
98
  try {
80
99
  if (existsSync(file)) {
81
100
  const stat = lstatSync(file);
82
- if (stat.isFile() && !stat.isSymbolicLink() && stat.size <= 1_000_000)
83
- routines = authorsFile.parse(JSON.parse(readFileSync(file, "utf8"))).routines;
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
+ }
84
106
  }
85
107
  }
86
108
  catch {
87
109
  routines = {};
110
+ writers = {};
88
111
  }
89
- const save = () => writeFileAtomic(file, JSON.stringify({ version: 1, routines }), { mode: 0o600 });
112
+ const save = () => writeFileAtomic(file, JSON.stringify({ version: 1, routines, writers }), { mode: 0o600 });
90
113
  return {
91
114
  /** The owner wrote this routine as it stands now. */
92
115
  record(id, routine) { routines = { ...routines, [id]: routineFingerprint(routine) }; save(); },
116
+ /** The owner's authorship ends; whoever wrote it last is still named. */
93
117
  forget(id) { if (Object.hasOwn(routines, id)) {
94
118
  const next = { ...routines };
95
119
  delete next[id];
96
120
  routines = next;
97
121
  save();
98
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
+ },
99
142
  /** Whether this routine, as it stands (or as a run snapshotted it), is
100
143
  * exactly what the owner wrote. */
101
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
+ },
102
166
  };
103
167
  }
@@ -0,0 +1,196 @@
1
+ // An OMB Cloud home is personal (docs/cloud-pro.md): only the owner's own
2
+ // devices connect, each with an admin session the Admin's signed pairing
3
+ // gave it. No pairing, sign-in or session without admin scope is minted or
4
+ // redeemed there (server/sessions.ts requireAdmin), and at every boot any
5
+ // that is stored is revoked: what it opened or wrote is nobody's from then
6
+ // on, and its key is recorded first, so no crash can ever make it the
7
+ // owner's.
8
+ //
9
+ // Before, every device carried its own key (v0.1.91), so a device the owner
10
+ // unpaired made the owner's conversations read as someone else's. Once, at
11
+ // the first boot of a personal Cloud home, and again after a restore, the
12
+ // keys named until then are settled, in two tiers:
13
+ // - adopted: every key that opened a conversation, wrote a line or answered
14
+ // a card, except a revoked one. Its conversations are the owner's for who
15
+ // opened them, their approval level and their folder (so the owner's rooms
16
+ // and conversations keep Auto or Full, and their project folder);
17
+ // - proven: only a key with proof it was the owner's, an admin session
18
+ // still paired, or one that answered a card (only the owner's devices
19
+ // could, on a Cloud home). Only those feed lending, memory, recall and the
20
+ // recent-work brief (cloud-lending.ts ownerOnlyConversation): a guest the
21
+ // owner unpaired before is indistinguishable from an owner's old device,
22
+ // so its lines never count as the owner's there.
23
+ // Routines fail closed: a routine's writer is the owner only with proof (the
24
+ // owner's key as its writer, the owner's fingerprint, #2023, a proposal the
25
+ // owner approved on its card, or a restore the owner started whose results
26
+ // conversation is theirs); every other routine is nobody's, and runs
27
+ // confined. What a revoked session opened loses its working folder (its next
28
+ // turn gets a private one), and its routines are paused.
29
+ import { lstatSync, readFileSync } from "node:fs";
30
+ import { z } from "zod";
31
+ import { writeFileAtomic } from "./atomic.js";
32
+ export const CLOUD_PERSONAL_REFUSAL = "OMB Cloud is personal: only your own devices can connect.";
33
+ const KEY = /^p_[\w-]{22}$/;
34
+ const keys = z.array(z.string().regex(KEY)).max(100_000);
35
+ const recordFile = z.object({
36
+ version: z.literal(2),
37
+ machineId: z.string().max(128),
38
+ /** Revoked here, ever: never anyone's but nobody's. */
39
+ revokedKeys: keys,
40
+ adoptedKeys: keys,
41
+ provenKeys: keys,
42
+ /** The earlier keys are settled (and routines named). */
43
+ settled: z.boolean(),
44
+ /** A restore applied here whose data is not settled yet. */
45
+ pendingRestore: z.string().max(128).optional(),
46
+ /** The last restore whose data is settled (a restore applied at a start
47
+ * that ended before recording it is found this way at the next). */
48
+ settledRestore: z.string().max(128).optional(),
49
+ }).strict();
50
+ function readRecord(options) {
51
+ const fresh = { version: 2, machineId: options.machineId, revokedKeys: [], adoptedKeys: [], provenKeys: [], settled: false, fresh: true };
52
+ let stat;
53
+ try {
54
+ stat = lstatSync(options.file, { throwIfNoEntry: false });
55
+ }
56
+ catch {
57
+ stat = null;
58
+ }
59
+ if (stat === undefined)
60
+ return fresh;
61
+ try {
62
+ if (!stat || !stat.isFile() || stat.size > 16_000_000)
63
+ throw new Error("not a file");
64
+ const record = recordFile.parse(JSON.parse(readFileSync(options.file, "utf8")));
65
+ // Another machine's (never carried by a backup): nothing in it is ours.
66
+ if (record.machineId !== options.machineId) {
67
+ options.log(`cloud home: ${options.file} is another machine's; settling this one's own`);
68
+ return fresh;
69
+ }
70
+ return record;
71
+ }
72
+ catch {
73
+ // Settled before, and unreadable now: nothing earlier is the owner's, and
74
+ // running the settlement again could make a revoked key theirs.
75
+ options.log(`cloud home: ${options.file} is unreadable, so nothing from before is treated as the owner's`);
76
+ return { ...fresh, settled: true };
77
+ }
78
+ }
79
+ function save(options, record) {
80
+ try {
81
+ writeFileAtomic(options.file, JSON.stringify(record), { mode: 0o600 });
82
+ return true;
83
+ }
84
+ catch (error) {
85
+ options.log(`cloud home: could not record who is the owner (${error instanceof Error ? error.message : String(error)}); trying again at the next start`);
86
+ return false;
87
+ }
88
+ }
89
+ /** A step that never stops the server from starting: logged when it fails
90
+ * (`failed` is then called), and done again at the next start (every step
91
+ * can be). */
92
+ function attempt(options, what, step, fallback, failed) {
93
+ try {
94
+ return step();
95
+ }
96
+ catch (error) {
97
+ options.log(`cloud home: could not ${what} (${error instanceof Error ? error.message : String(error)})`);
98
+ failed?.();
99
+ return fallback;
100
+ }
101
+ }
102
+ /** The settled keys, never one revoked since. */
103
+ function settledSets(record) {
104
+ const revoked = new Set(record.revokedKeys);
105
+ return { adopted: new Set(record.adoptedKeys.filter((key) => !revoked.has(key))), proven: new Set(record.provenKeys.filter((key) => !revoked.has(key))) };
106
+ }
107
+ /** Revoke every session that is not one of the owner's devices, and settle
108
+ * the earlier keys (once, and after a restore). Every step is recorded
109
+ * before it is taken, so a crash anywhere, or a failed save, only ever
110
+ * leaves a key less trusted, and the next start carries on. */
111
+ export function settleCloudOwnership(options) {
112
+ const { fresh, ...read } = readRecord(options);
113
+ let record = read;
114
+ const others = options.sessions.list().filter((session) => !session.scopes.includes("admin"));
115
+ const revoked = new Set([...record.revokedKeys, ...others.map((session) => options.personKey(session))]);
116
+ // A restore to settle: one applied at this start, or a later one than the
117
+ // last settled (its start ended before it was recorded). At the very first
118
+ // start, only one applied now: what came before settles the strict way.
119
+ const restore = options.restoredNow ??
120
+ (!fresh && options.lastRestore && options.lastRestore !== record.settledRestore && options.lastRestore !== record.pendingRestore ? options.lastRestore : undefined);
121
+ record = { ...record, revokedKeys: [...revoked], ...(restore ? { pendingRestore: restore } : {}),
122
+ ...(fresh && !options.restoredNow && options.lastRestore ? { settledRestore: options.lastRestore } : {}) };
123
+ // 1. The keys about to be revoked are recorded first.
124
+ if (!save(options, record))
125
+ return settledSets(record);
126
+ // 2. Their sessions go.
127
+ let count = 0;
128
+ for (const session of others)
129
+ if (attempt(options, "revoke a session", () => options.sessions.revoke(session.id), false))
130
+ count += 1;
131
+ options.log(`cloud home: revoked ${count} session${count === 1 ? "" : "s"} that ${count === 1 ? "was" : "were"} not the owner's own device${count === 1 ? "" : "s"}`);
132
+ // 3. What any revoked key opened or wrote is nobody's: its conversations
133
+ // lose their working folder (the owner's project, often), its routines stop.
134
+ const orphans = attempt(options, "mark conversations as nobody's", () => options.starters.reassign((person) => revoked.has(person), options.nobodyKey), []);
135
+ if (orphans.length)
136
+ attempt(options, "unpin their folders", () => options.unpin(orphans), undefined);
137
+ const stopped = attempt(options, "mark routines as nobody's", () => options.writers.reassign((person) => revoked.has(person), options.nobodyKey), []);
138
+ if (stopped.length)
139
+ attempt(options, "pause their routines", () => options.routines.pause(stopped), undefined);
140
+ if (fresh) {
141
+ options.log("cloud home: only your own devices can connect now. Review Settings → Remote access → Paired devices, and sign out any device that isn't yours.");
142
+ }
143
+ const restoring = record.pendingRestore !== undefined;
144
+ if (record.settled && !restoring)
145
+ return settledSets(record);
146
+ // 4. The earlier keys, recorded before anything is rewritten.
147
+ const eligible = (person) => KEY.test(person) && person !== options.ownerKey && person !== options.nobodyKey && !revoked.has(person);
148
+ // Settled only once every step below is done; else the next start does it again.
149
+ let complete = true;
150
+ const incomplete = () => { complete = false; };
151
+ const lines = attempt(options, "read who wrote in each conversation", () => options.lines(), { senders: [], answerers: [], approvals: [] }, incomplete);
152
+ const admins = options.sessions.list().filter((session) => session.scopes.includes("admin")).map((session) => options.personKey(session));
153
+ const proven = new Set([...record.provenKeys, ...admins, ...lines.answerers].filter(eligible));
154
+ const adopted = new Set([...record.adoptedKeys, ...proven, ...options.starters.people(), ...lines.senders].filter(eligible));
155
+ record = { ...record, adoptedKeys: [...adopted], provenKeys: [...proven] };
156
+ if (!save(options, record))
157
+ return settledSets(record);
158
+ // 5. Conversations the owner provably opened name the owner; routines are
159
+ // the owner's only with proof, else nobody's.
160
+ const moved = attempt(options, "name the owner on their conversations", () => options.starters.reassign((person) => proven.has(person), options.ownerKey), [], incomplete).length;
161
+ const ownersKey = (person) => person !== undefined && (person === options.ownerKey || proven.has(person));
162
+ // A card the owner allowed is proof of what it showed them: only while the
163
+ // routine still runs exactly that (its instructions, bot, schedule, where
164
+ // it runs and no attachment: anyone could change a routine on v0.1.91
165
+ // without being recorded).
166
+ const approved = new Set();
167
+ for (const [id, person, shape] of lines.approvals) {
168
+ if (ownersKey(person) && options.routines.shape(id) === shape)
169
+ approved.add(id);
170
+ }
171
+ let owners = 0, nobodys = 0;
172
+ attempt(options, "name each routine's writer", () => {
173
+ for (const id of options.routines.ids()) {
174
+ const writer = options.routines.writer(id);
175
+ // A restore is proof only for a routine reporting into a conversation
176
+ // that names nobody yet, or the owner (a backup from before can hold a
177
+ // guest's routine and its conversation).
178
+ const opener = options.routines.resultsOpener(id);
179
+ const owner = writer === options.ownerKey || options.routines.fingerprinted(id) || approved.has(id) ||
180
+ (restoring && writer !== options.nobodyKey && !(writer !== undefined && revoked.has(writer)) && (opener === undefined || ownersKey(opener)));
181
+ if (writer !== (owner ? options.ownerKey : options.nobodyKey))
182
+ options.routines.name(id, owner ? options.ownerKey : options.nobodyKey);
183
+ if (owner)
184
+ owners += 1;
185
+ else
186
+ nobodys += 1;
187
+ }
188
+ }, undefined, incomplete);
189
+ if (!complete)
190
+ return settledSets(record);
191
+ const { pendingRestore: done, ...rest } = record;
192
+ record = { ...rest, settled: true, ...(done ? { settledRestore: done } : {}) };
193
+ save(options, record);
194
+ options.log(`cloud home: settled what came before${restoring ? " (a restore)" : ""}: ${adopted.size} earlier device key${adopted.size === 1 ? "" : "s"} (${proven.size} proven), ${moved} conversation${moved === 1 ? "" : "s"} named the owner's, ${owners} routine${owners === 1 ? "" : "s"} the owner's and ${nobodys} nobody's`);
195
+ return settledSets(record);
196
+ }
@@ -0,0 +1,6 @@
1
+ // The server's first import (server/index.ts): on an OMB Cloud home, the
2
+ // secrets the launcher hands over its pipe are read, and the pipe closed,
3
+ // before any other module is loaded, so no process the server starts can
4
+ // inherit it (cloud-secrets.ts, cloud-home-start.ts).
5
+ import { takeCloudSecrets } from "./cloud-secrets.js";
6
+ export const BOOT_CLOUD_SECRETS = takeCloudSecrets();
@@ -0,0 +1,65 @@
1
+ // The OMB Cloud home's secrets, and how the server gets them
2
+ // (docs/cloud-pro.md). This module imports nothing but node:fs, so the
3
+ // server can read them before anything else it loads can start a process
4
+ // (cloud-secrets-boot.ts, the server's first import).
5
+ import { closeSync, readFileSync } from "node:fs";
6
+ /** The Cloud home's secrets: the signing secret and the included services'
7
+ * relay tokens (server/included-services.ts). The Admin sets them as Fly app
8
+ * secrets, which reach the machine as the launcher's environment; the
9
+ * launcher never puts them in a child's environment, because
10
+ * /proc/<pid>/environ keeps a process's starting environment for anything
11
+ * running as the same user to read. It hands them to the server over an
12
+ * inherited pipe instead (CLOUD_SECRETS_FD_ENV names its descriptor). */
13
+ export const CLOUD_HOME_SECRET_KEYS = ["OMB_CLOUD_BOOTSTRAP_SECRET", "OMB_CLOUD_BOAT_TOKEN", "OMB_CLOUD_VOICE_TOKEN", "OMB_CLOUD_DECIDER_TOKEN"];
14
+ export const CLOUD_SECRETS_FD_ENV = "OMB_CLOUD_SECRETS_FD";
15
+ /** The secrets present in an environment (the launcher's). */
16
+ export function cloudHomeSecrets(env) {
17
+ return Object.fromEntries(CLOUD_HOME_SECRET_KEYS.flatMap((key) => env[key] ? [[key, env[key]]] : []));
18
+ }
19
+ /** An environment with no Cloud home secret in it. */
20
+ export function withoutCloudSecrets(env) {
21
+ const kept = { ...env };
22
+ for (const key of CLOUD_HOME_SECRET_KEYS)
23
+ delete kept[key];
24
+ return kept;
25
+ }
26
+ function invalid(why) {
27
+ throw new Error(`Cloud home configuration is invalid: ${why}.`);
28
+ }
29
+ /** At server startup: the secrets the launcher handed over its pipe (read
30
+ * to the end and closed, so no child inherits it), or none when the server
31
+ * was started another way and reads its environment as before. Only the
32
+ * known secret names are taken. */
33
+ export function takeCloudSecrets(env = process.env) {
34
+ const named = env[CLOUD_SECRETS_FD_ENV];
35
+ if (named === undefined)
36
+ return {};
37
+ delete env[CLOUD_SECRETS_FD_ENV];
38
+ const fd = Number(named);
39
+ if (!Number.isInteger(fd) || fd < 3 || fd > 1024)
40
+ invalid(`${CLOUD_SECRETS_FD_ENV} must name an inherited descriptor`);
41
+ let raw;
42
+ try {
43
+ raw = readFileSync(fd, "utf8");
44
+ }
45
+ catch {
46
+ invalid("the Cloud home's secrets could not be read from the launcher");
47
+ }
48
+ finally {
49
+ try {
50
+ closeSync(fd);
51
+ }
52
+ catch { /* already closed */ }
53
+ }
54
+ let parsed;
55
+ try {
56
+ parsed = JSON.parse(raw);
57
+ }
58
+ catch {
59
+ parsed = null;
60
+ }
61
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
62
+ invalid("the Cloud home's secrets from the launcher are not readable");
63
+ const record = parsed;
64
+ return Object.fromEntries(CLOUD_HOME_SECRET_KEYS.flatMap((key) => typeof record[key] === "string" && record[key] ? [[key, record[key]]] : []));
65
+ }
@@ -71,6 +71,12 @@ export class ComputerControl {
71
71
  });
72
72
  return { snapshot: this.changed(botId), owned: true, acquired: true };
73
73
  }
74
+ /** Whether this workspace lease is the one holding the wheel right now.
75
+ * Read-only: unlike `acquireLease`, a free computer is not taken. */
76
+ ownsLease(botId, controlLeaseId) {
77
+ const entry = this.entries.get(botId);
78
+ return entry?.heldSinceMs != null && entry.controlLeaseId === controlLeaseId;
79
+ }
74
80
  /** The person hands the wheel back. Also settles any open help request —
75
81
  * the waiting bot resumes from this one state change. */
76
82
  release(botId) {