openmausbot 0.1.65 → 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-BZeH50da.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 +1 -0
- package/dist-server/index.js +14824 -13321
- package/dist-server/openmausbot.js +358 -87
- package/dist-server/pair-cli.js +358 -87
- package/dist-server/proxy-paths.js +1 -0
- package/dist-server/server/attachments.js +125 -13
- 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 +26 -19
- package/dist-server/server/custom-domain.js +14 -0
- package/dist-server/server/drivers/claude.js +77 -19
- package/dist-server/server/harness/bus.js +9 -4
- package/dist-server/server/harness/registry.js +8 -0
- package/dist-server/server/index.js +345 -72
- package/dist-server/server/provider-auth-sessions.js +7 -0
- package/dist-server/server/proxy-paths.js +1 -0
- package/dist-server/src/lib/composer-attachments.js +3 -3
- package/dist-server/vps-container-mcp.js +1 -0
- package/package.json +1 -1
- package/dist/assets/index-BOrNJcfG.js +0 -273
- package/dist/assets/index-Dwzk199I.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,7 +120,7 @@ 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";
|
|
120
|
-
import { createCustomDomainVerifier, normalizeCustomDomain } from "./custom-domain.js";
|
|
123
|
+
import { createCustomDomainVerifier, customDomainIpv4, normalizeCustomDomain } from "./custom-domain.js";
|
|
121
124
|
import { ProviderAuthSessions } from "./provider-auth-sessions.js";
|
|
122
125
|
import { clearSessionCookie, clientBotPatchViolation, clientGroupPatchViolation, labelFromUserAgent, requestOrigin, requestSource, resolveRequestAuth, parseCookies, serializeSessionCookie, sessionCookieName, } from "./request-auth.js";
|
|
123
126
|
import { cookieMaxAgeSeconds, formatPairingCode, SessionRegistry } from "./sessions.js";
|
|
@@ -191,6 +194,7 @@ function customDomainStatus() {
|
|
|
191
194
|
return {
|
|
192
195
|
customDomain: savedCustomDomain(), publicUrl: publicUrl(), fallbackUrl: FALLBACK_PUBLIC_URL,
|
|
193
196
|
supported: !DESKTOP_MANAGED, appPort: PORT, webhookPort: WEBHOOK_PORT,
|
|
197
|
+
serverIpv4: DESKTOP_MANAGED ? null : customDomainIpv4(),
|
|
194
198
|
};
|
|
195
199
|
}
|
|
196
200
|
const registry = new ProviderRegistry(BUILT_IN_DRIVERS);
|
|
@@ -248,7 +252,7 @@ const browserCleanup = new BrowserCleanupCoordinator({
|
|
|
248
252
|
? Promise.all(sessions.map(async (session) => {
|
|
249
253
|
const ok = await clearBrowserSessionState(status.binaryPath, session, { encryptionKey: browserEngineEncryptionKey() });
|
|
250
254
|
if (!ok)
|
|
251
|
-
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.`);
|
|
252
256
|
return ok;
|
|
253
257
|
}))
|
|
254
258
|
: Promise.resolve([true]);
|
|
@@ -443,10 +447,7 @@ function purgeGeneratedImagesForThread(threadId) {
|
|
|
443
447
|
continue;
|
|
444
448
|
generatedImagesByTurn.delete(key);
|
|
445
449
|
for (const attachment of attachments) {
|
|
446
|
-
|
|
447
|
-
unlinkSync(attachment.path);
|
|
448
|
-
}
|
|
449
|
-
catch { }
|
|
450
|
+
deleteAttachment(attachment.path);
|
|
450
451
|
}
|
|
451
452
|
}
|
|
452
453
|
}
|
|
@@ -466,10 +467,7 @@ function retireProviderTurn(turnId) {
|
|
|
466
467
|
continue;
|
|
467
468
|
generatedImagesByTurn.delete(key);
|
|
468
469
|
for (const attachment of attachments) {
|
|
469
|
-
|
|
470
|
-
unlinkSync(attachment.path);
|
|
471
|
-
}
|
|
472
|
-
catch { }
|
|
470
|
+
deleteAttachment(attachment.path);
|
|
473
471
|
}
|
|
474
472
|
}
|
|
475
473
|
}
|
|
@@ -524,7 +522,40 @@ function cancelDirectTurnDispatch(botId, expectedThreadId) {
|
|
|
524
522
|
/** The bot's browser for this turn: agent-browser, one isolated session per
|
|
525
523
|
* browser profile or per bot (docs/plans/browser-engine.md). Null, with the
|
|
526
524
|
* reason logged once, when the engine is not on this machine. */
|
|
527
|
-
|
|
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) {
|
|
528
559
|
const status = browserEngineStatus();
|
|
529
560
|
if (status.kind !== "ready") {
|
|
530
561
|
if (!engineUnavailableLogged) {
|
|
@@ -536,17 +567,29 @@ function browserIntegration(botId, profile) {
|
|
|
536
567
|
// A profile that no longer exists falls back to the bot's own session.
|
|
537
568
|
const profileTarget = profile && profile !== "guest" ? browserProfilePartitionTarget(cfg, profile) : null;
|
|
538
569
|
const partitionId = profile === "guest" ? "guest" : (profileTarget?.partitionId ?? "");
|
|
539
|
-
const session =
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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
|
+
} };
|
|
550
593
|
}
|
|
551
594
|
let engineUnavailableLogged = false;
|
|
552
595
|
let browserEngineInstall = null;
|
|
@@ -718,6 +761,9 @@ function checkedModelSelection(raw, current, requireAvailableModel = false) {
|
|
|
718
761
|
return { ok: false, status: 409, error: "the bot is working — stop it before changing models" };
|
|
719
762
|
}
|
|
720
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
|
+
}
|
|
721
767
|
// Model IDs remain free-form at the app's general API boundary. Custom
|
|
722
768
|
// engines can accept IDs that are not in their discovery catalog, and
|
|
723
769
|
// several drivers only learn the final catalog when a turn starts. The
|
|
@@ -3434,6 +3480,9 @@ async function startTurn(botId, text, opts) {
|
|
|
3434
3480
|
}
|
|
3435
3481
|
console.error(`[omb-turn] bot=${botId} text=${JSON.stringify(resolvedImages.text.slice(0, 70))} images=${turnImages.length} depth=${commsDepth} card=${Boolean(opts?.cardContinuation)}`);
|
|
3436
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
|
+
}
|
|
3437
3486
|
const model = opts?.runOn === "cloud" ? instance.models.default : bot.modelSelection.model;
|
|
3438
3487
|
// a cloud routine borrows the instance default model, so it borrows no
|
|
3439
3488
|
// per-bot effort either
|
|
@@ -3877,7 +3926,7 @@ async function startTurn(botId, text, opts) {
|
|
|
3877
3926
|
liveBot.browser !== false &&
|
|
3878
3927
|
instance.adapter.capabilities.browserMcp === true) {
|
|
3879
3928
|
const selectedProfile = liveBot.browserProfile;
|
|
3880
|
-
browser = browserIntegration(bot.id, selectedProfile);
|
|
3929
|
+
browser = await browserIntegration(bot.id, selectedProfile, { threadId, generation: dispatchClaimId });
|
|
3881
3930
|
if (browser)
|
|
3882
3931
|
integrations.browser = browser.integration;
|
|
3883
3932
|
// The browser lost its frame source when the Electron surface was
|
|
@@ -3887,8 +3936,9 @@ async function startTurn(botId, text, opts) {
|
|
|
3887
3936
|
// the work was a web page, because agent-browser runs its own headless
|
|
3888
3937
|
// Chrome on the host rather than inside that desktop.
|
|
3889
3938
|
if (browser) {
|
|
3890
|
-
const frame = { binaryPath: browser.
|
|
3891
|
-
|
|
3939
|
+
const frame = { binaryPath: browser.spec.command, env: browser.spec.env };
|
|
3940
|
+
const session = browser.session;
|
|
3941
|
+
browserCapture = () => browserRuntime.withAgentAction(session, () => agentBrowserFrame(frame));
|
|
3892
3942
|
}
|
|
3893
3943
|
}
|
|
3894
3944
|
// A cancelled adapter can be between accepting sendTurn and revealing
|
|
@@ -4619,6 +4669,10 @@ setupRetry = 0) {
|
|
|
4619
4669
|
const preparedComposio = bot.composio;
|
|
4620
4670
|
const instance = registry.get(bot.modelSelection.instanceId);
|
|
4621
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
|
+
}
|
|
4622
4676
|
if (!instance) {
|
|
4623
4677
|
const message = `${bot.name}'s model is unavailable`;
|
|
4624
4678
|
store.appendMessage(threadId, {
|
|
@@ -4801,6 +4855,11 @@ setupRetry = 0) {
|
|
|
4801
4855
|
return true;
|
|
4802
4856
|
}
|
|
4803
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 });
|
|
4804
4863
|
orchestration?.onClaimed?.();
|
|
4805
4864
|
// Connected-app discovery above can yield for a network round trip. A
|
|
4806
4865
|
// profile may be removed, or the browser feature switched off, during that
|
|
@@ -4811,24 +4870,19 @@ setupRetry = 0) {
|
|
|
4811
4870
|
readyBot.browser !== false &&
|
|
4812
4871
|
instance.adapter.capabilities.browserMcp === true) {
|
|
4813
4872
|
const selectedProfile = readyBot.browserProfile;
|
|
4814
|
-
const browser = browserIntegration(readyBot.id, selectedProfile);
|
|
4873
|
+
const browser = await browserIntegration(readyBot.id, selectedProfile, { threadId, generation: internalGeneration });
|
|
4815
4874
|
if (browser)
|
|
4816
4875
|
integrations.browser = browser.integration;
|
|
4817
4876
|
}
|
|
4818
|
-
// Stop/delete may land while
|
|
4819
|
-
//
|
|
4820
|
-
//
|
|
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.
|
|
4821
4880
|
const browserReadyBot = store.bot(readyBot.id);
|
|
4822
|
-
if (isCancelled?.() || !browserReadyBot ||
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
retryDelegationsWaitingOn(browserReadyBot.id);
|
|
4826
|
-
}
|
|
4881
|
+
if (isCancelled?.() || !browserReadyBot?.busy ||
|
|
4882
|
+
groupSpeakers.get(threadId) !== roomSpeaker ||
|
|
4883
|
+
activeInternalGenerationByThread.get(threadId) !== internalGeneration) {
|
|
4827
4884
|
return false;
|
|
4828
4885
|
}
|
|
4829
|
-
store.patchGroup(readyGroup.id, { busyBotId: bot.id }); // the store's change stream carries the frame
|
|
4830
|
-
roomSpeaker = { botId: bot.id, name: bot.name, color: bot.color };
|
|
4831
|
-
groupSpeakers.set(threadId, roomSpeaker);
|
|
4832
4886
|
// Room and Goal turns use the speaker's desktop, never the coordinator's.
|
|
4833
4887
|
// Claim the same lease as direct turns before asynchronous VM setup.
|
|
4834
4888
|
if (readyBot.computer === "vm") {
|
|
@@ -6594,6 +6648,41 @@ function persistMcpServers(next) {
|
|
|
6594
6648
|
// when an empty section is omitted by an older config file.
|
|
6595
6649
|
cfg.mcpServers = next;
|
|
6596
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
|
+
}
|
|
6597
6686
|
/** Rebuild the provider fleet after a config change so new keys take
|
|
6598
6687
|
* effect without a server restart (kills any in-flight turns). */
|
|
6599
6688
|
async function reloadProviders() {
|
|
@@ -6634,6 +6723,7 @@ async function reloadProviders() {
|
|
|
6634
6723
|
// and reload sequence single-flight so two settings requests cannot drop one
|
|
6635
6724
|
// another's changes or dispose a fleet while another reload is creating it.
|
|
6636
6725
|
let providerConfigBusy = false;
|
|
6726
|
+
const providerInstancesChanging = new Set();
|
|
6637
6727
|
let mcpConfigBusy = false;
|
|
6638
6728
|
const MAX_CONCURRENT_MCP_PROBES = 2;
|
|
6639
6729
|
let mcpProbesInFlight = 0;
|
|
@@ -6944,11 +7034,13 @@ const handleRequest = async (req, res) => {
|
|
|
6944
7034
|
if (!internalSender) {
|
|
6945
7035
|
return json(res, 401, { error: "unauthorized" });
|
|
6946
7036
|
}
|
|
6947
|
-
const requiredCapabilityKind = path
|
|
6948
|
-
? "
|
|
6949
|
-
: path
|
|
6950
|
-
? "
|
|
6951
|
-
: "
|
|
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";
|
|
6952
7044
|
if (internalCapability.kind !== requiredCapabilityKind) {
|
|
6953
7045
|
return json(res, 403, { error: "this internal capability cannot access that service" });
|
|
6954
7046
|
}
|
|
@@ -6992,6 +7084,30 @@ const handleRequest = async (req, res) => {
|
|
|
6992
7084
|
throw Object.assign(new Error("the internal turn capability has expired"), { status: 401 });
|
|
6993
7085
|
}
|
|
6994
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
|
+
}
|
|
6995
7111
|
if (method === "GET" && path === "/api/internal/agents") {
|
|
6996
7112
|
const sender = internalSender;
|
|
6997
7113
|
// title/description included so the caller can judge the team (who
|
|
@@ -8084,6 +8200,45 @@ const handleRequest = async (req, res) => {
|
|
|
8084
8200
|
: json(res, 404, { error: "no such webhook" });
|
|
8085
8201
|
}
|
|
8086
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
|
+
}
|
|
8087
8242
|
if (method === "GET" && path === "/api/events") {
|
|
8088
8243
|
const client = {
|
|
8089
8244
|
res,
|
|
@@ -9472,10 +9627,7 @@ const handleRequest = async (req, res) => {
|
|
|
9472
9627
|
if (!bot) {
|
|
9473
9628
|
// There are no awaits between the refreshed lookup and this patch, but
|
|
9474
9629
|
// keep the attachment invariant explicit if the store ever changes.
|
|
9475
|
-
|
|
9476
|
-
unlinkSync(saved.path);
|
|
9477
|
-
}
|
|
9478
|
-
catch { }
|
|
9630
|
+
deleteAttachment(saved.path);
|
|
9479
9631
|
return json(res, 404, { error: "no such bot" });
|
|
9480
9632
|
}
|
|
9481
9633
|
const visible = wireBot(bot);
|
|
@@ -9581,6 +9733,8 @@ const handleRequest = async (req, res) => {
|
|
|
9581
9733
|
if (body.requireAvailableModel !== undefined && typeof body.requireAvailableModel !== "boolean") {
|
|
9582
9734
|
return json(res, 400, { error: "requireAvailableModel must be true or false" });
|
|
9583
9735
|
}
|
|
9736
|
+
const beforeBrowserProfile = existingBot?.browserProfile;
|
|
9737
|
+
const beforeBrowserEnabled = existingBot?.browser;
|
|
9584
9738
|
// Neither Codex (free-form string field) nor Grok (lazy, logs-only)
|
|
9585
9739
|
// rejects an unknown effort level at their own boundary — this is the
|
|
9586
9740
|
// only real gate, so it stays. But it fires only when the target
|
|
@@ -9697,6 +9851,10 @@ const handleRequest = async (req, res) => {
|
|
|
9697
9851
|
if (existingBot?.busy && requestedProfile !== existingBot.browserProfile) {
|
|
9698
9852
|
return json(res, 409, { error: "stop this bot's turn before changing its browser profile" });
|
|
9699
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
|
+
}
|
|
9700
9858
|
if (requestedProfile === undefined)
|
|
9701
9859
|
patch.browserProfile = undefined;
|
|
9702
9860
|
else if (typeof requestedProfile === "string" &&
|
|
@@ -9909,6 +10067,12 @@ const handleRequest = async (req, res) => {
|
|
|
9909
10067
|
section !== undefined &&
|
|
9910
10068
|
sectionKey(existingBot?.section) !== sectionKey(section);
|
|
9911
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
|
+
}
|
|
9912
10076
|
if (profile.patch.soul !== undefined) {
|
|
9913
10077
|
// A mixed settings request must not turn a runtime revocation into
|
|
9914
10078
|
// a persist-first edit. Apply runtime fields with their existing
|
|
@@ -9925,6 +10089,12 @@ const handleRequest = async (req, res) => {
|
|
|
9925
10089
|
}
|
|
9926
10090
|
if (!bot)
|
|
9927
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
|
+
}
|
|
9928
10098
|
const chiefChanges = body.chiefOfStaff === true || chiefMovedSections
|
|
9929
10099
|
? store.setChiefOfStaff(bot.id)
|
|
9930
10100
|
: [];
|
|
@@ -10121,6 +10291,8 @@ const handleRequest = async (req, res) => {
|
|
|
10121
10291
|
localVmIdles.get(target.key)?.cancel();
|
|
10122
10292
|
localVmIdles.delete(target.key);
|
|
10123
10293
|
store.deleteBot(bot.id);
|
|
10294
|
+
browserLive.closeForBot(bot.id);
|
|
10295
|
+
await forgetTemporaryBrowser(bot.id);
|
|
10124
10296
|
}
|
|
10125
10297
|
catch (error) {
|
|
10126
10298
|
if (browserCleanupRequest)
|
|
@@ -11239,7 +11411,26 @@ const handleRequest = async (req, res) => {
|
|
|
11239
11411
|
// Windows never pushes PATH changes into a live process, so without
|
|
11240
11412
|
// this the answer is frozen at boot and "check again" is a no-op.
|
|
11241
11413
|
resetPathCache();
|
|
11242
|
-
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
|
+
}
|
|
11243
11434
|
}
|
|
11244
11435
|
const authStatus = /^\/api\/instances\/([\w.-]+)\/auth\/status$/.exec(path);
|
|
11245
11436
|
if (method === "GET" && authStatus) {
|
|
@@ -11261,12 +11452,12 @@ const handleRequest = async (req, res) => {
|
|
|
11261
11452
|
if (action === "refresh-models") {
|
|
11262
11453
|
if (!(await registry.refreshModels(instanceId)))
|
|
11263
11454
|
return json(res, 404, { error: "unknown instance" });
|
|
11264
|
-
return json(res, 200, { instances: await
|
|
11455
|
+
return json(res, 200, { instances: await describeInstances() });
|
|
11265
11456
|
}
|
|
11266
11457
|
if (action === "install") {
|
|
11267
11458
|
if (!(await registry.installRuntime(instanceId)))
|
|
11268
11459
|
return json(res, 404, { error: "managed installation is unavailable" });
|
|
11269
|
-
return json(res, 200, { instances: await
|
|
11460
|
+
return json(res, 200, { instances: await describeInstances() });
|
|
11270
11461
|
}
|
|
11271
11462
|
if (action === "auth/start") {
|
|
11272
11463
|
const instance = registry.get(instanceId);
|
|
@@ -11372,38 +11563,80 @@ const handleRequest = async (req, res) => {
|
|
|
11372
11563
|
}
|
|
11373
11564
|
// ── per-instance CLI path override (custom builds / versioned bins) ──
|
|
11374
11565
|
// PATCH /api/instances/:id {cli: "/path/to/cli" | ""} — "" reverts to the
|
|
11375
|
-
// driver default.
|
|
11566
|
+
// driver default. Only this idle instance is replaced; siblings keep running.
|
|
11376
11567
|
const instancePatch = /^\/api\/instances\/([\w.-]+)$/.exec(path);
|
|
11377
11568
|
if (method === "PATCH" && instancePatch) {
|
|
11378
11569
|
// same non-simple-request gate as the local-VM lifecycle routes
|
|
11379
11570
|
if (!String(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) {
|
|
11380
11571
|
return json(res, 415, { error: "content-type must be application/json" });
|
|
11381
11572
|
}
|
|
11382
|
-
const
|
|
11383
|
-
if (
|
|
11384
|
-
return json(res, 400, { error: "
|
|
11385
|
-
|
|
11386
|
-
|
|
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];
|
|
11387
11578
|
if (providerConfigBusy)
|
|
11388
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
|
+
}
|
|
11389
11583
|
providerConfigBusy = true;
|
|
11584
|
+
providerInstancesChanging.add(instanceId);
|
|
11390
11585
|
try {
|
|
11391
|
-
const result = withInstanceCli(cfg,
|
|
11392
|
-
|
|
11393
|
-
|
|
11394
|
-
|
|
11395
|
-
|
|
11396
|
-
|
|
11397
|
-
|
|
11398
|
-
|
|
11399
|
-
|
|
11400
|
-
|
|
11401
|
-
|
|
11402
|
-
|
|
11403
|
-
|
|
11404
|
-
|
|
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() });
|
|
11405
11637
|
}
|
|
11406
11638
|
finally {
|
|
11639
|
+
providerInstancesChanging.delete(instanceId);
|
|
11407
11640
|
providerConfigBusy = false;
|
|
11408
11641
|
}
|
|
11409
11642
|
}
|
|
@@ -11520,12 +11753,24 @@ const handleRequest = async (req, res) => {
|
|
|
11520
11753
|
return json(res, 400, { error: "nothing to save" });
|
|
11521
11754
|
if (providerConfigBusy)
|
|
11522
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
|
+
}
|
|
11523
11762
|
const disablingBuiltInBrowser = patch.features?.browser === false && builtInBrowserEnabled(cfg);
|
|
11524
11763
|
const removedBrowserProfileIds = patch.browserProfiles === undefined
|
|
11525
11764
|
? []
|
|
11526
11765
|
: (cfg.browserProfiles ?? [])
|
|
11527
11766
|
.map((profile) => profile.id)
|
|
11528
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." });
|
|
11529
11774
|
if (patch.browserProfiles !== undefined) {
|
|
11530
11775
|
const currentProfiles = new Map((cfg.browserProfiles ?? []).map((profile) => [profile.id, profile]));
|
|
11531
11776
|
const nextProfiles = patch.browserProfiles.map((profile) => {
|
|
@@ -11754,6 +11999,8 @@ const handleRequest = async (req, res) => {
|
|
|
11754
11999
|
return json(res, 409, { error: conflict });
|
|
11755
12000
|
}
|
|
11756
12001
|
const browserCleanupRequests = [];
|
|
12002
|
+
if (profileControlConflict())
|
|
12003
|
+
return json(res, 409, { error: "Release browser control before deleting its profile." });
|
|
11757
12004
|
try {
|
|
11758
12005
|
for (const profileId of removedBrowserProfileIds) {
|
|
11759
12006
|
const target = browserProfilePartitionTarget(cfg, profileId);
|
|
@@ -11818,6 +12065,12 @@ const handleRequest = async (req, res) => {
|
|
|
11818
12065
|
throw error;
|
|
11819
12066
|
}
|
|
11820
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
|
+
}
|
|
11821
12074
|
if (patch.browserProfiles !== undefined) {
|
|
11822
12075
|
const retained = new Set(patch.browserProfiles.map((profile) => profile.id));
|
|
11823
12076
|
try {
|
|
@@ -12310,6 +12563,21 @@ calendarCalls.start();
|
|
|
12310
12563
|
// Resolve the edition before accepting requests so /api/edition is never a guess.
|
|
12311
12564
|
console.log(describeEdition(await loadEnterpriseLayer()));
|
|
12312
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
|
+
}
|
|
12313
12581
|
server.listen(PORT, "127.0.0.1", () => {
|
|
12314
12582
|
console.log(`openmausbot server on http://127.0.0.1:${PORT}`);
|
|
12315
12583
|
});
|
|
@@ -12335,6 +12603,7 @@ const gracefulShutdown = createGracefulShutdown({
|
|
|
12335
12603
|
// asynchronous shutdown jobs drain. Invalidate their turn bearers before
|
|
12336
12604
|
// any cleanup function reaches an await.
|
|
12337
12605
|
revokeAllInternalCapabilities();
|
|
12606
|
+
browserLive.closeAll();
|
|
12338
12607
|
for (const idle of localVmIdles.values())
|
|
12339
12608
|
idle.cancel();
|
|
12340
12609
|
vps.closeAllVpsDesktopTunnels();
|
|
@@ -12345,6 +12614,10 @@ const gracefulShutdown = createGracefulShutdown({
|
|
|
12345
12614
|
tunnelListener?.close();
|
|
12346
12615
|
},
|
|
12347
12616
|
() => registry.disposeAll(),
|
|
12617
|
+
async () => {
|
|
12618
|
+
await Promise.all([...temporaryBrowserSessions.keys()].map((botId) => forgetTemporaryBrowser(botId)));
|
|
12619
|
+
await browserRuntime.closeAll();
|
|
12620
|
+
},
|
|
12348
12621
|
() => flushAllProfileHistory(),
|
|
12349
12622
|
],
|
|
12350
12623
|
// Cleanup jobs run concurrently. Release only after they settle (or reach
|