openmausbot 0.1.91 → 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.
- package/dist/assets/DesktopViewer-Do6lBa4C.js +5 -0
- package/dist/assets/{abnfDiagram-O67JEVCF-yP-w8pRq.js → abnfDiagram-O67JEVCF-Cd3X7Hxc.js} +1 -1
- package/dist/assets/{arc-DsivfZEh.js → arc-DIYc7Ymx.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-CILw7lzT.js → architectureDiagram-NJMV4G6O-DBmRGVyP.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-DEQUV3HF.js → blockDiagram-BEXU5L5S-CPFePSEE.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-lSXCZoKA.js → c4Diagram-YGBWAQC7-DD_DO1qy.js} +1 -1
- package/dist/assets/channel-Dtk5Mtic.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-QCD5bgWP.js → chunk-2Q5K7J3B-BNEeNSjX.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-C5-iZJuL.js → chunk-3FUC2YCW-BbinLaqH.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-cxsawMpY.js → chunk-5VM5RSS4-BBgYrgDR.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-DV8tV8H7.js → chunk-GTNCS2PH-ezXj-kA-.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-CJFZyfbW.js → chunk-GWA4HPMP-9FxZ3diV.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-BigbMXco.js → chunk-JWPE2WC7-CO-40JpI.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-oVxTfpDy.js → chunk-NETBCI7D-BEhjslYE.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-CH4BiroW.js → chunk-WEXAMYUT-CNeq4n7_.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-KEDa6gax.js → chunk-XXDRQBXY-OsCY7KSi.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-BiPjfqVv.js → classDiagram-v2-NBCMYWYE-CloRUEtk.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-Caxew3OY.js → cose-bilkent-JH36ORCC-D1BGZhp4.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-B_Pui369.js → cynefin-OW5HDTMX-CUf4T2CN.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-X64ydIhP.js → cynefinDiagram-VND7K2PF-DPEZrFGA.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-TqYCI1E1.js → dagre-6A5THRUB-bDe6GIv5.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-TeOt3rAQ.js → diagram-22UHCM2B-CdcUQz55.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-D70BLhDA.js → diagram-3UASUU5V-BZpqGg7c.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-BbQDLwUC.js → diagram-ATOU4E4O-DvGM6ugp.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-ChNJZ1u-.js → diagram-CDSNMT55-DSTmMm0s.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-DMBl2APa.js → diagram-MLGK6HIB-15cucnMn.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-CEqDchOp.js → diagram-MPIPVDR6-tMbuaE6n.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-BVDKRp_-.js → ebnfDiagram-ZINNZB2B-BYxYzjHI.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-CRkofO9B.js → elk-276RUBZZ-v6bj852W.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-DruOMh5e.js → erDiagram-OPXOYQCR-5lBBlnd2.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-8GMQeB7t.js → flowDiagram-KWPJA3E3-CZyl1zIj.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-ORE9jcsM.js → ganttDiagram-FUAMR5RP-BiAI-ltZ.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-BmsedsWh.js → gitGraphDiagram-X574FWY7-BAZISkj3.js} +1 -1
- package/dist/assets/index-Cdmjrl28.css +1 -0
- package/dist/assets/{index-DKNEM8Q6.js → index-DGBaEXrM.js} +1 -1
- package/dist/assets/index-DR7YPMkW.js +597 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-E_p7AooJ.js → infoDiagram-VRGFBTTK-B6ooOy-Z.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-Y-B2mKda.js → ishikawaDiagram-OU5B5YK6-BZ9ksi-B.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-eA8RELcK.js → journeyDiagram-ZHPQQLJL-DWqlzIEe.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-DqIL2e8e.js → kanban-definition-PNTS6WVX-JX6piDwL.js} +1 -1
- package/dist/assets/{linear-yC_29R6e.js → linear-CmOWzX0P.js} +1 -1
- package/dist/assets/{mermaid.core-VhEMkSND.js → mermaid.core-CF1zwoQ-.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-oJ7kyQZy.js → mindmap-definition-NLK3R4M7-CGh2K4fW.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-BmfYcY0O.js → pegDiagram-GJSIUBJH-CDwYN0s7.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-DKvgP7w9.js → pieDiagram-5QR66LMP-1rfjJocB.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-BEKL3W0L.js → quadrantDiagram-O4NWA36T-tKPvPiok.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-BEsiqR5s.js → railroadDiagram-XR7U4H2S-BHLm5vXR.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-DWBcWvAP.js → requirementDiagram-PLB6GJNP-CB45STp7.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-rpZ2_O4s.js → sankeyDiagram-IPEJSGJF-55QA_huC.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-B99HQdJR.js → sequenceDiagram-PO4LG4MO-BckFmQHn.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-BjN0qT5T.js → sizeCapture-INFHLROL-BFCOJF11.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-BbsVgGsC.js → stateDiagram-v2-GCMORJYK-CZvmiVVN.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-DG3gKkeR.js → swimlanes-2SLR337P-BpDUCEKy.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-M0GYW9Y3.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-BTHOdtF_.js → timeline-definition-EJHVYXUP-BpxHq6IK.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-acLSLFwc.js → usecaseDiagram-POWQR4AR-Cj66vm02.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-C5JzNX9G.js → vennDiagram-UO4OBE2U-C9nRkWE_.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-DL1-PrR_.js → wardleyDiagram-VNRHLVJA-D-me_nWj.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-Bvr1477p.js → xychartDiagram-PMCCYNJV-Dc_fL4mk.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/container-mcp.js +195 -15
- package/dist-server/drivers/agents-proxy.js +5 -4
- package/dist-server/enterprise/server/index.js +5 -1
- package/dist-server/index.js +9300 -4155
- package/dist-server/local-computer-proxy.js +128 -7
- package/dist-server/mcp-gate.js +11 -5
- package/dist-server/model-catalog/models-dev.snapshot.json +7299 -0
- package/dist-server/openmausbot.js +3910 -1182
- package/dist-server/pair-cli.js +3910 -1182
- package/dist-server/server/boat.js +2 -2
- 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/config.js +174 -14
- package/dist-server/server/container-computer.js +3 -2
- package/dist-server/server/desktop-viewer-targets.js +26 -0
- package/dist-server/server/drivers/acp/core.js +167 -21
- package/dist-server/server/drivers/acp/opencode-go.js +614 -85
- 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/chatgpt-plan-auth.js +481 -0
- package/dist-server/server/drivers/claude.js +312 -34
- 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 +41 -3
- package/dist-server/server/drivers/openai-compat.js +73 -23
- package/dist-server/server/engine-install.js +46 -12
- package/dist-server/server/env-path.js +15 -3
- package/dist-server/server/harness/registry.js +2 -2
- package/dist-server/server/index.js +1338 -184
- package/dist-server/server/lending-memory.js +479 -0
- package/dist-server/server/local-vm-idle.js +24 -6
- 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 +4 -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 +194 -0
- package/dist-server/server/routine-requests.js +1 -1
- package/dist-server/server/routines.js +1 -1
- package/dist-server/server/sessions.js +56 -9
- package/dist-server/server/skills.js +25 -10
- package/dist-server/server/store.js +62 -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 +79 -12
- 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 +3 -0
- package/dist-server/server/workspace.js +65 -15
- 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 +196 -15
- package/dist-server/workspace-backup.worker.js +73 -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
|
@@ -388,7 +388,7 @@ async function waitReady(cfg, boxId, budgetMs = 90_000) {
|
|
|
388
388
|
const boatIdCache = new Map();
|
|
389
389
|
function boatInventoryProblem(status, body, included = false) {
|
|
390
390
|
if (status === 401 || status === 403) {
|
|
391
|
-
return included ? INCLUDED_BOAT_UNAVAILABLE : "boat.dev rejected the Boat API key — update it in Settings →
|
|
391
|
+
return included ? INCLUDED_BOAT_UNAVAILABLE : "boat.dev rejected the Boat API key — update it in Settings → API keys";
|
|
392
392
|
}
|
|
393
393
|
if (status === 429)
|
|
394
394
|
return "boat.dev is rate-limiting this account — wait a minute and refresh";
|
|
@@ -708,7 +708,7 @@ export async function inspectBoatIdentity(cfg, boxId) {
|
|
|
708
708
|
function inventoryFailure(inventory) {
|
|
709
709
|
const error = new Error(inventory.configured
|
|
710
710
|
? (inventory.problem ?? "Cloud computer inventory is unavailable")
|
|
711
|
-
: "Boat is not configured — add its API key in Settings →
|
|
711
|
+
: "Boat is not configured — add its API key in Settings → API keys");
|
|
712
712
|
error.status = inventory.configured ? 503 : 409;
|
|
713
713
|
return error;
|
|
714
714
|
}
|
|
@@ -442,8 +442,17 @@ export function agentBrowserIntegration(input) {
|
|
|
442
442
|
...(sourceEnv.USERPROFILE ? { USERPROFILE: sourceEnv.USERPROFILE } : {}),
|
|
443
443
|
})),
|
|
444
444
|
};
|
|
445
|
-
|
|
445
|
+
// A Linux host with no display cannot run headed Chrome, and OMB's managed
|
|
446
|
+
// browser is headless automation (docs/browser-packaging.md). Pin headless
|
|
447
|
+
// there even for a headed-capable caller. The engine's own flag is
|
|
448
|
+
// AGENT_BROWSER_HEADED; an explicit "0" pins the default so a daemon that
|
|
449
|
+
// kept a headed launch config still comes back headless instead of failing
|
|
450
|
+
// every launch until it is killed by hand (#1383).
|
|
451
|
+
const displayless = (input.platform ?? process.platform) === "linux" && !sourceEnv.DISPLAY && !sourceEnv.WAYLAND_DISPLAY;
|
|
452
|
+
if (input.headless !== false || displayless) {
|
|
446
453
|
env.AGENT_BROWSER_HEADLESS = "1";
|
|
454
|
+
env.AGENT_BROWSER_HEADED = "0";
|
|
455
|
+
}
|
|
447
456
|
if (input.attachCdpUrl)
|
|
448
457
|
env.AGENT_BROWSER_CDP = input.attachCdpUrl;
|
|
449
458
|
// MCP clients may filter the parent environment. Carry the configured
|
|
@@ -39,6 +39,19 @@ function displayUrl(value) {
|
|
|
39
39
|
return "";
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
+
/** Chrome's own launch-failure wording: the engine is installed and its
|
|
43
|
+
* daemon answers, but no browser can start (no display, an early exit).
|
|
44
|
+
* Distinct from a missing engine — the fix is a fresh session, not an
|
|
45
|
+
* install, so the generic "check the engine is installed" guidance would
|
|
46
|
+
* send the operator the wrong way (#1383). */
|
|
47
|
+
function browserLaunchFailure(error) {
|
|
48
|
+
const detail = [
|
|
49
|
+
error?.stderr,
|
|
50
|
+
error?.stdout,
|
|
51
|
+
error instanceof Error ? error.message : String(error),
|
|
52
|
+
].filter((part) => typeof part === "string").join("\n");
|
|
53
|
+
return /missing x server|\$DISPLAY|devtoolsactiveport|chrome exited|could not launch|failed to launch|browser launch failed/iu.test(detail);
|
|
54
|
+
}
|
|
42
55
|
/** Project only the viewer protocol. Never relay engine commands, results or console output. */
|
|
43
56
|
export function normalizeBrowserLiveMessage(value) {
|
|
44
57
|
const message = object(value);
|
|
@@ -150,6 +163,7 @@ export function parseBrowserLiveAction(value) {
|
|
|
150
163
|
export class BrowserLive {
|
|
151
164
|
runtime;
|
|
152
165
|
viewers = new Map();
|
|
166
|
+
sessionResets = new Map();
|
|
153
167
|
constructor({ runtime }) { this.runtime = runtime; }
|
|
154
168
|
current(viewer) {
|
|
155
169
|
try {
|
|
@@ -298,12 +312,37 @@ export class BrowserLive {
|
|
|
298
312
|
throw new Error("stale viewer");
|
|
299
313
|
const result = object(JSON.parse(stdout));
|
|
300
314
|
const data = object(result?.data);
|
|
301
|
-
if (result?.success !== true || !data)
|
|
302
|
-
|
|
315
|
+
if (result?.success !== true || !data) {
|
|
316
|
+
// Keep the engine's own error internal (logged, never sent) so a
|
|
317
|
+
// launch failure is recognized for what it is below.
|
|
318
|
+
const detail = [result?.error, result?.message].filter((part) => typeof part === "string").join(" ");
|
|
319
|
+
throw new Error(detail ? `browser command failed: ${detail}` : "browser command failed");
|
|
320
|
+
}
|
|
303
321
|
return data;
|
|
304
322
|
}
|
|
305
323
|
catch (error) {
|
|
306
324
|
console.warn("browser-live:", error);
|
|
325
|
+
if (browserLaunchFailure(error)) {
|
|
326
|
+
// The daemon is alive but holding a launch configuration Chrome
|
|
327
|
+
// cannot satisfy on this host (a headed launch with no display). It
|
|
328
|
+
// fails every command the same way until the daemon is killed —
|
|
329
|
+
// discard the session so the next connect starts a fresh one. One
|
|
330
|
+
// tracked reset per session: a reconnect or repeated command joins
|
|
331
|
+
// the in-flight teardown instead of spawning another (#1383).
|
|
332
|
+
if (!this.sessionResets.has(viewer.session)) {
|
|
333
|
+
const reset = closeBrowserSession(viewer.spec.command, env).then((closed) => {
|
|
334
|
+
if (closed)
|
|
335
|
+
console.warn(`browser-live: reset ${viewer.session} after a failed browser launch; reconnecting starts a fresh session`);
|
|
336
|
+
else
|
|
337
|
+
console.warn(`browser-live: reset of ${viewer.session} after a failed browser launch did not finish; the next command retries it`);
|
|
338
|
+
return closed;
|
|
339
|
+
}, () => false);
|
|
340
|
+
this.sessionResets.set(viewer.session, reset);
|
|
341
|
+
void reset.finally(() => { if (this.sessionResets.get(viewer.session) === reset)
|
|
342
|
+
this.sessionResets.delete(viewer.session); });
|
|
343
|
+
}
|
|
344
|
+
throw new BrowserLiveError("The browser could not start on this server. Reconnect to retry with a fresh browser session.", 503);
|
|
345
|
+
}
|
|
307
346
|
throw new BrowserLiveError("The browser could not complete this action. Check that the browser engine is installed, then reconnect.", 503);
|
|
308
347
|
}
|
|
309
348
|
}
|
|
@@ -380,6 +419,11 @@ export class BrowserLive {
|
|
|
380
419
|
options.res.once("close", () => this.close(viewer));
|
|
381
420
|
options.res.once("error", () => this.close(viewer));
|
|
382
421
|
try {
|
|
422
|
+
// A failed launch already started this session's daemon teardown; wait
|
|
423
|
+
// for it so the status probe meets a fresh daemon, not the stuck one.
|
|
424
|
+
const resetting = this.sessionResets.get(viewer.session);
|
|
425
|
+
if (resetting)
|
|
426
|
+
await resetting;
|
|
383
427
|
let status = await this.command(viewer, ["stream", "status"]);
|
|
384
428
|
if (status.enabled !== true) {
|
|
385
429
|
try {
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { killCliTree, spawnCli } from "./procs.js";
|
|
2
2
|
import { DEFAULT_BROWSER_RESULT_BUDGET, shapeBrowserToolResult, slimBrowserToolList, stripHarnessOwnedArguments } from "./browser-tool-shape.js";
|
|
3
3
|
export const BROWSER_CONTROL_REFUSAL = "Browser tools are paused while a person controls this browser. Wait for them to hand control back; do not try another browser or execution tool.";
|
|
4
|
+
/** Harness-owned recovery tool, advertised beside the engine's own tools. A
|
|
5
|
+
* phone or chat-only user has no Browser panel Restart button; without this
|
|
6
|
+
* an interrupted action refuses the bot's browser until the app restarts. */
|
|
7
|
+
export const BROWSER_RESTART_TOOL = "restart_browser";
|
|
8
|
+
const BROWSER_RESTART_TOOL_SPEC = {
|
|
9
|
+
name: BROWSER_RESTART_TOOL,
|
|
10
|
+
description: "Close and restart your browser. Use this only when a browser tool says a browser action was interrupted and the browser must be restarted. Open pages are closed; afterwards open the page you need again and check whether the interrupted action already happened before repeating it.",
|
|
11
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
12
|
+
};
|
|
13
|
+
const BROWSER_INTERRUPTED = `A browser action was interrupted. Restart this browser with the ${BROWSER_RESTART_TOOL} tool before continuing.`;
|
|
4
14
|
const MAX_REQUEST_BYTES = 1_048_576;
|
|
5
15
|
const MAX_RESPONSE_BYTES = 16_777_216;
|
|
6
16
|
/** Startup, not per-request work: a cold engine spawn can exceed a tight
|
|
@@ -200,10 +210,15 @@ class BrowserClient {
|
|
|
200
210
|
export class BrowserRuntime {
|
|
201
211
|
gates = new Map();
|
|
202
212
|
clients = new Map();
|
|
213
|
+
/** Last advertised tools per session, so a turn that starts while the
|
|
214
|
+
* browser is uncertain still sees the tools it can use after recovering. */
|
|
215
|
+
toolLists = new Map();
|
|
203
216
|
options;
|
|
204
|
-
|
|
217
|
+
closeBrowser;
|
|
218
|
+
constructor({ closeBrowser, ...options } = {}) {
|
|
205
219
|
const budget = Number(process.env.OMB_BROWSER_RESULT_BUDGET);
|
|
206
220
|
this.options = { requestTimeoutMs: 120_000, takeoverTimeoutMs: 15_000, idleMs: 60_000, maxPending: 16, resultBudget: Number.isFinite(budget) && budget > 0 ? budget : DEFAULT_BROWSER_RESULT_BUDGET, ...options };
|
|
221
|
+
this.closeBrowser = closeBrowser ?? (async () => false);
|
|
207
222
|
}
|
|
208
223
|
gate(session) {
|
|
209
224
|
let gate = this.gates.get(session);
|
|
@@ -227,7 +242,7 @@ export class BrowserRuntime {
|
|
|
227
242
|
if (gate.owner !== null)
|
|
228
243
|
throw new Error(BROWSER_CONTROL_REFUSAL);
|
|
229
244
|
if (gate.uncertain)
|
|
230
|
-
throw new Error(
|
|
245
|
+
throw new Error(BROWSER_INTERRUPTED);
|
|
231
246
|
if (gate.closing)
|
|
232
247
|
throw new Error("The browser is closing. Try again shortly.");
|
|
233
248
|
gate.agents++;
|
|
@@ -250,10 +265,21 @@ export class BrowserRuntime {
|
|
|
250
265
|
// must refuse the closing window itself or its client outlives restart().
|
|
251
266
|
if (method !== "tools/call" && this.gate(session).closing)
|
|
252
267
|
throw new Error("The browser is closing. Try again shortly.");
|
|
268
|
+
if (method === "tools/call" && params && typeof params === "object" && params.name === BROWSER_RESTART_TOOL) {
|
|
269
|
+
beforeDispatch?.();
|
|
270
|
+
await this.agentRestart(session, () => this.closeBrowser(session, spec));
|
|
271
|
+
beforeDispatch?.(); // A turn revoked while the browser closed receives no result.
|
|
272
|
+
return { content: [{ type: "text", text: "Browser restarted. Open pages were closed; open the page you need again and check whether the interrupted action already happened before repeating it." }] };
|
|
273
|
+
}
|
|
253
274
|
// Uncertainty survives client replacement and never self-resolves: refuse
|
|
254
|
-
// every new browser request until an explicit restart clears it.
|
|
255
|
-
|
|
256
|
-
|
|
275
|
+
// every new browser request until an explicit restart clears it. Listing
|
|
276
|
+
// never reaches the engine; it answers from the last list so the restart
|
|
277
|
+
// tool is reachable and the engine's tools are there once it succeeds.
|
|
278
|
+
if (this.gate(session).uncertain) {
|
|
279
|
+
if (method === "tools/list")
|
|
280
|
+
return withRestartTool(this.toolLists.get(session) ?? { tools: [] });
|
|
281
|
+
throw new Error(BROWSER_INTERRUPTED);
|
|
282
|
+
}
|
|
257
283
|
const invoke = async () => {
|
|
258
284
|
const key = JSON.stringify([spec.command, spec.args, Object.entries(spec.env).sort(([a], [b]) => a.localeCompare(b))]);
|
|
259
285
|
let entry = this.clients.get(session);
|
|
@@ -281,8 +307,11 @@ export class BrowserRuntime {
|
|
|
281
307
|
const request = method === "tools/call" ? stripHarnessOwnedArguments(params) : params;
|
|
282
308
|
let result = await entry.client.rpc(method, request);
|
|
283
309
|
beforeDispatch?.(); // A turn revoked while the tool ran receives no result.
|
|
284
|
-
if (method === "tools/list")
|
|
285
|
-
|
|
310
|
+
if (method === "tools/list") {
|
|
311
|
+
const tools = withRestartTool(slimBrowserToolList(result));
|
|
312
|
+
this.toolLists.set(session, tools);
|
|
313
|
+
return tools;
|
|
314
|
+
}
|
|
286
315
|
const toolName = request && typeof request === "object" && typeof request.name === "string" ? request.name : undefined;
|
|
287
316
|
if (toolName === "agent_browser_open" && result && typeof result === "object" &&
|
|
288
317
|
result.isError !== true) {
|
|
@@ -448,6 +477,35 @@ export class BrowserRuntime {
|
|
|
448
477
|
this.changed(gate);
|
|
449
478
|
}
|
|
450
479
|
}
|
|
480
|
+
/** The agent's own recovery, for a turn with no Browser panel to press
|
|
481
|
+
* Restart in. The same barrier as restart() — the native browser must
|
|
482
|
+
* close — but it never takes a person's hold, and a failed close leaves no
|
|
483
|
+
* owner behind that would lock a person out of the panel. */
|
|
484
|
+
async agentRestart(session, closeBrowser) {
|
|
485
|
+
const gate = this.gate(session);
|
|
486
|
+
if (gate.owner !== null)
|
|
487
|
+
throw new Error(BROWSER_CONTROL_REFUSAL);
|
|
488
|
+
if (gate.closing || gate.releasing || gate.agents || gate.humans)
|
|
489
|
+
throw new Error("The browser is busy. Wait for current work to finish before restarting.");
|
|
490
|
+
gate.ready = false;
|
|
491
|
+
gate.closing = true;
|
|
492
|
+
this.changed(gate);
|
|
493
|
+
try {
|
|
494
|
+
if (!await closeBrowser())
|
|
495
|
+
throw new Error("The browser could not be closed. Ask the person to press Restart in the Browser panel of OpenMausBot on their computer, or to restart OpenMausBot.");
|
|
496
|
+
await this.clients.get(session)?.client.stop();
|
|
497
|
+
await this.clients.get(session)?.client.stop(); // see restart()
|
|
498
|
+
gate.uncertain = false;
|
|
499
|
+
}
|
|
500
|
+
catch (error) {
|
|
501
|
+
gate.uncertain = true;
|
|
502
|
+
throw error;
|
|
503
|
+
}
|
|
504
|
+
finally {
|
|
505
|
+
gate.closing = false;
|
|
506
|
+
this.changed(gate);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
451
509
|
/** Call after the underlying browser is closed when recovering an uncertain
|
|
452
510
|
* action. This closes the MCP transport, not saved logins or profile files. */
|
|
453
511
|
async close(session) {
|
|
@@ -460,10 +518,18 @@ export class BrowserRuntime {
|
|
|
460
518
|
await this.clients.get(session)?.client.stop();
|
|
461
519
|
gate.closing = false;
|
|
462
520
|
this.changed(gate);
|
|
463
|
-
if (!gate.owner && !gate.agents && !gate.humans)
|
|
521
|
+
if (!gate.owner && !gate.agents && !gate.humans) {
|
|
464
522
|
this.gates.delete(session);
|
|
523
|
+
this.toolLists.delete(session);
|
|
524
|
+
}
|
|
465
525
|
}
|
|
466
526
|
async closeAll() {
|
|
467
527
|
await Promise.all([...new Set([...this.clients.keys(), ...this.gates.keys()])].map((session) => this.close(session)));
|
|
468
528
|
}
|
|
469
529
|
}
|
|
530
|
+
function withRestartTool(result) {
|
|
531
|
+
if (!result || typeof result !== "object" || !Array.isArray(result.tools))
|
|
532
|
+
return result;
|
|
533
|
+
const tools = (result.tools).filter((tool) => tool?.name !== BROWSER_RESTART_TOOL);
|
|
534
|
+
return { ...result, tools: [...tools, BROWSER_RESTART_TOOL_SPEC] };
|
|
535
|
+
}
|
|
@@ -12,11 +12,13 @@
|
|
|
12
12
|
import { trimResultText } from "./mcp-trim.js";
|
|
13
13
|
/** Launch, session and network settings OpenMausBot owns through the
|
|
14
14
|
* environment (see browser-engine.ts). A model has no business setting them
|
|
15
|
-
* per call — `session` would reach another bot's browser, `extraArgs
|
|
16
|
-
* `caCert` change the launch — and each one cost more schema
|
|
17
|
-
* own description.
|
|
15
|
+
* per call — `session` would reach another bot's browser, `extraArgs`,
|
|
16
|
+
* `caCert` and `headed` change the launch — and each one cost more schema
|
|
17
|
+
* than the tool's own description. `headed` in particular overrides
|
|
18
|
+
* AGENT_BROWSER_HEADED and the managed config, so a single call can pin a
|
|
19
|
+
* daemon to a launch mode this host cannot satisfy (#1383). */
|
|
18
20
|
export const HARNESS_OWNED_BROWSER_PARAMS = new Set([
|
|
19
|
-
"allowedDomains", "caCert", "clearCaCert", "extraArgs", "idleTimeout", "namespace",
|
|
21
|
+
"allowedDomains", "caCert", "clearCaCert", "extraArgs", "headed", "idleTimeout", "namespace",
|
|
20
22
|
"restore", "restoreCheckFn", "restoreCheckText", "restoreCheckUrl", "restoreSave", "session", "timeoutMs",
|
|
21
23
|
]);
|
|
22
24
|
/** Characters of one browser result that may enter the model's context.
|
|
@@ -29,7 +29,7 @@ export function chiefOfStaffSystemPrompt(chiefId, bots, canDelegate, trustedOpen
|
|
|
29
29
|
});
|
|
30
30
|
const delegation = canDelegate
|
|
31
31
|
? boundedCoordination
|
|
32
|
-
? "Use list_bots or list_room_targets for the live reachable roster. Use coordinate_bots to ask actual teammates for advice or assign concrete work. Outside a room,
|
|
32
|
+
? "Use list_bots or list_room_targets for the live reachable roster. Use coordinate_bots to ask actual teammates for advice or assign concrete work. Outside a room, each distinct assignment starts a fresh thread for that teammate, using their own model and permissions. Include the context they need in each brief. Busy teammates queue. Give self-contained briefs, then end your turn; you resume automatically after their results return. Leads can coordinate their own specialists. Do not poll, send acknowledgements as new work, or substitute native helpers for named bots. On return, verify the requested outcome, resolve decisions within the user's scope, request concrete corrections with rework=true when necessary, and return one consolidated answer. Consultations are advice, not proof that work or tests ran. A refusal from coordinate_bots means nothing was sent: fix what it names (usually the id — copy it from list_bots or your roster; a unique teammate name also works) and retry, and never describe a handoff the tool did not accept."
|
|
33
33
|
: [
|
|
34
34
|
"Use list_bots to confirm the live roster and IDs. When assigning work to a teammate, use delegate_bot: it returns immediately, keeps you available to the user, and delivers the teammate's outcome back into this conversation automatically — success or failure. When the result arrives you are woken with it: report it to the user and act. If the teammate fails or stalls, tell the user plainly and decide the next step yourself.",
|
|
35
35
|
"After delegate_bot accepts the task, acknowledge the handoff and continue with any independent work or end your turn. Do not call wait_delegation or repeatedly poll check_delegation in the same turn.",
|
|
@@ -1,19 +1,33 @@
|
|
|
1
1
|
// Entry point of the OMB Cloud Pro home image (deploy/fly/Dockerfile).
|
|
2
2
|
//
|
|
3
|
-
// Starts as root
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// webhook receiver on :8800) and the Caddy edge on 0.0.0.0:8080.
|
|
7
|
-
// the only listener the network can reach, and it always
|
|
8
|
-
// X-Forwarded-*, so request-auth.ts never grants a remote
|
|
9
|
-
// trust. If either child exits, both stop and the machine
|
|
10
|
-
// exception is the server asking to be started again
|
|
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).
|
|
11
23
|
import { spawn } from "node:child_process";
|
|
12
24
|
import { chownSync, readFileSync, statSync } from "node:fs";
|
|
13
25
|
import { dirname, join, posix, resolve } from "node:path";
|
|
14
26
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
15
|
-
import { CLOUD_HOME_RESTART_EXIT_CODE, 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";
|
|
16
28
|
const SERVICE_USER = "maus";
|
|
29
|
+
/** The descriptor the server reads its secrets from. */
|
|
30
|
+
const SECRETS_FD = 3;
|
|
17
31
|
/** uid/gid from /etc/passwd; Node has no getpwnam. */
|
|
18
32
|
export function passwdIds(passwd, name) {
|
|
19
33
|
for (const line of passwd.split("\n")) {
|
|
@@ -23,22 +37,80 @@ export function passwdIds(passwd, name) {
|
|
|
23
37
|
}
|
|
24
38
|
return null;
|
|
25
39
|
}
|
|
26
|
-
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
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. */
|
|
30
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();
|
|
31
63
|
const server = {
|
|
32
|
-
...
|
|
64
|
+
...allowed, HOME: home, OMB_DATA_DIR: env.OMB_DATA_DIR || posix.join(home, ".openmausbot"),
|
|
33
65
|
OMB_PORT: "8799", OMB_WEBHOOK_PORT: "8800", OMB_PUBLIC_URL: config.publicOrigin,
|
|
34
66
|
OMB_WEBHOOK_PUBLIC_URL: env.OMB_WEBHOOK_PUBLIC_URL || config.publicOrigin,
|
|
67
|
+
[CLOUD_SECRETS_FD_ENV]: String(SECRETS_FD),
|
|
35
68
|
};
|
|
36
69
|
const edge = {
|
|
37
70
|
PATH: env.PATH ?? "/usr/local/bin:/usr/bin:/bin", HOME: "/tmp/omb-edge",
|
|
38
71
|
XDG_DATA_HOME: "/tmp/omb-edge/data", XDG_CONFIG_HOME: "/tmp/omb-edge/config",
|
|
39
72
|
OMB_CLOUD_PUBLIC_HOST: cloudHomeHost(config),
|
|
40
73
|
};
|
|
41
|
-
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;
|
|
42
114
|
}
|
|
43
115
|
/** What the launcher does when the server child exits: start it again only
|
|
44
116
|
* when it asked to (a committed restore), and only a few times in a row. */
|
|
@@ -54,8 +126,11 @@ export function startCloudHome(env = process.env) {
|
|
|
54
126
|
for (const warning of config.warnings)
|
|
55
127
|
console.warn(`cloud home: ${warning}`);
|
|
56
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;
|
|
57
132
|
if (process.getuid?.() === 0) {
|
|
58
|
-
|
|
133
|
+
ids = passwdIds(readFileSync("/etc/passwd", "utf8"), SERVICE_USER);
|
|
59
134
|
if (!ids)
|
|
60
135
|
throw new Error(`The ${SERVICE_USER} user is missing from this image.`);
|
|
61
136
|
// A new volume is a root-owned mount point. Only the mount point itself
|
|
@@ -64,14 +139,32 @@ export function startCloudHome(env = process.env) {
|
|
|
64
139
|
if (stat.uid !== ids.uid || stat.gid !== ids.gid)
|
|
65
140
|
chownSync(home, ids.uid, ids.gid);
|
|
66
141
|
process.setgroups?.([]);
|
|
67
|
-
|
|
68
|
-
process.
|
|
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);
|
|
69
155
|
}
|
|
70
|
-
if (process.getuid?.() === 0)
|
|
71
|
-
throw new Error("The Cloud home must run as an unprivileged user.");
|
|
72
|
-
prepareCloudHomeVolume(home, config.machineId);
|
|
73
156
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
74
|
-
const
|
|
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(", ")}`);
|
|
75
168
|
const children = [];
|
|
76
169
|
let stopping = false;
|
|
77
170
|
const stop = (failed) => {
|
|
@@ -87,8 +180,7 @@ export function startCloudHome(env = process.env) {
|
|
|
87
180
|
child.kill("SIGKILL"); }, 20_000);
|
|
88
181
|
force.unref();
|
|
89
182
|
};
|
|
90
|
-
const
|
|
91
|
-
const child = spawn(command, args, { env: childEnv, stdio: "inherit" });
|
|
183
|
+
const watch = (child, again) => {
|
|
92
184
|
children.push(child);
|
|
93
185
|
child.once("error", () => stop(true));
|
|
94
186
|
child.once("exit", (code) => {
|
|
@@ -98,7 +190,7 @@ export function startCloudHome(env = process.env) {
|
|
|
98
190
|
});
|
|
99
191
|
};
|
|
100
192
|
let restarts = 0;
|
|
101
|
-
const runServer = () =>
|
|
193
|
+
const runServer = () => watch(spawnWithSecrets(process.execPath, [join(here, "index.js")], server, secrets, ids), (code) => {
|
|
102
194
|
if (serverExitAction(code, stopping, restarts) !== "restart")
|
|
103
195
|
return false;
|
|
104
196
|
restarts++;
|
|
@@ -106,7 +198,7 @@ export function startCloudHome(env = process.env) {
|
|
|
106
198
|
return true;
|
|
107
199
|
});
|
|
108
200
|
runServer();
|
|
109
|
-
|
|
201
|
+
watch(spawn(edgeBin, ["run", "--config", edgeConfig, "--adapter", "caddyfile"], { env: edge, stdio: "inherit", ...(ids ? { uid: ids.uid, gid: ids.gid } : {}) }));
|
|
110
202
|
process.once("SIGTERM", () => stop(false));
|
|
111
203
|
process.once("SIGINT", () => stop(false));
|
|
112
204
|
}
|
|
@@ -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,6 +36,10 @@ 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. */
|
|
@@ -89,6 +96,9 @@ export function cloudHomeConfiguration(env = process.env) {
|
|
|
89
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`);
|
|
90
97
|
return { machineId, adminOrigin, publicOrigin, bootstrapSecret, warnings };
|
|
91
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";
|
|
92
102
|
/** The environment without a platform gateway's settings (CLOUD_IGNORED_KEYS). */
|
|
93
103
|
export function withoutIgnoredCloudKeys(env) {
|
|
94
104
|
const kept = { ...env };
|
|
@@ -115,6 +125,18 @@ export function cloudHomePlaceRefusal(place) {
|
|
|
115
125
|
export function boatNotConfiguredMessage(cloudHome) {
|
|
116
126
|
return `Cloud Boat is not configured — add a Boat API key or choose ${cloudHome ? "Browser" : "Local VM"}`;
|
|
117
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
|
+
}
|
|
118
140
|
/** The Admin's side of the signature (openmaus-cloud cloudPairingSignature). */
|
|
119
141
|
export function cloudPairingSignature(secret, timestamp, nonce, body) {
|
|
120
142
|
const bodyHash = createHash("sha256").update(body).digest("base64url");
|
|
@@ -164,18 +186,32 @@ export function createCloudPairing(options) {
|
|
|
164
186
|
}
|
|
165
187
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
166
188
|
return { status: 400, body: { error: "invalid_body" } };
|
|
167
|
-
const { label, ttlSeconds } = parsed;
|
|
189
|
+
const { label, ttlSeconds, purpose, owner } = parsed;
|
|
168
190
|
// oxlint-disable-next-line no-control-regex
|
|
169
191
|
if (label !== undefined && (typeof label !== "string" || label.length > 80 || /[\x00-\x1f\x7f]/.test(label)))
|
|
170
192
|
return { status: 400, body: { error: "invalid_label" } };
|
|
171
193
|
if (ttlSeconds !== undefined && (!Number.isSafeInteger(ttlSeconds) || ttlSeconds < 1))
|
|
172
194
|
return { status: 400, body: { error: "invalid_ttl" } };
|
|
173
|
-
|
|
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);
|
|
174
204
|
const opened = sessions.openPairing({
|
|
175
205
|
scopes: ["admin", "client"],
|
|
176
206
|
label: typeof label === "string" && label.trim() ? label.trim() : "OMB Cloud",
|
|
177
207
|
ttlMs: ttl * 1000,
|
|
208
|
+
browser,
|
|
209
|
+
...(browser ? { owner: owner } : {}),
|
|
178
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" } };
|
|
179
215
|
return { status: 200, body: { code: formatPairingCode(opened.code), credential: opened.credential, expiresAt: opened.expiresAt } };
|
|
180
216
|
},
|
|
181
217
|
};
|