openmausbot 0.1.63 → 0.1.66
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/index-BftlaHAF.css +1 -0
- package/dist/assets/index-DuR3H-Hf.js +278 -0
- package/dist/assets/{index-C-SoIGdn.js → index-kW5L1QeO.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/browser-proxy.js +121 -0
- package/dist-server/container-mcp.js +4 -1
- package/dist-server/index.js +17996 -15876
- package/dist-server/openmausbot.js +752 -245
- package/dist-server/pair-cli.js +752 -245
- package/dist-server/prepare-cloudflared.js +10 -2
- package/dist-server/proxy-paths.js +1 -0
- package/dist-server/server/attachments.js +140 -16
- package/dist-server/server/browser-engine-release.js +10 -8
- package/dist-server/server/browser-engine.js +226 -26
- package/dist-server/server/browser-live.js +567 -0
- package/dist-server/server/browser-proxy.js +129 -0
- package/dist-server/server/browser-runtime.js +370 -0
- package/dist-server/server/claude-accounts.js +103 -0
- package/dist-server/server/config.js +31 -20
- package/dist-server/server/custom-domain.js +196 -0
- package/dist-server/server/drivers/claude.js +77 -19
- package/dist-server/server/drivers/codex-device-auth.js +234 -0
- package/dist-server/server/drivers/codex.js +10 -0
- package/dist-server/server/harness/bus.js +9 -4
- package/dist-server/server/harness/registry.js +15 -0
- package/dist-server/server/index.js +446 -91
- package/dist-server/server/provider-auth-sessions.js +118 -0
- package/dist-server/server/proxy-paths.js +1 -0
- package/dist-server/server/sse-fanout.js +53 -0
- package/dist-server/src/lib/composer-attachments.js +23 -6
- package/dist-server/vps-container-mcp.js +4 -1
- package/package.json +1 -1
- package/dist/assets/index-BuntoOSX.js +0 -264
- package/dist/assets/index-oPMv6a_k.css +0 -1
|
@@ -14,11 +14,12 @@ import { CREDENTIAL_TARGETS, credentialResumeOutcome, credentialIsConfigured, is
|
|
|
14
14
|
import { HELD_NOTE, approvalHeldNote, approvalHeldReason, approvalModeForOrigin, autoVerdict, rememberableApprovalKey } from "./auto-approve.js";
|
|
15
15
|
import { requestReview, resolveAutoReviewMode, shouldReview } from "./auto-review.js";
|
|
16
16
|
import { updateClaudeCli } from "./claude-update.js";
|
|
17
|
+
import { configuredAccountDirectory, assertSeparateClaudeAccount, claudeAccountInfo, createClaudeAccountSchema, instanceSettingsSchema, newClaudeAccount } from "./claude-accounts.js";
|
|
17
18
|
import { BrowserCleanupCoordinator, finalizeBrowserCleanupMutation, requireBrowserCleanupAcknowledged, } from "./browser-lifecycle-cleanup.js";
|
|
18
19
|
import * as checkpoints from "./checkpoints.js";
|
|
19
20
|
import { appendDecision, readDecisions } from "./decision-log.js";
|
|
20
21
|
import { validateBotCwd } from "./bot-cwd.js";
|
|
21
|
-
import { ATTACHMENTS_DIR, attachmentExists, extensionForMime, FILE_MAX_BYTES, IMAGE_MAX_BYTES, readAttachment, saveFile, saveImage, saveImageUpload, validateAttachmentUploadId, } from "./attachments.js";
|
|
22
|
+
import { ATTACHMENTS_DIR, attachmentExists, cleanupStaleAttachmentPartials, deleteAttachment, extensionForMime, FILE_MAX_BYTES, IMAGE_MAX_BYTES, readAttachment, saveFile, saveImage, saveImageUpload, validateAttachmentUploadId, } from "./attachments.js";
|
|
22
23
|
import { messageFileDisposition, messageFileDownloadName, messageAttachmentName, messageFileRoots, messageImageTargetAt, messageReferencesFile, openMessageFile, } from "./message-file.js";
|
|
23
24
|
import { avatarGenerationRequestSchema, avatarGenerationStateMatches, generateAvatarImage, snapshotAvatarGenerationState, } from "./avatar-image.js";
|
|
24
25
|
import { fitsOnOneLine, parseBotProfilePatch } from "./bot-profile.js";
|
|
@@ -32,7 +33,7 @@ import { chiefOfStaffSystemPrompt } from "./chief-of-staff.js";
|
|
|
32
33
|
import { peerAllowed, peerName, peerRosterSystemPrompt, reachablePeers, roomPeerRosterSystemPrompt, roomRosterLine } from "./peer-roster.js";
|
|
33
34
|
import { openMausStatusSystemPrompt } from "./openmaus-status-capsule.js";
|
|
34
35
|
import { containerComputerAction, containerComputerExists, containerComputerFrame, containerComputerMcp, containerComputerScreenshot, containerComputerStatus, containerRuntimeStatus, localVmRecreatableOnDemand, perBotLocalVmTarget, SHARED_LOCAL_VM_TARGET, setupCommands, } from "./container-computer.js";
|
|
35
|
-
import { ensureDirs, instanceConfigs, loadConfig, localVmMaxInstances, localVmMode, parseConfigPatch, roomTurnTimeoutMinutes, saveConfig, showToolCallsEnabled, skillRecorderEnabled, builtInBrowserEnabled, browserProfileReplacementConflict, browserProfilePartitionTarget, syncCredentialEnv, withInstanceCli, vpsSshAlias, DATA_DIR, EVENTS_DIR, NATIVE_DIR, customMcpServers, } from "./config.js";
|
|
36
|
+
import { ensureDirs, instanceConfigs, loadConfig, localVmMaxInstances, localVmMode, parseConfigPatch, roomTurnTimeoutMinutes, saveConfig, showToolCallsEnabled, skillRecorderEnabled, builtInBrowserEnabled, browserProfileReplacementConflict, browserProfilePartitionTarget, syncCredentialEnv, withInstanceCli, persistableInstanceConfigs, vpsSshAlias, DATA_DIR, EVENTS_DIR, NATIVE_DIR, customMcpServers, } from "./config.js";
|
|
36
37
|
import { ComputerControl } from "./computer-control.js";
|
|
37
38
|
import { MAX_REMOTE_COMMAND_LENGTH } from "./remote-computer.js";
|
|
38
39
|
import { augmentedPath, findCliCandidates, resetPathCache } from "./env-path.js";
|
|
@@ -87,7 +88,9 @@ import * as vps from "./vps-computer.js";
|
|
|
87
88
|
import { RoutineManager } from "./routines.js";
|
|
88
89
|
import { CalendarCallManager } from "./calendar-calls.js";
|
|
89
90
|
import { BUILT_IN_BROWSER_SYSTEM_PROMPT } from "./browser-engine.js";
|
|
90
|
-
import {
|
|
91
|
+
import { BrowserRuntime } from "./browser-runtime.js";
|
|
92
|
+
import { BrowserLive } from "./browser-live.js";
|
|
93
|
+
import { agentBrowserFrame, agentBrowserIntegration, browserEngineEncryptionKey, prepareBrowserSessionState, clearBrowserSessionState, ensureChrome, installAgentBrowserBinary, resolveAgentBrowserBinary, browserEngineStatus, browserSessionId, describeBrowserEngine, } from "./browser-engine.js";
|
|
91
94
|
import { createScreenFrameSource } from "./screen-frame-source.js";
|
|
92
95
|
import { screenFrameHash, screenSurfaceForTool, screenTouchingTool, settledFrameIsNews } from "./screen-frame-gate.js";
|
|
93
96
|
import { RoutineRequestService } from "./routine-requests.js";
|
|
@@ -117,9 +120,12 @@ import { createGracefulShutdown } from "./graceful-shutdown.js";
|
|
|
117
120
|
import { acquireDataDirLeaseForProcess } from "./data-dir-lease.js";
|
|
118
121
|
import { describeEdition, editionStatus, loadEnterpriseLayer } from "./enterprise.js";
|
|
119
122
|
import { environmentDescriptor, loadEnvironmentId } from "./environment.js";
|
|
123
|
+
import { createCustomDomainVerifier, customDomainIpv4, normalizeCustomDomain } from "./custom-domain.js";
|
|
124
|
+
import { ProviderAuthSessions } from "./provider-auth-sessions.js";
|
|
120
125
|
import { clearSessionCookie, clientBotPatchViolation, clientGroupPatchViolation, labelFromUserAgent, requestOrigin, requestSource, resolveRequestAuth, parseCookies, serializeSessionCookie, sessionCookieName, } from "./request-auth.js";
|
|
121
126
|
import { cookieMaxAgeSeconds, formatPairingCode, SessionRegistry } from "./sessions.js";
|
|
122
127
|
import { describeBrand, loadBrand } from "./brand.js";
|
|
128
|
+
import { deliverSseFrame } from "./sse-fanout.js";
|
|
123
129
|
import { PHONE_SECRET_PROTOCOL_VERSION, PhoneSecretBridge, PhoneSecretError, PhoneSecretSubmissionRegistry, assertPhoneSecretRequestMatches, phoneSecretOperationId, } from "./phone-secret.js";
|
|
124
130
|
const PORT = Number(process.env.OMB_PORT || process.env.OGB_PORT || 8799);
|
|
125
131
|
const WEBHOOK_PORT = Number(process.env.OMB_WEBHOOK_PORT || PORT + 1);
|
|
@@ -169,9 +175,30 @@ const DESKTOP_MANAGED = process.env.OMB_DESKTOP_PARENT === "1";
|
|
|
169
175
|
let desktopMutationToken = DESKTOP_MANAGED ? "" : undefined;
|
|
170
176
|
let companionMutationToken = DESKTOP_MANAGED ? "" : undefined;
|
|
171
177
|
// Where remote clients reach this server (a proxy's public address); pairing URLs use it.
|
|
172
|
-
const
|
|
178
|
+
const FALLBACK_PUBLIC_URL = process.env.OMB_PUBLIC_URL?.trim().replace(/\/+$/, "") || null;
|
|
173
179
|
const cfg = loadConfig();
|
|
180
|
+
const customDomainVerifier = createCustomDomainVerifier({ environmentId: ENVIRONMENT_ID });
|
|
181
|
+
let customDomainRevision = 0;
|
|
182
|
+
function savedCustomDomain() {
|
|
183
|
+
if (DESKTOP_MANAGED || !cfg.customDomain)
|
|
184
|
+
return null;
|
|
185
|
+
try {
|
|
186
|
+
return normalizeCustomDomain(cfg.customDomain);
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function publicUrl() { return savedCustomDomain() ?? FALLBACK_PUBLIC_URL; }
|
|
193
|
+
function customDomainStatus() {
|
|
194
|
+
return {
|
|
195
|
+
customDomain: savedCustomDomain(), publicUrl: publicUrl(), fallbackUrl: FALLBACK_PUBLIC_URL,
|
|
196
|
+
supported: !DESKTOP_MANAGED, appPort: PORT, webhookPort: WEBHOOK_PORT,
|
|
197
|
+
serverIpv4: DESKTOP_MANAGED ? null : customDomainIpv4(),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
174
200
|
const registry = new ProviderRegistry(BUILT_IN_DRIVERS);
|
|
201
|
+
const providerAuthSessions = new ProviderAuthSessions();
|
|
175
202
|
await registry.load(instanceConfigs(cfg));
|
|
176
203
|
const bundledSkills = loadBundledSkills();
|
|
177
204
|
const availableSkills = () => mergeSkills(bundledSkills, loadUserSkills(join(DATA_DIR, "skills")));
|
|
@@ -225,7 +252,7 @@ const browserCleanup = new BrowserCleanupCoordinator({
|
|
|
225
252
|
? Promise.all(sessions.map(async (session) => {
|
|
226
253
|
const ok = await clearBrowserSessionState(status.binaryPath, session, { encryptionKey: browserEngineEncryptionKey() });
|
|
227
254
|
if (!ok)
|
|
228
|
-
console.warn(`browser cleanup: could not clear
|
|
255
|
+
console.warn(`browser cleanup: could not clear saved state for session ${session}; restart OpenMausBot to retry this profile's cleanup. Do not use state clear --all: it erases other profiles too.`);
|
|
229
256
|
return ok;
|
|
230
257
|
}))
|
|
231
258
|
: Promise.resolve([true]);
|
|
@@ -420,10 +447,7 @@ function purgeGeneratedImagesForThread(threadId) {
|
|
|
420
447
|
continue;
|
|
421
448
|
generatedImagesByTurn.delete(key);
|
|
422
449
|
for (const attachment of attachments) {
|
|
423
|
-
|
|
424
|
-
unlinkSync(attachment.path);
|
|
425
|
-
}
|
|
426
|
-
catch { }
|
|
450
|
+
deleteAttachment(attachment.path);
|
|
427
451
|
}
|
|
428
452
|
}
|
|
429
453
|
}
|
|
@@ -443,10 +467,7 @@ function retireProviderTurn(turnId) {
|
|
|
443
467
|
continue;
|
|
444
468
|
generatedImagesByTurn.delete(key);
|
|
445
469
|
for (const attachment of attachments) {
|
|
446
|
-
|
|
447
|
-
unlinkSync(attachment.path);
|
|
448
|
-
}
|
|
449
|
-
catch { }
|
|
470
|
+
deleteAttachment(attachment.path);
|
|
450
471
|
}
|
|
451
472
|
}
|
|
452
473
|
}
|
|
@@ -501,7 +522,40 @@ function cancelDirectTurnDispatch(botId, expectedThreadId) {
|
|
|
501
522
|
/** The bot's browser for this turn: agent-browser, one isolated session per
|
|
502
523
|
* browser profile or per bot (docs/plans/browser-engine.md). Null, with the
|
|
503
524
|
* reason logged once, when the engine is not on this machine. */
|
|
504
|
-
|
|
525
|
+
const browserRuntime = new BrowserRuntime();
|
|
526
|
+
const browserLive = new BrowserLive({ runtime: browserRuntime });
|
|
527
|
+
// Temporary profiles last for this server run, but are never saved to disk.
|
|
528
|
+
// The viewer and the agent must address the SAME temporary browser.
|
|
529
|
+
const temporaryBrowserSessions = new Map();
|
|
530
|
+
function currentBrowserSession(botId, profile) {
|
|
531
|
+
if (profile === "guest") {
|
|
532
|
+
let session = temporaryBrowserSessions.get(botId);
|
|
533
|
+
if (!session) {
|
|
534
|
+
session = browserSessionId(botId, "guest");
|
|
535
|
+
temporaryBrowserSessions.set(botId, session);
|
|
536
|
+
}
|
|
537
|
+
return session;
|
|
538
|
+
}
|
|
539
|
+
const target = profile ? browserProfilePartitionTarget(cfg, profile) : null;
|
|
540
|
+
return browserSessionId(botId, target?.partitionId ?? "");
|
|
541
|
+
}
|
|
542
|
+
async function forgetTemporaryBrowser(botId) {
|
|
543
|
+
const session = temporaryBrowserSessions.get(botId);
|
|
544
|
+
if (!session)
|
|
545
|
+
return;
|
|
546
|
+
temporaryBrowserSessions.delete(botId);
|
|
547
|
+
const engine = browserEngineStatus();
|
|
548
|
+
if (engine.kind !== "ready")
|
|
549
|
+
return;
|
|
550
|
+
const closed = await clearBrowserSessionState(engine.binaryPath, session, {
|
|
551
|
+
env: { PATH: augmentedPath() }, encryptionKey: browserEngineEncryptionKey(),
|
|
552
|
+
});
|
|
553
|
+
if (closed)
|
|
554
|
+
await browserRuntime.close(session);
|
|
555
|
+
else
|
|
556
|
+
console.warn(`temporary browser ${session}: could not close its session; run agent-browser --session ${session} close on this server`);
|
|
557
|
+
}
|
|
558
|
+
async function browserIntegration(botId, profile, turn) {
|
|
505
559
|
const status = browserEngineStatus();
|
|
506
560
|
if (status.kind !== "ready") {
|
|
507
561
|
if (!engineUnavailableLogged) {
|
|
@@ -513,16 +567,29 @@ function browserIntegration(botId, profile) {
|
|
|
513
567
|
// A profile that no longer exists falls back to the bot's own session.
|
|
514
568
|
const profileTarget = profile && profile !== "guest" ? browserProfilePartitionTarget(cfg, profile) : null;
|
|
515
569
|
const partitionId = profile === "guest" ? "guest" : (profileTarget?.partitionId ?? "");
|
|
516
|
-
const session =
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
570
|
+
const session = currentBrowserSession(botId, profile);
|
|
571
|
+
const spec = agentBrowserIntegration({
|
|
572
|
+
binaryPath: status.binaryPath,
|
|
573
|
+
session,
|
|
574
|
+
encryptionKey: browserEngineEncryptionKey(),
|
|
575
|
+
persistent: profile !== "guest",
|
|
576
|
+
env: { ...process.env, PATH: augmentedPath() },
|
|
577
|
+
});
|
|
578
|
+
await prepareBrowserSessionState(status.binaryPath, session, { env: spec.env, persistent: profile !== "guest", isCurrent: () => {
|
|
579
|
+
const current = store.bot(botId);
|
|
580
|
+
return !!current && current.browser !== false && builtInBrowserEnabled(cfg)
|
|
581
|
+
&& currentBrowserSession(current.id, current.browserProfile) === session
|
|
582
|
+
&& (!turn || activeInternalGenerationByThread.get(turn.threadId) === turn.generation);
|
|
583
|
+
} });
|
|
584
|
+
if (!turn)
|
|
585
|
+
return { profile: partitionId, session, spec, integration: spec };
|
|
586
|
+
const token = mintInternalCapability({ botId, ...turn, browserSession: session,
|
|
587
|
+
kind: "browser", depth: 0, skillAuthoring: false, createdBots: 0 });
|
|
588
|
+
return { profile: partitionId, session, spec, integration: {
|
|
589
|
+
command: process.execPath, args: [SPAWNED_PROXIES.browser], env: {
|
|
590
|
+
...AGENTS_NODE_FLAG, OMB_BROWSER_TOKEN: token, OMB_HARNESS_URL: `http://127.0.0.1:${PORT}`,
|
|
591
|
+
},
|
|
592
|
+
} };
|
|
526
593
|
}
|
|
527
594
|
let engineUnavailableLogged = false;
|
|
528
595
|
let browserEngineInstall = null;
|
|
@@ -694,6 +761,9 @@ function checkedModelSelection(raw, current, requireAvailableModel = false) {
|
|
|
694
761
|
return { ok: false, status: 409, error: "the bot is working — stop it before changing models" };
|
|
695
762
|
}
|
|
696
763
|
const target = registry.get(selection.instanceId);
|
|
764
|
+
if (providerInstancesChanging.has(selection.instanceId)) {
|
|
765
|
+
return { ok: false, status: 409, error: "this provider account is being updated — try again shortly" };
|
|
766
|
+
}
|
|
697
767
|
// Model IDs remain free-form at the app's general API boundary. Custom
|
|
698
768
|
// engines can accept IDs that are not in their discovery catalog, and
|
|
699
769
|
// several drivers only learn the final catalog when a turn starts. The
|
|
@@ -1658,6 +1728,7 @@ function messageWindow(threadId, messageId, limit) {
|
|
|
1658
1728
|
}
|
|
1659
1729
|
const sseClients = new Set();
|
|
1660
1730
|
sessions.onSessionRevoked((sessionId) => {
|
|
1731
|
+
providerAuthSessions.revokeOwner(sessionId);
|
|
1661
1732
|
for (const client of sseClients) {
|
|
1662
1733
|
if (client.sessionId !== sessionId)
|
|
1663
1734
|
continue;
|
|
@@ -1720,10 +1791,9 @@ function broadcast(payload) {
|
|
|
1720
1791
|
for (const client of [...sseClients]) {
|
|
1721
1792
|
if (!wants(client, kind))
|
|
1722
1793
|
continue;
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
catch {
|
|
1794
|
+
// Screen frames are replaceable and durable events are not: see
|
|
1795
|
+
// ./sse-fanout.ts for the backpressure/bound decision this makes.
|
|
1796
|
+
if (deliverSseFrame(client, kind, client.admin ? frame : clientFrame) === "disconnected") {
|
|
1727
1797
|
sseClients.delete(client);
|
|
1728
1798
|
}
|
|
1729
1799
|
}
|
|
@@ -3410,6 +3480,9 @@ async function startTurn(botId, text, opts) {
|
|
|
3410
3480
|
}
|
|
3411
3481
|
console.error(`[omb-turn] bot=${botId} text=${JSON.stringify(resolvedImages.text.slice(0, 70))} images=${turnImages.length} depth=${commsDepth} card=${Boolean(opts?.cardContinuation)}`);
|
|
3412
3482
|
const instanceId = instance.instanceId;
|
|
3483
|
+
if (providerInstancesChanging.has(instanceId)) {
|
|
3484
|
+
throw Object.assign(new Error("this provider account is being updated — try again shortly"), { status: 409 });
|
|
3485
|
+
}
|
|
3413
3486
|
const model = opts?.runOn === "cloud" ? instance.models.default : bot.modelSelection.model;
|
|
3414
3487
|
// a cloud routine borrows the instance default model, so it borrows no
|
|
3415
3488
|
// per-bot effort either
|
|
@@ -3853,7 +3926,7 @@ async function startTurn(botId, text, opts) {
|
|
|
3853
3926
|
liveBot.browser !== false &&
|
|
3854
3927
|
instance.adapter.capabilities.browserMcp === true) {
|
|
3855
3928
|
const selectedProfile = liveBot.browserProfile;
|
|
3856
|
-
browser = browserIntegration(bot.id, selectedProfile);
|
|
3929
|
+
browser = await browserIntegration(bot.id, selectedProfile, { threadId, generation: dispatchClaimId });
|
|
3857
3930
|
if (browser)
|
|
3858
3931
|
integrations.browser = browser.integration;
|
|
3859
3932
|
// The browser lost its frame source when the Electron surface was
|
|
@@ -3863,8 +3936,9 @@ async function startTurn(botId, text, opts) {
|
|
|
3863
3936
|
// the work was a web page, because agent-browser runs its own headless
|
|
3864
3937
|
// Chrome on the host rather than inside that desktop.
|
|
3865
3938
|
if (browser) {
|
|
3866
|
-
const frame = { binaryPath: browser.
|
|
3867
|
-
|
|
3939
|
+
const frame = { binaryPath: browser.spec.command, env: browser.spec.env };
|
|
3940
|
+
const session = browser.session;
|
|
3941
|
+
browserCapture = () => browserRuntime.withAgentAction(session, () => agentBrowserFrame(frame));
|
|
3868
3942
|
}
|
|
3869
3943
|
}
|
|
3870
3944
|
// A cancelled adapter can be between accepting sendTurn and revealing
|
|
@@ -4595,6 +4669,10 @@ setupRetry = 0) {
|
|
|
4595
4669
|
const preparedComposio = bot.composio;
|
|
4596
4670
|
const instance = registry.get(bot.modelSelection.instanceId);
|
|
4597
4671
|
const userName = cfg.profile?.name?.trim() || "User";
|
|
4672
|
+
if (providerInstancesChanging.has(bot.modelSelection.instanceId)) {
|
|
4673
|
+
onDispatchError?.(`${bot.name}'s provider account is being updated — try again shortly`);
|
|
4674
|
+
return true;
|
|
4675
|
+
}
|
|
4598
4676
|
if (!instance) {
|
|
4599
4677
|
const message = `${bot.name}'s model is unavailable`;
|
|
4600
4678
|
store.appendMessage(threadId, {
|
|
@@ -4777,6 +4855,11 @@ setupRetry = 0) {
|
|
|
4777
4855
|
return true;
|
|
4778
4856
|
}
|
|
4779
4857
|
store.setActivity(bot.id, "working");
|
|
4858
|
+
// Claim cleanup ownership before browser preparation can yield or reject.
|
|
4859
|
+
// The finally block must release exactly this setup, never a newer turn.
|
|
4860
|
+
roomSpeaker = { botId: bot.id, name: bot.name, color: bot.color };
|
|
4861
|
+
groupSpeakers.set(threadId, roomSpeaker);
|
|
4862
|
+
store.patchGroup(readyGroup.id, { busyBotId: bot.id });
|
|
4780
4863
|
orchestration?.onClaimed?.();
|
|
4781
4864
|
// Connected-app discovery above can yield for a network round trip. A
|
|
4782
4865
|
// profile may be removed, or the browser feature switched off, during that
|
|
@@ -4787,24 +4870,19 @@ setupRetry = 0) {
|
|
|
4787
4870
|
readyBot.browser !== false &&
|
|
4788
4871
|
instance.adapter.capabilities.browserMcp === true) {
|
|
4789
4872
|
const selectedProfile = readyBot.browserProfile;
|
|
4790
|
-
const browser = browserIntegration(readyBot.id, selectedProfile);
|
|
4873
|
+
const browser = await browserIntegration(readyBot.id, selectedProfile, { threadId, generation: internalGeneration });
|
|
4791
4874
|
if (browser)
|
|
4792
4875
|
integrations.browser = browser.integration;
|
|
4793
4876
|
}
|
|
4794
|
-
// Stop/delete may land while
|
|
4795
|
-
//
|
|
4796
|
-
//
|
|
4877
|
+
// Stop/delete may land while browser state is being prepared. Capability
|
|
4878
|
+
// publication and this exact claim are both fenced; finally releases only
|
|
4879
|
+
// this setup, so a replacement turn's busy state is never cleared here.
|
|
4797
4880
|
const browserReadyBot = store.bot(readyBot.id);
|
|
4798
|
-
if (isCancelled?.() || !browserReadyBot ||
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
retryDelegationsWaitingOn(browserReadyBot.id);
|
|
4802
|
-
}
|
|
4881
|
+
if (isCancelled?.() || !browserReadyBot?.busy ||
|
|
4882
|
+
groupSpeakers.get(threadId) !== roomSpeaker ||
|
|
4883
|
+
activeInternalGenerationByThread.get(threadId) !== internalGeneration) {
|
|
4803
4884
|
return false;
|
|
4804
4885
|
}
|
|
4805
|
-
store.patchGroup(readyGroup.id, { busyBotId: bot.id }); // the store's change stream carries the frame
|
|
4806
|
-
roomSpeaker = { botId: bot.id, name: bot.name, color: bot.color };
|
|
4807
|
-
groupSpeakers.set(threadId, roomSpeaker);
|
|
4808
4886
|
// Room and Goal turns use the speaker's desktop, never the coordinator's.
|
|
4809
4887
|
// Claim the same lease as direct turns before asynchronous VM setup.
|
|
4810
4888
|
if (readyBot.computer === "vm") {
|
|
@@ -6570,9 +6648,45 @@ function persistMcpServers(next) {
|
|
|
6570
6648
|
// when an empty section is omitted by an older config file.
|
|
6571
6649
|
cfg.mcpServers = next;
|
|
6572
6650
|
}
|
|
6651
|
+
async function describeInstances() {
|
|
6652
|
+
const configs = instanceConfigs(cfg);
|
|
6653
|
+
return (await registry.describe()).map((instance) => {
|
|
6654
|
+
const entry = configs[instance.instanceId];
|
|
6655
|
+
if (entry?.driver !== "claudeAgent")
|
|
6656
|
+
return instance;
|
|
6657
|
+
try {
|
|
6658
|
+
const claudeAccount = claudeAccountInfo(instance.instanceId, entry, instance.cli ?? instance.cliDefault ?? "claude");
|
|
6659
|
+
return { ...instance, claudeAccount, install: { ...instance.install, signInCommand: claudeAccount.signInCommand } };
|
|
6660
|
+
}
|
|
6661
|
+
catch {
|
|
6662
|
+
// A malformed saved config remains a repairable shadow, never takes
|
|
6663
|
+
// the model picker down or offers a login for the wrong directory.
|
|
6664
|
+
return { ...instance, install: { ...instance.install, signInCommand: undefined } };
|
|
6665
|
+
}
|
|
6666
|
+
});
|
|
6667
|
+
}
|
|
6668
|
+
async function persistProviderInstance(instanceId, instances) {
|
|
6669
|
+
saveConfig({ instances }, { replaceInstances: true });
|
|
6670
|
+
cfg.instances = instances;
|
|
6671
|
+
providerAuthSessions.clearInstance(instanceId);
|
|
6672
|
+
bus.detach(instanceId);
|
|
6673
|
+
// No whole-fleet reload: other bots keep their live CLI processes, event
|
|
6674
|
+
// subscriptions and approval capabilities while this one is replaced.
|
|
6675
|
+
if (Object.hasOwn(instances, instanceId)) {
|
|
6676
|
+
await registry.load({ [instanceId]: instanceConfigs(cfg)[instanceId] });
|
|
6677
|
+
const live = registry.get(instanceId);
|
|
6678
|
+
if (live)
|
|
6679
|
+
bus.attach([live]);
|
|
6680
|
+
}
|
|
6681
|
+
else {
|
|
6682
|
+
await registry.dispose(instanceId);
|
|
6683
|
+
}
|
|
6684
|
+
resetPathCache();
|
|
6685
|
+
}
|
|
6573
6686
|
/** Rebuild the provider fleet after a config change so new keys take
|
|
6574
6687
|
* effect without a server restart (kills any in-flight turns). */
|
|
6575
6688
|
async function reloadProviders() {
|
|
6689
|
+
providerAuthSessions.clear();
|
|
6576
6690
|
// Every provider process is about to die. Revoke all turn capabilities in
|
|
6577
6691
|
// one synchronous step before the first teardown await, including room/task
|
|
6578
6692
|
// threads that are not a bot's default DM.
|
|
@@ -6609,6 +6723,7 @@ async function reloadProviders() {
|
|
|
6609
6723
|
// and reload sequence single-flight so two settings requests cannot drop one
|
|
6610
6724
|
// another's changes or dispose a fleet while another reload is creating it.
|
|
6611
6725
|
let providerConfigBusy = false;
|
|
6726
|
+
const providerInstancesChanging = new Set();
|
|
6612
6727
|
let mcpConfigBusy = false;
|
|
6613
6728
|
const MAX_CONCURRENT_MCP_PROBES = 2;
|
|
6614
6729
|
let mcpProbesInFlight = 0;
|
|
@@ -6716,6 +6831,12 @@ const handleRequest = async (req, res) => {
|
|
|
6716
6831
|
if (method === "GET" && path === "/.well-known/openmausbot/environment") {
|
|
6717
6832
|
return json(res, 200, environmentDescriptor({ environmentId: ENVIRONMENT_ID, desktopManaged: DESKTOP_MANAGED }));
|
|
6718
6833
|
}
|
|
6834
|
+
const domainCheck = /^\/\.well-known\/openmausbot\/domain-check\/([a-f0-9]{64})$/.exec(path);
|
|
6835
|
+
if (method === "GET" && domainCheck) {
|
|
6836
|
+
res.setHeader("cache-control", "no-store");
|
|
6837
|
+
const challenge = customDomainVerifier.challenge(domainCheck[1]);
|
|
6838
|
+
return json(res, challenge ? 200 : 404, challenge ?? { error: "No active domain check." });
|
|
6839
|
+
}
|
|
6719
6840
|
if (method === "POST" && path === "/api/auth/pair") {
|
|
6720
6841
|
// JSON only: a cross-site HTML form cannot send this content type
|
|
6721
6842
|
// without a preflight, so a stray unused code cannot be planted as a
|
|
@@ -6803,7 +6924,7 @@ const handleRequest = async (req, res) => {
|
|
|
6803
6924
|
const scopes = Array.isArray(requested) ? requested.filter((v) => v === "admin" || v === "client") : undefined;
|
|
6804
6925
|
const opened = sessions.openPairing({ label: typeof body?.label === "string" ? body.label : undefined, scopes });
|
|
6805
6926
|
const origin = requestOrigin(req);
|
|
6806
|
-
const base =
|
|
6927
|
+
const base = publicUrl() ?? (auth.kind === "session" && origin ? origin : null);
|
|
6807
6928
|
const code = formatPairingCode(opened.code);
|
|
6808
6929
|
return json(res, 200, {
|
|
6809
6930
|
id: opened.id,
|
|
@@ -6816,7 +6937,42 @@ const handleRequest = async (req, res) => {
|
|
|
6816
6937
|
});
|
|
6817
6938
|
}
|
|
6818
6939
|
if (method === "GET" && path === "/api/auth/pairing")
|
|
6819
|
-
return json(res, 200, { pairings: sessions.openPairings(), publicUrl:
|
|
6940
|
+
return json(res, 200, { pairings: sessions.openPairings(), publicUrl: publicUrl() });
|
|
6941
|
+
// Admin-only via request-auth's default deny. Connecting a domain only
|
|
6942
|
+
// changes future pairing links; DNS, proxy setup, webhooks and all existing
|
|
6943
|
+
// sessions remain untouched. The tunnel/deployment URL is kept as fallback.
|
|
6944
|
+
if (path === "/api/settings/custom-domain") {
|
|
6945
|
+
res.setHeader("cache-control", "no-store");
|
|
6946
|
+
if (method === "GET")
|
|
6947
|
+
return json(res, 200, customDomainStatus());
|
|
6948
|
+
if (method === "POST" || method === "DELETE") {
|
|
6949
|
+
if (DESKTOP_MANAGED)
|
|
6950
|
+
return json(res, 409, { error: "Custom domains are configured on a self-hosted OpenMausBot server, not the desktop companion." });
|
|
6951
|
+
if (!/^application\/json\b/i.test(String(req.headers["content-type"] ?? ""))) {
|
|
6952
|
+
return json(res, 415, { error: "content-type must be application/json" });
|
|
6953
|
+
}
|
|
6954
|
+
if (method === "DELETE") {
|
|
6955
|
+
saveConfig({ customDomain: "" });
|
|
6956
|
+
cfg.customDomain = "";
|
|
6957
|
+
customDomainRevision++;
|
|
6958
|
+
return json(res, 200, customDomainStatus());
|
|
6959
|
+
}
|
|
6960
|
+
const body = await readBody(req, 4096);
|
|
6961
|
+
if (typeof body?.domain !== "string")
|
|
6962
|
+
return json(res, 400, { error: "Enter your domain name." });
|
|
6963
|
+
const revision = customDomainRevision;
|
|
6964
|
+
const verified = await customDomainVerifier.verify(body.domain);
|
|
6965
|
+
if (auth.kind === "session" && !sessions.isLive(auth.session.id)) {
|
|
6966
|
+
return json(res, 401, { error: "Your session ended. Sign in again before connecting a domain." });
|
|
6967
|
+
}
|
|
6968
|
+
if (revision !== customDomainRevision)
|
|
6969
|
+
return json(res, 409, { error: "Domain settings changed during verification. Try again." });
|
|
6970
|
+
saveConfig({ customDomain: verified.origin });
|
|
6971
|
+
cfg.customDomain = verified.origin;
|
|
6972
|
+
customDomainRevision++;
|
|
6973
|
+
return json(res, 200, customDomainStatus());
|
|
6974
|
+
}
|
|
6975
|
+
}
|
|
6820
6976
|
m = path.match(/^\/api\/auth\/pairing\/([\w-]+)$/);
|
|
6821
6977
|
if (m && method === "DELETE") {
|
|
6822
6978
|
const cancelled = sessions.cancelPairing(m[1]);
|
|
@@ -6878,11 +7034,13 @@ const handleRequest = async (req, res) => {
|
|
|
6878
7034
|
if (!internalSender) {
|
|
6879
7035
|
return json(res, 401, { error: "unauthorized" });
|
|
6880
7036
|
}
|
|
6881
|
-
const requiredCapabilityKind = path
|
|
6882
|
-
? "
|
|
6883
|
-
: path
|
|
6884
|
-
? "
|
|
6885
|
-
: "
|
|
7037
|
+
const requiredCapabilityKind = path === "/api/internal/browser/mcp"
|
|
7038
|
+
? "browser"
|
|
7039
|
+
: path.startsWith("/api/internal/connectors/")
|
|
7040
|
+
? "connectors"
|
|
7041
|
+
: path === "/api/internal/computer-control"
|
|
7042
|
+
? "computer"
|
|
7043
|
+
: "agents";
|
|
6886
7044
|
if (internalCapability.kind !== requiredCapabilityKind) {
|
|
6887
7045
|
return json(res, 403, { error: "this internal capability cannot access that service" });
|
|
6888
7046
|
}
|
|
@@ -6926,6 +7084,30 @@ const handleRequest = async (req, res) => {
|
|
|
6926
7084
|
throw Object.assign(new Error("the internal turn capability has expired"), { status: 401 });
|
|
6927
7085
|
}
|
|
6928
7086
|
};
|
|
7087
|
+
if (method === "POST" && path === "/api/internal/browser/mcp") {
|
|
7088
|
+
const body = await readInternalBody();
|
|
7089
|
+
const bot = store.bot(internalCapability.botId);
|
|
7090
|
+
if (!bot || bot.browser === false || !builtInBrowserEnabled(cfg)) {
|
|
7091
|
+
return json(res, 403, { error: "browser tools are not enabled for this bot" });
|
|
7092
|
+
}
|
|
7093
|
+
const browser = await browserIntegration(bot.id, bot.browserProfile);
|
|
7094
|
+
if (!browser || browser.session !== internalCapability.browserSession) {
|
|
7095
|
+
return json(res, 409, { error: "this browser profile changed; start a new turn" });
|
|
7096
|
+
}
|
|
7097
|
+
if (body?.method !== "tools/list" && body?.method !== "tools/call") {
|
|
7098
|
+
return json(res, 400, { error: "unsupported browser method" });
|
|
7099
|
+
}
|
|
7100
|
+
const result = await browserRuntime.agentRpc(browser.session, browser.spec, body.method, body.params, () => {
|
|
7101
|
+
requireActiveInternalCapability();
|
|
7102
|
+
const current = store.bot(bot.id);
|
|
7103
|
+
if (!current || current.browser === false || !builtInBrowserEnabled(cfg) ||
|
|
7104
|
+
currentBrowserSession(current.id, current.browserProfile) !== browser.session) {
|
|
7105
|
+
throw Object.assign(new Error("Browser access changed while connecting."), { status: 409 });
|
|
7106
|
+
}
|
|
7107
|
+
});
|
|
7108
|
+
requireActiveInternalCapability();
|
|
7109
|
+
return json(res, 200, { result });
|
|
7110
|
+
}
|
|
6929
7111
|
if (method === "GET" && path === "/api/internal/agents") {
|
|
6930
7112
|
const sender = internalSender;
|
|
6931
7113
|
// title/description included so the caller can judge the team (who
|
|
@@ -8018,8 +8200,52 @@ const handleRequest = async (req, res) => {
|
|
|
8018
8200
|
: json(res, 404, { error: "no such webhook" });
|
|
8019
8201
|
}
|
|
8020
8202
|
// ── events stream ──
|
|
8203
|
+
// Owner-only (default-deny in request-auth). Never mix login frames into
|
|
8204
|
+
// the general events feed, which is also visible to client-only devices.
|
|
8205
|
+
const liveBrowserMatch = /^\/api\/bots\/([\w-]+)\/browser\/(live|action)$/.exec(path);
|
|
8206
|
+
if (liveBrowserMatch) {
|
|
8207
|
+
res.setHeader("cache-control", "no-store");
|
|
8208
|
+
const bot = store.bot(liveBrowserMatch[1]);
|
|
8209
|
+
if (!bot)
|
|
8210
|
+
return json(res, 404, { error: "no such bot" });
|
|
8211
|
+
if (!builtInBrowserEnabled(cfg) || bot.browser === false) {
|
|
8212
|
+
return json(res, 409, { error: "Enable this bot's browser in its profile first." });
|
|
8213
|
+
}
|
|
8214
|
+
const browser = await browserIntegration(bot.id, bot.browserProfile);
|
|
8215
|
+
if (!browser)
|
|
8216
|
+
return json(res, 503, { error: "Install the browser engine first." });
|
|
8217
|
+
const owner = auth.kind === "session" ? auth.session.id : "local-owner";
|
|
8218
|
+
const isCurrent = () => {
|
|
8219
|
+
const current = store.bot(bot.id);
|
|
8220
|
+
return !!current && current.browser !== false && builtInBrowserEnabled(cfg)
|
|
8221
|
+
&& currentBrowserSession(current.id, current.browserProfile) === browser.session
|
|
8222
|
+
&& (auth.kind !== "session" || sessions.isLive(auth.session.id));
|
|
8223
|
+
};
|
|
8224
|
+
if (method === "GET" && liveBrowserMatch[2] === "live") {
|
|
8225
|
+
req.socket.setTimeout(0);
|
|
8226
|
+
return await browserLive.open({ botId: bot.id, session: browser.session, spec: browser.spec, owner, isCurrent, res });
|
|
8227
|
+
}
|
|
8228
|
+
if (method === "POST" && liveBrowserMatch[2] === "action") {
|
|
8229
|
+
const body = await readBody(req, 32_768);
|
|
8230
|
+
if (!isCurrent())
|
|
8231
|
+
return json(res, 409, { error: "This browser session changed. Reopen the browser panel." });
|
|
8232
|
+
if (typeof body?.viewerId !== "string")
|
|
8233
|
+
return json(res, 400, { error: "A live browser connection is required." });
|
|
8234
|
+
if (body.type === "restart" && store.bots.some((candidate) => candidate.busy &&
|
|
8235
|
+
currentBrowserSession(candidate.id, candidate.browserProfile) === browser.session)) {
|
|
8236
|
+
return json(res, 409, { error: "Stop every bot using this profile before restarting its browser." });
|
|
8237
|
+
}
|
|
8238
|
+
return json(res, 200, await browserLive.action({ viewerId: body.viewerId, botId: bot.id, owner, body }));
|
|
8239
|
+
}
|
|
8240
|
+
return json(res, 405, { error: "method not allowed" });
|
|
8241
|
+
}
|
|
8021
8242
|
if (method === "GET" && path === "/api/events") {
|
|
8022
|
-
const client = {
|
|
8243
|
+
const client = {
|
|
8244
|
+
res,
|
|
8245
|
+
admin: auth.scopes.includes("admin"),
|
|
8246
|
+
screens: url.searchParams.get("screens") !== "off",
|
|
8247
|
+
backpressured: false,
|
|
8248
|
+
};
|
|
8023
8249
|
if (auth.kind === "session")
|
|
8024
8250
|
client.sessionId = auth.session.id;
|
|
8025
8251
|
res.writeHead(200, {
|
|
@@ -9401,10 +9627,7 @@ const handleRequest = async (req, res) => {
|
|
|
9401
9627
|
if (!bot) {
|
|
9402
9628
|
// There are no awaits between the refreshed lookup and this patch, but
|
|
9403
9629
|
// keep the attachment invariant explicit if the store ever changes.
|
|
9404
|
-
|
|
9405
|
-
unlinkSync(saved.path);
|
|
9406
|
-
}
|
|
9407
|
-
catch { }
|
|
9630
|
+
deleteAttachment(saved.path);
|
|
9408
9631
|
return json(res, 404, { error: "no such bot" });
|
|
9409
9632
|
}
|
|
9410
9633
|
const visible = wireBot(bot);
|
|
@@ -9510,6 +9733,8 @@ const handleRequest = async (req, res) => {
|
|
|
9510
9733
|
if (body.requireAvailableModel !== undefined && typeof body.requireAvailableModel !== "boolean") {
|
|
9511
9734
|
return json(res, 400, { error: "requireAvailableModel must be true or false" });
|
|
9512
9735
|
}
|
|
9736
|
+
const beforeBrowserProfile = existingBot?.browserProfile;
|
|
9737
|
+
const beforeBrowserEnabled = existingBot?.browser;
|
|
9513
9738
|
// Neither Codex (free-form string field) nor Grok (lazy, logs-only)
|
|
9514
9739
|
// rejects an unknown effort level at their own boundary — this is the
|
|
9515
9740
|
// only real gate, so it stays. But it fires only when the target
|
|
@@ -9626,6 +9851,10 @@ const handleRequest = async (req, res) => {
|
|
|
9626
9851
|
if (existingBot?.busy && requestedProfile !== existingBot.browserProfile) {
|
|
9627
9852
|
return json(res, 409, { error: "stop this bot's turn before changing its browser profile" });
|
|
9628
9853
|
}
|
|
9854
|
+
if (existingBot && requestedProfile !== existingBot.browserProfile &&
|
|
9855
|
+
browserRuntime.heldBy(currentBrowserSession(existingBot.id, existingBot.browserProfile))) {
|
|
9856
|
+
return json(res, 409, { error: "Release browser control before changing its profile." });
|
|
9857
|
+
}
|
|
9629
9858
|
if (requestedProfile === undefined)
|
|
9630
9859
|
patch.browserProfile = undefined;
|
|
9631
9860
|
else if (typeof requestedProfile === "string" &&
|
|
@@ -9838,6 +10067,12 @@ const handleRequest = async (req, res) => {
|
|
|
9838
10067
|
section !== undefined &&
|
|
9839
10068
|
sectionKey(existingBot?.section) !== sectionKey(section);
|
|
9840
10069
|
let bot;
|
|
10070
|
+
const freshBrowserBot = store.bot(m[1]);
|
|
10071
|
+
if (freshBrowserBot && body.browserProfile !== undefined &&
|
|
10072
|
+
patch.browserProfile !== freshBrowserBot.browserProfile &&
|
|
10073
|
+
(freshBrowserBot.busy || browserRuntime.heldBy(currentBrowserSession(freshBrowserBot.id, freshBrowserBot.browserProfile)))) {
|
|
10074
|
+
return json(res, 409, { error: "Stop the bot and release browser control before changing its profile." });
|
|
10075
|
+
}
|
|
9841
10076
|
if (profile.patch.soul !== undefined) {
|
|
9842
10077
|
// A mixed settings request must not turn a runtime revocation into
|
|
9843
10078
|
// a persist-first edit. Apply runtime fields with their existing
|
|
@@ -9854,6 +10089,12 @@ const handleRequest = async (req, res) => {
|
|
|
9854
10089
|
}
|
|
9855
10090
|
if (!bot)
|
|
9856
10091
|
return json(res, 404, { error: "no such bot" });
|
|
10092
|
+
if (existingBot && (bot.browserProfile !== beforeBrowserProfile || bot.browser !== beforeBrowserEnabled)) {
|
|
10093
|
+
browserLive.closeForBot(bot.id);
|
|
10094
|
+
if (beforeBrowserProfile === "guest" && (bot.browserProfile !== "guest" || bot.browser === false)) {
|
|
10095
|
+
void forgetTemporaryBrowser(bot.id).catch((error) => console.warn("temporary browser cleanup failed", error));
|
|
10096
|
+
}
|
|
10097
|
+
}
|
|
9857
10098
|
const chiefChanges = body.chiefOfStaff === true || chiefMovedSections
|
|
9858
10099
|
? store.setChiefOfStaff(bot.id)
|
|
9859
10100
|
: [];
|
|
@@ -10050,6 +10291,8 @@ const handleRequest = async (req, res) => {
|
|
|
10050
10291
|
localVmIdles.get(target.key)?.cancel();
|
|
10051
10292
|
localVmIdles.delete(target.key);
|
|
10052
10293
|
store.deleteBot(bot.id);
|
|
10294
|
+
browserLive.closeForBot(bot.id);
|
|
10295
|
+
await forgetTemporaryBrowser(bot.id);
|
|
10053
10296
|
}
|
|
10054
10297
|
catch (error) {
|
|
10055
10298
|
if (browserCleanupRequest)
|
|
@@ -11168,7 +11411,32 @@ const handleRequest = async (req, res) => {
|
|
|
11168
11411
|
// Windows never pushes PATH changes into a live process, so without
|
|
11169
11412
|
// this the answer is frozen at boot and "check again" is a no-op.
|
|
11170
11413
|
resetPathCache();
|
|
11171
|
-
return json(res, 200, { instances: await
|
|
11414
|
+
return json(res, 200, { instances: await describeInstances() });
|
|
11415
|
+
}
|
|
11416
|
+
if (method === "POST" && path === "/api/instances/claude-accounts") {
|
|
11417
|
+
if (!String(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) {
|
|
11418
|
+
return json(res, 415, { error: "content-type must be application/json" });
|
|
11419
|
+
}
|
|
11420
|
+
const parsed = createClaudeAccountSchema.safeParse(await readBody(req, 8192));
|
|
11421
|
+
if (!parsed.success)
|
|
11422
|
+
return json(res, 400, { error: "Enter an account name (up to 80 characters) and an optional configuration directory." });
|
|
11423
|
+
if (providerConfigBusy)
|
|
11424
|
+
return json(res, 409, { error: "provider settings are already being updated" });
|
|
11425
|
+
providerConfigBusy = true;
|
|
11426
|
+
try {
|
|
11427
|
+
const { instanceId, instances } = newClaudeAccount(cfg, parsed.data);
|
|
11428
|
+
await persistProviderInstance(instanceId, instances);
|
|
11429
|
+
return json(res, 201, { instanceId, instances: await describeInstances() });
|
|
11430
|
+
}
|
|
11431
|
+
finally {
|
|
11432
|
+
providerConfigBusy = false;
|
|
11433
|
+
}
|
|
11434
|
+
}
|
|
11435
|
+
const authStatus = /^\/api\/instances\/([\w.-]+)\/auth\/status$/.exec(path);
|
|
11436
|
+
if (method === "GET" && authStatus) {
|
|
11437
|
+
res.setHeader("cache-control", "no-store");
|
|
11438
|
+
const state = await providerAuthSessions.status(authStatus[1], auth.kind === "session" ? auth.session.id : "loopback", url.searchParams.get("flowId") ?? "");
|
|
11439
|
+
return json(res, 200, { auth: state });
|
|
11172
11440
|
}
|
|
11173
11441
|
const instanceAction = /^\/api\/instances\/([\w.-]+)\/(refresh-models|install|auth\/start|auth\/complete|auth\/cancel)$/.exec(path);
|
|
11174
11442
|
if (method === "POST" && instanceAction) {
|
|
@@ -11177,22 +11445,31 @@ const handleRequest = async (req, res) => {
|
|
|
11177
11445
|
}
|
|
11178
11446
|
const instanceId = instanceAction[1];
|
|
11179
11447
|
const action = instanceAction[2];
|
|
11448
|
+
const owner = auth.kind === "session" ? auth.session.id : "loopback";
|
|
11449
|
+
if (action.startsWith("auth/"))
|
|
11450
|
+
res.setHeader("cache-control", "no-store");
|
|
11180
11451
|
try {
|
|
11181
11452
|
if (action === "refresh-models") {
|
|
11182
11453
|
if (!(await registry.refreshModels(instanceId)))
|
|
11183
11454
|
return json(res, 404, { error: "unknown instance" });
|
|
11184
|
-
return json(res, 200, { instances: await
|
|
11455
|
+
return json(res, 200, { instances: await describeInstances() });
|
|
11185
11456
|
}
|
|
11186
11457
|
if (action === "install") {
|
|
11187
11458
|
if (!(await registry.installRuntime(instanceId)))
|
|
11188
11459
|
return json(res, 404, { error: "managed installation is unavailable" });
|
|
11189
|
-
return json(res, 200, { instances: await
|
|
11460
|
+
return json(res, 200, { instances: await describeInstances() });
|
|
11190
11461
|
}
|
|
11191
11462
|
if (action === "auth/start") {
|
|
11192
|
-
const
|
|
11193
|
-
if (!
|
|
11194
|
-
return json(res, 404, { error: "
|
|
11195
|
-
|
|
11463
|
+
const instance = registry.get(instanceId);
|
|
11464
|
+
if (!instance)
|
|
11465
|
+
return json(res, 404, { error: "unknown instance" });
|
|
11466
|
+
const started = await providerAuthSessions.start(instance, owner);
|
|
11467
|
+
// Revocation can arrive while the CLI is obtaining a device code.
|
|
11468
|
+
if (auth.kind === "session" && !sessions.isLive(auth.session.id)) {
|
|
11469
|
+
providerAuthSessions.revokeOwner(owner);
|
|
11470
|
+
return json(res, 401, { error: "Your session ended. Start a new sign-in." });
|
|
11471
|
+
}
|
|
11472
|
+
return json(res, 200, { auth: started });
|
|
11196
11473
|
}
|
|
11197
11474
|
if (action === "auth/complete") {
|
|
11198
11475
|
const body = await readBody(req);
|
|
@@ -11200,20 +11477,16 @@ const handleRequest = async (req, res) => {
|
|
|
11200
11477
|
const callbackUrl = typeof body?.callbackUrl === "string" ? body.callbackUrl : "";
|
|
11201
11478
|
if (!flowId || !callbackUrl)
|
|
11202
11479
|
return json(res, 400, { error: "flowId and callbackUrl are required" });
|
|
11203
|
-
|
|
11204
|
-
return json(res, 404, { error: "account setup is unavailable" });
|
|
11205
|
-
}
|
|
11480
|
+
await providerAuthSessions.complete(instanceId, owner, flowId, callbackUrl);
|
|
11206
11481
|
return json(res, 200, { ok: true });
|
|
11207
11482
|
}
|
|
11208
|
-
|
|
11209
|
-
|
|
11210
|
-
}
|
|
11483
|
+
const body = await readBody(req, 4096);
|
|
11484
|
+
await providerAuthSessions.cancel(instanceId, owner, typeof body?.flowId === "string" ? body.flowId : "");
|
|
11211
11485
|
return json(res, 200, { ok: true });
|
|
11212
11486
|
}
|
|
11213
11487
|
catch (error) {
|
|
11214
|
-
const
|
|
11215
|
-
|
|
11216
|
-
: 500;
|
|
11488
|
+
const requestedStatus = error && typeof error === "object" ? error.status : undefined;
|
|
11489
|
+
const status = typeof requestedStatus === "number" && [400, 401, 404, 409, 413, 415].includes(requestedStatus) ? requestedStatus : 500;
|
|
11217
11490
|
return json(res, status, { error: error instanceof Error ? error.message : String(error) });
|
|
11218
11491
|
}
|
|
11219
11492
|
}
|
|
@@ -11290,38 +11563,80 @@ const handleRequest = async (req, res) => {
|
|
|
11290
11563
|
}
|
|
11291
11564
|
// ── per-instance CLI path override (custom builds / versioned bins) ──
|
|
11292
11565
|
// PATCH /api/instances/:id {cli: "/path/to/cli" | ""} — "" reverts to the
|
|
11293
|
-
// driver default.
|
|
11566
|
+
// driver default. Only this idle instance is replaced; siblings keep running.
|
|
11294
11567
|
const instancePatch = /^\/api\/instances\/([\w.-]+)$/.exec(path);
|
|
11295
11568
|
if (method === "PATCH" && instancePatch) {
|
|
11296
11569
|
// same non-simple-request gate as the local-VM lifecycle routes
|
|
11297
11570
|
if (!String(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) {
|
|
11298
11571
|
return json(res, 415, { error: "content-type must be application/json" });
|
|
11299
11572
|
}
|
|
11300
|
-
const
|
|
11301
|
-
if (
|
|
11302
|
-
return json(res, 400, { error: "
|
|
11303
|
-
|
|
11304
|
-
|
|
11573
|
+
const parsed = instanceSettingsSchema.safeParse(await readBody(req, 16384));
|
|
11574
|
+
if (!parsed.success)
|
|
11575
|
+
return json(res, 400, { error: "Supply a valid CLI path, account name or configuration directory." });
|
|
11576
|
+
const body = parsed.data;
|
|
11577
|
+
const instanceId = instancePatch[1];
|
|
11305
11578
|
if (providerConfigBusy)
|
|
11306
11579
|
return json(res, 409, { error: "provider settings are already being updated" });
|
|
11580
|
+
if (store.bots.some((bot) => bot.busy && bot.modelSelection.instanceId === instanceId)) {
|
|
11581
|
+
return json(res, 409, { error: "Wait for bots using this account to finish before changing its settings." });
|
|
11582
|
+
}
|
|
11307
11583
|
providerConfigBusy = true;
|
|
11584
|
+
providerInstancesChanging.add(instanceId);
|
|
11308
11585
|
try {
|
|
11309
|
-
const result = withInstanceCli(cfg,
|
|
11310
|
-
|
|
11311
|
-
|
|
11312
|
-
|
|
11313
|
-
|
|
11314
|
-
|
|
11315
|
-
|
|
11316
|
-
|
|
11317
|
-
|
|
11318
|
-
|
|
11319
|
-
|
|
11320
|
-
|
|
11321
|
-
|
|
11322
|
-
|
|
11586
|
+
const result = body.cli === undefined ? { ok: true, config: cfg } : withInstanceCli(cfg, instanceId, body.cli);
|
|
11587
|
+
const instances = persistableInstanceConfigs(result.config);
|
|
11588
|
+
if (!result.ok || !Object.hasOwn(instances, instanceId))
|
|
11589
|
+
return json(res, 404, { error: `unknown instance "${instanceId}"` });
|
|
11590
|
+
const entry = instances[instanceId];
|
|
11591
|
+
if ((body.displayName !== undefined || body.configDir !== undefined) && entry.driver !== "claudeAgent") {
|
|
11592
|
+
return json(res, 400, { error: "Account settings are currently available for Claude only." });
|
|
11593
|
+
}
|
|
11594
|
+
if (body.displayName !== undefined)
|
|
11595
|
+
entry.displayName = body.displayName;
|
|
11596
|
+
if (body.configDir !== undefined) {
|
|
11597
|
+
let previousDir;
|
|
11598
|
+
try {
|
|
11599
|
+
previousDir = configuredAccountDirectory(entry);
|
|
11600
|
+
}
|
|
11601
|
+
catch { /* Allow repairing an unused malformed account. */ }
|
|
11602
|
+
entry.config = { ...entry.config, configDir: body.configDir };
|
|
11603
|
+
if (previousDir !== configuredAccountDirectory(entry)) {
|
|
11604
|
+
const used = store.bots.some((bot) => bot.modelSelection.instanceId === instanceId || store.tasks(bot.id).some((task) => task.resumeCursors[instanceId] || task.lastInstanceId === instanceId));
|
|
11605
|
+
if (used)
|
|
11606
|
+
return json(res, 409, { error: "This account is used by bots or conversation history. Add another account and select it for the bot instead." });
|
|
11607
|
+
assertSeparateClaudeAccount(instances, instanceId, entry);
|
|
11608
|
+
}
|
|
11609
|
+
}
|
|
11610
|
+
await persistProviderInstance(instanceId, instances);
|
|
11611
|
+
return json(res, 200, { instances: await describeInstances() });
|
|
11612
|
+
}
|
|
11613
|
+
finally {
|
|
11614
|
+
providerInstancesChanging.delete(instanceId);
|
|
11615
|
+
providerConfigBusy = false;
|
|
11616
|
+
}
|
|
11617
|
+
}
|
|
11618
|
+
if (method === "DELETE" && instancePatch) {
|
|
11619
|
+
const instanceId = instancePatch[1];
|
|
11620
|
+
if (providerConfigBusy)
|
|
11621
|
+
return json(res, 409, { error: "provider settings are already being updated" });
|
|
11622
|
+
const instances = persistableInstanceConfigs(cfg);
|
|
11623
|
+
if (!Object.hasOwn(instances, instanceId))
|
|
11624
|
+
return json(res, 404, { error: "unknown instance" });
|
|
11625
|
+
if (instances[instanceId].driver !== "claudeAgent" || instanceId === "claude") {
|
|
11626
|
+
return json(res, 400, { error: "Only added Claude accounts can be removed here." });
|
|
11627
|
+
}
|
|
11628
|
+
if (cfg.defaultModelSelection?.instanceId === instanceId || store.bots.some((bot) => bot.modelSelection.instanceId === instanceId)) {
|
|
11629
|
+
return json(res, 409, { error: "Choose another account for the bots and default model using this account before removing it." });
|
|
11630
|
+
}
|
|
11631
|
+
providerConfigBusy = true;
|
|
11632
|
+
providerInstancesChanging.add(instanceId);
|
|
11633
|
+
try {
|
|
11634
|
+
delete instances[instanceId];
|
|
11635
|
+
await persistProviderInstance(instanceId, instances);
|
|
11636
|
+
return json(res, 200, { instances: await describeInstances() });
|
|
11323
11637
|
}
|
|
11324
11638
|
finally {
|
|
11639
|
+
providerInstancesChanging.delete(instanceId);
|
|
11325
11640
|
providerConfigBusy = false;
|
|
11326
11641
|
}
|
|
11327
11642
|
}
|
|
@@ -11438,12 +11753,24 @@ const handleRequest = async (req, res) => {
|
|
|
11438
11753
|
return json(res, 400, { error: "nothing to save" });
|
|
11439
11754
|
if (providerConfigBusy)
|
|
11440
11755
|
return json(res, 409, { error: "provider settings are already being updated" });
|
|
11756
|
+
if (patch.browserProfiles !== undefined && body.expectedBrowserProfiles !== undefined) {
|
|
11757
|
+
const current = (cfg.browserProfiles ?? []).map(({ id, name }) => ({ id, name }));
|
|
11758
|
+
if (JSON.stringify(body.expectedBrowserProfiles) !== JSON.stringify(current)) {
|
|
11759
|
+
return json(res, 409, { error: "Browser profiles changed in another window. Review the refreshed list and try again." });
|
|
11760
|
+
}
|
|
11761
|
+
}
|
|
11441
11762
|
const disablingBuiltInBrowser = patch.features?.browser === false && builtInBrowserEnabled(cfg);
|
|
11442
11763
|
const removedBrowserProfileIds = patch.browserProfiles === undefined
|
|
11443
11764
|
? []
|
|
11444
11765
|
: (cfg.browserProfiles ?? [])
|
|
11445
11766
|
.map((profile) => profile.id)
|
|
11446
11767
|
.filter((id) => !patch.browserProfiles.some((profile) => profile.id === id));
|
|
11768
|
+
const profileControlConflict = () => removedBrowserProfileIds.some((id) => {
|
|
11769
|
+
const target = browserProfilePartitionTarget(cfg, id);
|
|
11770
|
+
return target && browserRuntime.heldBy(browserSessionId("", target.partitionId));
|
|
11771
|
+
});
|
|
11772
|
+
if (profileControlConflict())
|
|
11773
|
+
return json(res, 409, { error: "Release browser control before deleting its profile." });
|
|
11447
11774
|
if (patch.browserProfiles !== undefined) {
|
|
11448
11775
|
const currentProfiles = new Map((cfg.browserProfiles ?? []).map((profile) => [profile.id, profile]));
|
|
11449
11776
|
const nextProfiles = patch.browserProfiles.map((profile) => {
|
|
@@ -11672,6 +11999,8 @@ const handleRequest = async (req, res) => {
|
|
|
11672
11999
|
return json(res, 409, { error: conflict });
|
|
11673
12000
|
}
|
|
11674
12001
|
const browserCleanupRequests = [];
|
|
12002
|
+
if (profileControlConflict())
|
|
12003
|
+
return json(res, 409, { error: "Release browser control before deleting its profile." });
|
|
11675
12004
|
try {
|
|
11676
12005
|
for (const profileId of removedBrowserProfileIds) {
|
|
11677
12006
|
const target = browserProfilePartitionTarget(cfg, profileId);
|
|
@@ -11736,6 +12065,12 @@ const handleRequest = async (req, res) => {
|
|
|
11736
12065
|
throw error;
|
|
11737
12066
|
}
|
|
11738
12067
|
let browserReferenceCleanupError = null;
|
|
12068
|
+
if (disablingBuiltInBrowser)
|
|
12069
|
+
browserLive.closeAll();
|
|
12070
|
+
for (const request of browserCleanupRequests) {
|
|
12071
|
+
if (request.kind === "profile")
|
|
12072
|
+
browserLive.closeForSession(browserSessionId("", request.partitionId));
|
|
12073
|
+
}
|
|
11739
12074
|
if (patch.browserProfiles !== undefined) {
|
|
11740
12075
|
const retained = new Set(patch.browserProfiles.map((profile) => profile.id));
|
|
11741
12076
|
try {
|
|
@@ -12228,6 +12563,21 @@ calendarCalls.start();
|
|
|
12228
12563
|
// Resolve the edition before accepting requests so /api/edition is never a guess.
|
|
12229
12564
|
console.log(describeEdition(await loadEnterpriseLayer()));
|
|
12230
12565
|
console.log(describeBrand(loadBrand()));
|
|
12566
|
+
// Reclaim upload partials a previous run crashed out of, and warm the
|
|
12567
|
+
// attachment quota cache off the same scan. This used to happen implicitly on
|
|
12568
|
+
// every reservation, which is exactly what made uploads quadratic in
|
|
12569
|
+
// directory size; do the initial sweep before accepting requests.
|
|
12570
|
+
// ponytail: once per boot, not periodic. A partial orphaned while this
|
|
12571
|
+
// process is up survives until the next restart — add a timer only if that
|
|
12572
|
+
// shows up as real quota pressure.
|
|
12573
|
+
try {
|
|
12574
|
+
const reclaimedPartials = cleanupStaleAttachmentPartials();
|
|
12575
|
+
if (reclaimedPartials > 0)
|
|
12576
|
+
console.log(`reclaimed ${reclaimedPartials} abandoned upload partial(s)`);
|
|
12577
|
+
}
|
|
12578
|
+
catch (error) {
|
|
12579
|
+
console.warn(`attachments: startup partial cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
12580
|
+
}
|
|
12231
12581
|
server.listen(PORT, "127.0.0.1", () => {
|
|
12232
12582
|
console.log(`openmausbot server on http://127.0.0.1:${PORT}`);
|
|
12233
12583
|
});
|
|
@@ -12253,6 +12603,7 @@ const gracefulShutdown = createGracefulShutdown({
|
|
|
12253
12603
|
// asynchronous shutdown jobs drain. Invalidate their turn bearers before
|
|
12254
12604
|
// any cleanup function reaches an await.
|
|
12255
12605
|
revokeAllInternalCapabilities();
|
|
12606
|
+
browserLive.closeAll();
|
|
12256
12607
|
for (const idle of localVmIdles.values())
|
|
12257
12608
|
idle.cancel();
|
|
12258
12609
|
vps.closeAllVpsDesktopTunnels();
|
|
@@ -12263,6 +12614,10 @@ const gracefulShutdown = createGracefulShutdown({
|
|
|
12263
12614
|
tunnelListener?.close();
|
|
12264
12615
|
},
|
|
12265
12616
|
() => registry.disposeAll(),
|
|
12617
|
+
async () => {
|
|
12618
|
+
await Promise.all([...temporaryBrowserSessions.keys()].map((botId) => forgetTemporaryBrowser(botId)));
|
|
12619
|
+
await browserRuntime.closeAll();
|
|
12620
|
+
},
|
|
12266
12621
|
() => flushAllProfileHistory(),
|
|
12267
12622
|
],
|
|
12268
12623
|
// Cleanup jobs run concurrently. Release only after they settle (or reach
|