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.
- package/dist/assets/DesktopViewer-CKNR3SEW.js +5 -0
- package/dist/assets/{abnfDiagram-O67JEVCF-yP-w8pRq.js → abnfDiagram-O67JEVCF-Ckrm1I1X.js} +1 -1
- package/dist/assets/{arc-DsivfZEh.js → arc-BBdBuCgB.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-CILw7lzT.js → architectureDiagram-NJMV4G6O-CDdA3Hjs.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-DEQUV3HF.js → blockDiagram-BEXU5L5S-AUrsx1YR.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-lSXCZoKA.js → c4Diagram-YGBWAQC7-BEqQ0Unh.js} +1 -1
- package/dist/assets/channel-BU94VpxE.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-QCD5bgWP.js → chunk-2Q5K7J3B-CEEuvd4C.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-C5-iZJuL.js → chunk-3FUC2YCW-DyANCiHl.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-cxsawMpY.js → chunk-5VM5RSS4-Dx_Slpw9.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-DV8tV8H7.js → chunk-GTNCS2PH-Dl7h7iiD.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-CJFZyfbW.js → chunk-GWA4HPMP-BLx07SPb.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-BigbMXco.js → chunk-JWPE2WC7-BMxDj3n7.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-oVxTfpDy.js → chunk-NETBCI7D-CN66PT4H.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-CH4BiroW.js → chunk-WEXAMYUT-6HwhsDKw.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-KEDa6gax.js → chunk-XXDRQBXY-gWnLt7SN.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-BiPjfqVv.js → classDiagram-v2-NBCMYWYE-CMS5l2ea.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-Caxew3OY.js → cose-bilkent-JH36ORCC-CV7sQwIG.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-B_Pui369.js → cynefin-OW5HDTMX-CSyl-7vl.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-X64ydIhP.js → cynefinDiagram-VND7K2PF-BCLKL6az.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-TqYCI1E1.js → dagre-6A5THRUB-DiaZs6S1.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-TeOt3rAQ.js → diagram-22UHCM2B-BeTtmt3I.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-D70BLhDA.js → diagram-3UASUU5V-CsdBG-QQ.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-BbQDLwUC.js → diagram-ATOU4E4O-BczvdbR1.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-ChNJZ1u-.js → diagram-CDSNMT55-DHTTTMhi.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-DMBl2APa.js → diagram-MLGK6HIB-RTjHyd8z.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-CEqDchOp.js → diagram-MPIPVDR6-BDenHEH_.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-BVDKRp_-.js → ebnfDiagram-ZINNZB2B-BSiRSbtL.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-CRkofO9B.js → elk-276RUBZZ-BCAElss-.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-DruOMh5e.js → erDiagram-OPXOYQCR-C4_1ZPJe.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-8GMQeB7t.js → flowDiagram-KWPJA3E3-Bcb4Z75S.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-ORE9jcsM.js → ganttDiagram-FUAMR5RP-BiPOzxzb.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-BmsedsWh.js → gitGraphDiagram-X574FWY7-B1wtF3ZS.js} +1 -1
- package/dist/assets/index-Biji-bfL.css +1 -0
- package/dist/assets/{index-DKNEM8Q6.js → index-BkYwilkG.js} +1 -1
- package/dist/assets/index-DZlhoES0.js +597 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-E_p7AooJ.js → infoDiagram-VRGFBTTK-A19No71U.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-Y-B2mKda.js → ishikawaDiagram-OU5B5YK6-BQsqJ_cV.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-eA8RELcK.js → journeyDiagram-ZHPQQLJL-DAO0pVan.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-DqIL2e8e.js → kanban-definition-PNTS6WVX-NepmKEe2.js} +1 -1
- package/dist/assets/{linear-yC_29R6e.js → linear-bq1PTDTW.js} +1 -1
- package/dist/assets/{mermaid.core-VhEMkSND.js → mermaid.core-s4LTbQle.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-oJ7kyQZy.js → mindmap-definition-NLK3R4M7-LtdbgHFH.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-BmfYcY0O.js → pegDiagram-GJSIUBJH-tdbaCxiK.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-DKvgP7w9.js → pieDiagram-5QR66LMP-DvkbpW8l.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-BEKL3W0L.js → quadrantDiagram-O4NWA36T-Do4KaH-E.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-BEsiqR5s.js → railroadDiagram-XR7U4H2S-BQWULOAs.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-DWBcWvAP.js → requirementDiagram-PLB6GJNP-w9kei1FU.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-rpZ2_O4s.js → sankeyDiagram-IPEJSGJF-D2gRy0e8.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-B99HQdJR.js → sequenceDiagram-PO4LG4MO-a2sO5_Eo.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-BjN0qT5T.js → sizeCapture-INFHLROL-CiPJmFEB.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-BbsVgGsC.js → stateDiagram-v2-GCMORJYK-DAkUUlL7.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-DG3gKkeR.js → swimlanes-2SLR337P-DANf2R8Y.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-CdHxG8aM.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-BTHOdtF_.js → timeline-definition-EJHVYXUP-BKpQmzrT.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-acLSLFwc.js → usecaseDiagram-POWQR4AR-BtI7qLN-.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-C5JzNX9G.js → vennDiagram-UO4OBE2U-gXJXrAG_.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-DL1-PrR_.js → wardleyDiagram-VNRHLVJA-GU8Po5-n.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-Bvr1477p.js → xychartDiagram-PMCCYNJV-BF4J30g6.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/novnc-NOTICE.txt +118 -0
- package/dist-server/cloud-home-start.js +112 -14
- package/dist-server/companion/src/routes.js +24 -1
- package/dist-server/container-mcp.js +201 -16
- package/dist-server/drivers/agents-proxy.js +7 -4
- package/dist-server/enterprise/server/index.js +5 -1
- package/dist-server/index.js +10698 -4737
- package/dist-server/local-computer-proxy.js +133 -8
- package/dist-server/mcp-gate.js +16 -6
- package/dist-server/model-catalog/models-dev.snapshot.json +7299 -0
- package/dist-server/openmausbot.js +4656 -1258
- package/dist-server/pair-cli.js +4656 -1258
- package/dist-server/server/boat.js +5 -5
- package/dist-server/server/browser-engine.js +10 -1
- package/dist-server/server/browser-live.js +46 -2
- package/dist-server/server/browser-runtime.js +74 -8
- package/dist-server/server/browser-tool-shape.js +6 -4
- package/dist-server/server/chief-of-staff.js +1 -1
- package/dist-server/server/cloud-home-start.js +118 -26
- package/dist-server/server/cloud-home.js +39 -3
- package/dist-server/server/cloud-lending.js +74 -10
- package/dist-server/server/cloud-owner.js +196 -0
- package/dist-server/server/cloud-secrets-boot.js +6 -0
- package/dist-server/server/cloud-secrets.js +65 -0
- package/dist-server/server/computer-control.js +6 -0
- package/dist-server/server/config.js +187 -14
- package/dist-server/server/container-computer.js +46 -34
- package/dist-server/server/desktop-viewer-targets.js +26 -0
- package/dist-server/server/digest.js +12 -2
- package/dist-server/server/drivers/acp/core.js +339 -33
- package/dist-server/server/drivers/acp/cursor.js +13 -1
- package/dist-server/server/drivers/acp/opencode-go.js +614 -85
- package/dist-server/server/drivers/acp/quiet-status.js +257 -0
- package/dist-server/server/drivers/acp/qwen.js +18 -0
- package/dist-server/server/drivers/agents-catalog.js +6 -6
- package/dist-server/server/drivers/antigravity-acp.js +69 -11
- package/dist-server/server/drivers/antigravity-temp.js +360 -0
- package/dist-server/server/drivers/antigravity.js +21 -0
- package/dist-server/server/drivers/builtIn.js +2 -0
- package/dist-server/server/drivers/cerebras.js +108 -0
- package/dist-server/server/drivers/chat-mcp-tools.js +20 -3
- package/dist-server/server/drivers/chatgpt-plan-auth.js +481 -0
- package/dist-server/server/drivers/claude.js +329 -37
- package/dist-server/server/drivers/codex.js +228 -18
- package/dist-server/server/drivers/grok.js +2 -2
- package/dist-server/server/drivers/mistral.js +4 -3
- package/dist-server/server/drivers/openai-chat-protocol.js +4 -3
- package/dist-server/server/drivers/openai-chat.js +66 -4
- package/dist-server/server/drivers/openai-compat.js +73 -23
- package/dist-server/server/drivers/prompt-split.js +2 -0
- package/dist-server/server/engine-install.js +46 -12
- package/dist-server/server/env-path.js +21 -5
- package/dist-server/server/harness/registry.js +2 -2
- package/dist-server/server/index.js +1545 -238
- package/dist-server/server/lending-memory.js +479 -0
- package/dist-server/server/local-vm-idle.js +24 -6
- package/dist-server/server/local-vm-stop-reason.js +26 -0
- package/dist-server/server/managed-desktop.js +3 -0
- package/dist-server/server/mcp-bridge.js +5 -3
- package/dist-server/server/mcp-http.js +6 -3
- package/dist-server/server/mcp-oauth-discovery.js +133 -0
- package/dist-server/server/mcp-oauth-store.js +70 -0
- package/dist-server/server/mcp-oauth.js +550 -0
- package/dist-server/server/mcp-probe.js +14 -1
- package/dist-server/server/mcp-registry.js +43 -0
- package/dist-server/server/memory-upkeep.js +94 -84
- package/dist-server/server/model-catalog/catalog.js +374 -0
- package/dist-server/server/model-catalog/presets.js +176 -0
- package/dist-server/server/model-catalog/trim.js +219 -0
- package/dist-server/server/provider-key-check.js +5 -2
- package/dist-server/server/recent-work.js +1 -1
- package/dist-server/server/request-auth.js +23 -0
- package/dist-server/server/routes/antigravity-leftovers.js +45 -0
- package/dist-server/server/routes/bot-memory.js +31 -6
- package/dist-server/server/routes/desktop-viewer.js +238 -0
- package/dist-server/server/routine-requests.js +1 -1
- package/dist-server/server/routines.js +6 -3
- package/dist-server/server/sessions.js +56 -9
- package/dist-server/server/skills.js +25 -10
- package/dist-server/server/store.js +116 -122
- package/dist-server/server/thread-events.js +2 -0
- package/dist-server/server/thread-starters.js +33 -0
- package/dist-server/server/thread-title.js +46 -0
- package/dist-server/server/tts/fish.js +4 -5
- package/dist-server/server/tts/index.js +5 -3
- package/dist-server/server/vps-computer.js +80 -13
- package/dist-server/server/webhooks.js +18 -1
- package/dist-server/server/workspace-backup-policy.js +3 -0
- package/dist-server/server/workspace-backup.js +25 -0
- package/dist-server/server/workspace.js +66 -16
- package/dist-server/shared/approval-mode.js +1 -1
- package/dist-server/shared/redact.js +1 -0
- package/dist-server/shared/tool-name.js +22 -0
- package/dist-server/shared/workspace-backup-client.js +1 -0
- package/dist-server/src/lib/citations.js +177 -0
- package/dist-server/src/lib/composer-attachments.js +7 -1
- package/dist-server/vps-container-mcp.js +202 -16
- package/dist-server/workspace-backup.worker.js +87 -9
- package/enterprise/server/index.js +5 -1
- package/package.json +1 -1
- package/dist/assets/channel-DrfGMu7X.js +0 -1
- package/dist/assets/index-BlnCEcU6.js +0 -588
- package/dist/assets/index-DVxjOvID.css +0 -1
- 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,
|
|
13
|
-
// carries everything said in it, so one line (or
|
|
14
|
-
// a teammate bot or a local process, before or
|
|
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.
|
|
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({
|
|
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
|
-
|
|
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) {
|