openmausbot 0.1.58 → 0.1.60
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-CE7W1AIk.js → index-BIzf_lw1.js} +1 -1
- package/dist/assets/index-Tha1umgl.js +264 -0
- package/dist/assets/index-oPMv6a_k.css +1 -0
- package/dist/index.html +2 -2
- package/dist-server/container-mcp.js +1 -2
- package/dist-server/index.js +736 -662
- package/dist-server/proxy-paths.js +0 -1
- package/dist-server/server/auto-approve.js +49 -8
- package/dist-server/server/browser-engine.js +31 -0
- package/dist-server/server/composio.js +43 -21
- package/dist-server/server/container-computer.js +32 -7
- package/dist-server/server/drivers/antigravity-acp.js +1 -2
- package/dist-server/server/drivers/antigravity-runtime.js +2 -2
- package/dist-server/server/drivers/codex-instructions.js +66 -0
- package/dist-server/server/drivers/codex.js +84 -49
- package/dist-server/server/index.js +208 -222
- package/dist-server/server/procs.js +34 -22
- package/dist-server/server/proxy-paths.js +0 -1
- package/dist-server/shared/approval-mode.js +3 -2
- package/dist-server/vps-container-mcp.js +1 -2
- package/package.json +1 -1
- package/dist/assets/index-B433sv8K.css +0 -1
- package/dist/assets/index-r-WPT-Pu.js +0 -264
- package/dist-server/drivers/browser-proxy.js +0 -15037
- package/dist-server/server/browser-connection.js +0 -187
- package/dist-server/server/drivers/browser-proxy.js +0 -520
|
@@ -11,7 +11,7 @@ import { BOT_PROFILE_LIMITS } from "../shared/bot-profile.js";
|
|
|
11
11
|
import { approvalModeFor, supportsApprovalMode, requiresNativeApproval, isEmergencyApprovalDowngrade, isApprovalMode, } from "../shared/approval-mode.js";
|
|
12
12
|
import { escapeAttribute } from "../src/lib/composer-attachments.js";
|
|
13
13
|
import { CREDENTIAL_TARGETS, credentialResumeOutcome, credentialIsConfigured, isReusableCredentialRequest, isCredentialTargetId, } from "../shared/credential-request.js";
|
|
14
|
-
import { approvalHeldReason, approvalModeForOrigin, autoVerdict, rememberableApprovalKey } from "./auto-approve.js";
|
|
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
17
|
import { BrowserCleanupCoordinator, finalizeBrowserCleanupMutation, requireBrowserCleanupAcknowledged, } from "./browser-lifecycle-cleanup.js";
|
|
@@ -84,8 +84,8 @@ import { redactSecretsInText } from "./redact.js";
|
|
|
84
84
|
import * as vps from "./vps-computer.js";
|
|
85
85
|
import { RoutineManager } from "./routines.js";
|
|
86
86
|
import { CalendarCallManager } from "./calendar-calls.js";
|
|
87
|
-
import { BUILT_IN_BROWSER_SYSTEM_PROMPT
|
|
88
|
-
import { agentBrowserIntegration, browserEngineEncryptionKey, browserEngineStatus, browserSessionId, describeBrowserEngine, } from "./browser-engine.js";
|
|
87
|
+
import { BUILT_IN_BROWSER_SYSTEM_PROMPT } from "./browser-engine.js";
|
|
88
|
+
import { agentBrowserIntegration, browserEngineEncryptionKey, clearBrowserSessionState, ensureChrome, installAgentBrowserBinary, resolveAgentBrowserBinary, browserEngineStatus, browserSessionId, describeBrowserEngine, } from "./browser-engine.js";
|
|
89
89
|
import { captureOutsideHumanControl } from "./private-screen-capture.js";
|
|
90
90
|
import { screenFrameHash, screenTouchingTool, settledFrameIsNews } from "./screen-frame-gate.js";
|
|
91
91
|
import { RoutineRequestService } from "./routine-requests.js";
|
|
@@ -203,9 +203,40 @@ function applyDesktopMutationTokenMessage(raw) {
|
|
|
203
203
|
}
|
|
204
204
|
return true;
|
|
205
205
|
}
|
|
206
|
+
// Browser data of a deleted bot or profile: the engine's saved session
|
|
207
|
+
// state, cleared here on every host (the desktop no longer owns a browser).
|
|
208
|
+
// The coordinator keeps its durable journal and replay; this is its outbox.
|
|
206
209
|
const browserCleanup = new BrowserCleanupCoordinator({
|
|
207
210
|
file: join(DATA_DIR, "browser-cleanups.json"),
|
|
208
|
-
send:
|
|
211
|
+
send: (request) => {
|
|
212
|
+
const status = browserEngineStatus();
|
|
213
|
+
// Guest sessions are throwaway and never saved, so only the bot's own
|
|
214
|
+
// session and shared profile sessions have state to clear.
|
|
215
|
+
const sessions = request.type === "openmausbot:browser-bot-deleted" && request.botId
|
|
216
|
+
? [browserSessionId(request.botId, "")]
|
|
217
|
+
: request.partitionId
|
|
218
|
+
? [browserSessionId("", request.partitionId)]
|
|
219
|
+
: [];
|
|
220
|
+
// Failed erasure leaves the committed intent pending for retry; it must
|
|
221
|
+
// never acknowledge that saved state was removed when it was not.
|
|
222
|
+
const work = status.kind === "ready" && sessions.length
|
|
223
|
+
? Promise.all(sessions.map(async (session) => {
|
|
224
|
+
const ok = await clearBrowserSessionState(status.binaryPath, session, { encryptionKey: browserEngineEncryptionKey() });
|
|
225
|
+
if (!ok)
|
|
226
|
+
console.warn(`browser cleanup: could not clear the engine's saved state for session ${session}; clear it with \`agent-browser --session ${session} state clear --all\``);
|
|
227
|
+
return ok;
|
|
228
|
+
}))
|
|
229
|
+
: Promise.resolve([true]);
|
|
230
|
+
void work.then((results) => results.every(Boolean), (error) => {
|
|
231
|
+
console.warn("browser cleanup: could not clear saved session state", error);
|
|
232
|
+
return false;
|
|
233
|
+
}).then((ok) => {
|
|
234
|
+
browserCleanup.receive({ type: "openmausbot:browser-lifecycle-result", requestId: request.requestId, ok });
|
|
235
|
+
}).catch((error) => {
|
|
236
|
+
console.warn("browser cleanup: could not acknowledge cleanup", error);
|
|
237
|
+
});
|
|
238
|
+
return true;
|
|
239
|
+
},
|
|
209
240
|
});
|
|
210
241
|
const phoneSecrets = new PhoneSecretBridge(postDesktopPrivateMessage);
|
|
211
242
|
utilityParentPort?.on("message", (event) => {
|
|
@@ -219,8 +250,7 @@ utilityParentPort?.on("message", (event) => {
|
|
|
219
250
|
return;
|
|
220
251
|
if (phoneSecrets.receive(message))
|
|
221
252
|
return;
|
|
222
|
-
|
|
223
|
-
composio.applyManagedBrokerMessage(message);
|
|
253
|
+
composio.applyManagedBrokerMessage(message);
|
|
224
254
|
}
|
|
225
255
|
catch (error) {
|
|
226
256
|
console.error(`[desktop-sync] rejected private parent message: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -300,10 +330,8 @@ function revokeInternalCapabilityForProviderEvent(event) {
|
|
|
300
330
|
* length; only capabilities for currently active turns are retained. */
|
|
301
331
|
function authorizedInternalCapability(header) {
|
|
302
332
|
const got = Buffer.from(Array.isArray(header) ? "" : (header ?? ""));
|
|
303
|
-
const now = Date.now();
|
|
304
333
|
for (const [token, capability] of internalCapabilities) {
|
|
305
|
-
if (capability
|
|
306
|
-
activeInternalGenerationByThread.get(capability.threadId) !== capability.generation) {
|
|
334
|
+
if (!internalCapabilityIsActive(capability)) {
|
|
307
335
|
internalCapabilities.delete(token);
|
|
308
336
|
continue;
|
|
309
337
|
}
|
|
@@ -314,6 +342,12 @@ function authorizedInternalCapability(header) {
|
|
|
314
342
|
return null;
|
|
315
343
|
}
|
|
316
344
|
function internalCapabilityIsActive(capability) {
|
|
345
|
+
if (capability.localVmTarget) {
|
|
346
|
+
const owner = localVmLeaseFor(capability.localVmTarget).current(localVmOwnerBusy);
|
|
347
|
+
if (localVmThreadTargets.get(capability.threadId) !== capability.localVmTarget ||
|
|
348
|
+
owner?.threadId !== capability.threadId || owner.botId !== capability.botId)
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
317
351
|
return (capability.orphanExpiresAt > Date.now() &&
|
|
318
352
|
activeInternalGenerationByThread.get(capability.threadId) === capability.generation);
|
|
319
353
|
}
|
|
@@ -368,84 +402,6 @@ function agentsIntegration(botId, threadId, depth, skillAuthoring, generation) {
|
|
|
368
402
|
},
|
|
369
403
|
};
|
|
370
404
|
}
|
|
371
|
-
const browserCapabilitiesByThread = new Map();
|
|
372
|
-
const pendingBrowserCapabilityRevocations = new Map();
|
|
373
|
-
const BROWSER_REVOCATION_RETRY_MS = [250, 1_000, 3_000, 10_000, 30_000];
|
|
374
|
-
async function revokeReleasedBrowserCapability(active, attempt = 0) {
|
|
375
|
-
const token = active.capability.token;
|
|
376
|
-
try {
|
|
377
|
-
await revokeBrowserCapability(active.connection, active.capability);
|
|
378
|
-
const pending = pendingBrowserCapabilityRevocations.get(token);
|
|
379
|
-
if (pending)
|
|
380
|
-
clearTimeout(pending.timer);
|
|
381
|
-
pendingBrowserCapabilityRevocations.delete(token);
|
|
382
|
-
}
|
|
383
|
-
catch (error) {
|
|
384
|
-
if (Date.now() >= active.capability.expiresAt) {
|
|
385
|
-
pendingBrowserCapabilityRevocations.delete(token);
|
|
386
|
-
return;
|
|
387
|
-
}
|
|
388
|
-
if (attempt === 0) {
|
|
389
|
-
console.error(`[browser] could not revoke turn capability; retrying until its absolute expiry: ${error instanceof Error ? error.message : String(error)}`);
|
|
390
|
-
}
|
|
391
|
-
const delay = Math.min(BROWSER_REVOCATION_RETRY_MS[Math.min(attempt, BROWSER_REVOCATION_RETRY_MS.length - 1)], Math.max(1, active.capability.expiresAt - Date.now()));
|
|
392
|
-
const timer = setTimeout(() => {
|
|
393
|
-
const pending = pendingBrowserCapabilityRevocations.get(token);
|
|
394
|
-
if (!pending || pending.timer !== timer)
|
|
395
|
-
return;
|
|
396
|
-
void revokeReleasedBrowserCapability(active, attempt + 1);
|
|
397
|
-
}, delay);
|
|
398
|
-
timer.unref?.();
|
|
399
|
-
const previous = pendingBrowserCapabilityRevocations.get(token);
|
|
400
|
-
if (previous)
|
|
401
|
-
clearTimeout(previous.timer);
|
|
402
|
-
pendingBrowserCapabilityRevocations.set(token, { active, attempt: attempt + 1, timer });
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
async function releaseBrowserCapabilityForThread(threadId, expectedOwnerId) {
|
|
406
|
-
const active = browserCapabilitiesByThread.get(threadId);
|
|
407
|
-
if (!active || (expectedOwnerId !== undefined && active.ownerId !== expectedOwnerId))
|
|
408
|
-
return;
|
|
409
|
-
browserCapabilitiesByThread.delete(threadId);
|
|
410
|
-
await revokeReleasedBrowserCapability(active);
|
|
411
|
-
}
|
|
412
|
-
async function releaseBrowserCapabilitiesForBot(botId) {
|
|
413
|
-
const threads = [...browserCapabilitiesByThread]
|
|
414
|
-
.filter(([, active]) => active.botId === botId)
|
|
415
|
-
.map(([threadId]) => threadId);
|
|
416
|
-
await Promise.all(threads.map((threadId) => releaseBrowserCapabilityForThread(threadId)));
|
|
417
|
-
}
|
|
418
|
-
async function releaseAllBrowserCapabilities() {
|
|
419
|
-
const active = [...browserCapabilitiesByThread.values()];
|
|
420
|
-
browserCapabilitiesByThread.clear();
|
|
421
|
-
const connections = new Map();
|
|
422
|
-
for (const entry of active) {
|
|
423
|
-
connections.set(`${entry.connection.url}:${entry.connection.token}`, entry.connection);
|
|
424
|
-
}
|
|
425
|
-
for (const pending of pendingBrowserCapabilityRevocations.values()) {
|
|
426
|
-
connections.set(`${pending.active.connection.url}:${pending.active.connection.token}`, pending.active.connection);
|
|
427
|
-
}
|
|
428
|
-
await Promise.all([...connections.values()].map(async (connection) => {
|
|
429
|
-
try {
|
|
430
|
-
// Master clear is atomic at the host. It also invalidates a token whose
|
|
431
|
-
// earlier per-turn revoke timed out, which is essential for feature-off
|
|
432
|
-
// and graceful-shutdown boundaries.
|
|
433
|
-
await clearBrowserCapabilities(connection);
|
|
434
|
-
for (const [token, pending] of pendingBrowserCapabilityRevocations) {
|
|
435
|
-
if (pending.active.connection.url === connection.url &&
|
|
436
|
-
pending.active.connection.token === connection.token) {
|
|
437
|
-
clearTimeout(pending.timer);
|
|
438
|
-
pendingBrowserCapabilityRevocations.delete(token);
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
catch {
|
|
443
|
-
await Promise.all(active
|
|
444
|
-
.filter((entry) => entry.connection.url === connection.url && entry.connection.token === connection.token)
|
|
445
|
-
.map((entry) => revokeReleasedBrowserCapability(entry)));
|
|
446
|
-
}
|
|
447
|
-
}));
|
|
448
|
-
}
|
|
449
405
|
class DirectTurnSetupCancelled extends Error {
|
|
450
406
|
}
|
|
451
407
|
const directTurnDispatchClaims = new Map();
|
|
@@ -540,55 +496,10 @@ function cancelDirectTurnDispatch(botId, expectedThreadId) {
|
|
|
540
496
|
// start early enough for those old events to settle the replacement.
|
|
541
497
|
return claim;
|
|
542
498
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
const control = controlIntegration(botId, threadId, generation);
|
|
548
|
-
// A profile that no longer exists falls back to the bot's own session.
|
|
549
|
-
// Canonical ids belong to config/bot references; Electron must receive the
|
|
550
|
-
// exact immutable partition inherited from #567 so an upgrade cannot move
|
|
551
|
-
// a bot into another account. Guest remains a throwaway partition.
|
|
552
|
-
const profileTarget = profile && profile !== "guest"
|
|
553
|
-
? browserProfilePartitionTarget(cfg, profile)
|
|
554
|
-
: null;
|
|
555
|
-
const partitionId = profile === "guest" ? "guest" : (profileTarget?.partitionId ?? "");
|
|
556
|
-
await releaseBrowserCapabilityForThread(threadId);
|
|
557
|
-
const capability = await registerBrowserCapability(connection, botId, partitionId);
|
|
558
|
-
const active = { botId, ownerId, connection, capability };
|
|
559
|
-
// Registration crosses a process boundary. Stop/delete/config changes can
|
|
560
|
-
// land while the desktop host is minting the token; revalidate in the same
|
|
561
|
-
// event-loop turn that publishes it. If ownership was lost, no agent ever
|
|
562
|
-
// receives the bearer and the just-created token is revoked immediately.
|
|
563
|
-
if (!stillValid()) {
|
|
564
|
-
await revokeReleasedBrowserCapability(active);
|
|
565
|
-
return null;
|
|
566
|
-
}
|
|
567
|
-
browserCapabilitiesByThread.set(threadId, active);
|
|
568
|
-
return {
|
|
569
|
-
connection,
|
|
570
|
-
capability,
|
|
571
|
-
profile: partitionId,
|
|
572
|
-
integration: {
|
|
573
|
-
command: process.execPath,
|
|
574
|
-
args: [SPAWNED_PROXIES.browser],
|
|
575
|
-
env: {
|
|
576
|
-
...AGENTS_NODE_FLAG,
|
|
577
|
-
OMB_BROWSER_URL: connection.url,
|
|
578
|
-
OMB_BROWSER_TOKEN: capability.token,
|
|
579
|
-
OMB_BROWSER_PROFILE: partitionId,
|
|
580
|
-
OMB_BOT_ID: botId,
|
|
581
|
-
OMB_CONTROL_URL: control.url,
|
|
582
|
-
OMB_CONTROL_TOKEN: control.token,
|
|
583
|
-
},
|
|
584
|
-
},
|
|
585
|
-
};
|
|
586
|
-
}
|
|
587
|
-
/** No desktop surface (a server, or a Windows desktop): the bot's browser is
|
|
588
|
-
* agent-browser, one isolated session per profile or per bot
|
|
589
|
-
* (docs/plans/browser-engine.md). Returns null, with the reason logged once,
|
|
590
|
-
* when the engine is not on this machine. */
|
|
591
|
-
function engineBrowserIntegration(botId, profile) {
|
|
499
|
+
/** The bot's browser for this turn: agent-browser, one isolated session per
|
|
500
|
+
* browser profile or per bot (docs/plans/browser-engine.md). Null, with the
|
|
501
|
+
* reason logged once, when the engine is not on this machine. */
|
|
502
|
+
function browserIntegration(botId, profile) {
|
|
592
503
|
const status = browserEngineStatus();
|
|
593
504
|
if (status.kind !== "ready") {
|
|
594
505
|
if (!engineUnavailableLogged) {
|
|
@@ -597,12 +508,11 @@ function engineBrowserIntegration(botId, profile) {
|
|
|
597
508
|
}
|
|
598
509
|
return null;
|
|
599
510
|
}
|
|
511
|
+
// A profile that no longer exists falls back to the bot's own session.
|
|
600
512
|
const profileTarget = profile && profile !== "guest" ? browserProfilePartitionTarget(cfg, profile) : null;
|
|
601
513
|
const partitionId = profile === "guest" ? "guest" : (profileTarget?.partitionId ?? "");
|
|
602
514
|
const session = browserSessionId(botId, partitionId);
|
|
603
515
|
return {
|
|
604
|
-
connection: null,
|
|
605
|
-
capability: null,
|
|
606
516
|
profile: partitionId,
|
|
607
517
|
integration: agentBrowserIntegration({
|
|
608
518
|
binaryPath: status.binaryPath,
|
|
@@ -613,13 +523,14 @@ function engineBrowserIntegration(botId, profile) {
|
|
|
613
523
|
};
|
|
614
524
|
}
|
|
615
525
|
let engineUnavailableLogged = false;
|
|
526
|
+
let browserEngineInstall = null;
|
|
527
|
+
let browserEngineInstallError = null;
|
|
616
528
|
export function browserEngineSummary() {
|
|
617
|
-
if (availableBrowserConnection())
|
|
618
|
-
return { kind: "desktop" };
|
|
619
529
|
const status = browserEngineStatus();
|
|
530
|
+
const progress = { ...(browserEngineInstall ? { installing: true } : {}), ...(browserEngineInstallError ? { installError: browserEngineInstallError } : {}) };
|
|
620
531
|
return status.kind === "ready"
|
|
621
|
-
? { kind: "
|
|
622
|
-
: { kind: "unavailable", reason: status.reason, installable: status.installable };
|
|
532
|
+
? { kind: "engine", version: status.version, ...progress }
|
|
533
|
+
: { kind: "unavailable", reason: status.reason, installable: status.installable, ...progress };
|
|
623
534
|
}
|
|
624
535
|
function phoneIntegration() {
|
|
625
536
|
const env = { ...AGENTS_NODE_FLAG };
|
|
@@ -680,7 +591,7 @@ const routineRequestEnvelopeSchema = z.discriminatedUnion("action", [
|
|
|
680
591
|
...["pause", "resume", "run_now", "delete"].map((action) => z.object({ ...routineRequestSourceSchema, action: z.literal(action), routineId: z.unknown() }).strict()),
|
|
681
592
|
]);
|
|
682
593
|
/** The loopback endpoint a bot's computer proxy polls before acting. */
|
|
683
|
-
function controlIntegration(botId, threadId, generation) {
|
|
594
|
+
function controlIntegration(botId, threadId, generation, localVmTarget) {
|
|
684
595
|
return {
|
|
685
596
|
url: `http://127.0.0.1:${PORT}/api/internal/computer-control?botId=${encodeURIComponent(botId)}`,
|
|
686
597
|
token: mintInternalCapability({
|
|
@@ -689,6 +600,7 @@ function controlIntegration(botId, threadId, generation) {
|
|
|
689
600
|
generation,
|
|
690
601
|
depth: 0,
|
|
691
602
|
kind: "computer",
|
|
603
|
+
...(localVmTarget ? { localVmTarget } : {}),
|
|
692
604
|
skillAuthoring: false,
|
|
693
605
|
createdBots: 0,
|
|
694
606
|
}),
|
|
@@ -1959,7 +1871,9 @@ const watchdog = new TurnWatchdog({
|
|
|
1959
1871
|
stallMs: TURN_STALL_MS,
|
|
1960
1872
|
checkMs: 60_000,
|
|
1961
1873
|
onStall: (turn) => {
|
|
1962
|
-
|
|
1874
|
+
// Room targets carry an invocation identity; only those claims belong
|
|
1875
|
+
// to the room grace cleanup added here.
|
|
1876
|
+
const stalledVmTarget = groupSpeakers.has(turn.threadId) ? localVmThreadTargets.get(turn.threadId) : undefined;
|
|
1963
1877
|
revokeInternalCapabilitiesForThread(turn.threadId);
|
|
1964
1878
|
repeats.settle(turn.threadId);
|
|
1965
1879
|
const bot = store.bot(turn.botId);
|
|
@@ -1988,6 +1902,9 @@ const watchdog = new TurnWatchdog({
|
|
|
1988
1902
|
retry.unref?.();
|
|
1989
1903
|
return;
|
|
1990
1904
|
}
|
|
1905
|
+
if (stalledVmTarget && localVmThreadTargets.get(turn.threadId) === stalledVmTarget) {
|
|
1906
|
+
releaseLocalVmThread(turn.threadId);
|
|
1907
|
+
}
|
|
1991
1908
|
const group = store.groupByThread(turn.threadId);
|
|
1992
1909
|
const speaker = groupSpeakers.get(turn.threadId);
|
|
1993
1910
|
if (group && group.busyBotId === turn.botId && speaker?.botId === turn.botId) {
|
|
@@ -2083,7 +2000,6 @@ bus.subscribe((event) => {
|
|
|
2083
2000
|
watchdog.setWaitingOnHuman(event.threadId, false);
|
|
2084
2001
|
else if (event.type === "turn.completed") {
|
|
2085
2002
|
watchdog.settle(event.threadId);
|
|
2086
|
-
void releaseBrowserCapabilityForThread(event.threadId);
|
|
2087
2003
|
revokeInternalCapabilityForProviderEvent(event);
|
|
2088
2004
|
}
|
|
2089
2005
|
else if (event.type === "session.exited") {
|
|
@@ -2093,7 +2009,6 @@ bus.subscribe((event) => {
|
|
|
2093
2009
|
const directBotBusy = store.botByThread(event.threadId)?.busy === true;
|
|
2094
2010
|
const roomBusy = Boolean(store.groupByThread(event.threadId)?.busyBotId);
|
|
2095
2011
|
if (!directBotBusy && !roomBusy) {
|
|
2096
|
-
void releaseBrowserCapabilityForThread(event.threadId);
|
|
2097
2012
|
}
|
|
2098
2013
|
}
|
|
2099
2014
|
else
|
|
@@ -2552,18 +2467,23 @@ bus.subscribe((event) => {
|
|
|
2552
2467
|
// the guards in auto-approve.ts; explicitly acknowledged Full does not.
|
|
2553
2468
|
const asker = bot ?? (speaker ? store.bot(speaker.botId) : undefined);
|
|
2554
2469
|
const unattended = permission && asker && event.requestId ? isUnattended(asker.id) : false;
|
|
2470
|
+
const effectiveApprovalMode = asker ? approvalModeForTurn(asker, isInternalTurn(event.threadId)) : "ask";
|
|
2555
2471
|
const verdict = permission && asker && event.requestId
|
|
2556
2472
|
? autoVerdict({
|
|
2557
2473
|
// the same origin the dispatch used, so a peer-started turn's
|
|
2558
2474
|
// residual asks are judged as Approve for me here too
|
|
2559
|
-
approvalMode:
|
|
2475
|
+
approvalMode: effectiveApprovalMode,
|
|
2560
2476
|
autoApprove: false,
|
|
2561
2477
|
alwaysAllow: asker.alwaysAllow,
|
|
2562
2478
|
}, event.tool, event.summary, {
|
|
2563
2479
|
unattended,
|
|
2564
2480
|
scope: event.approvalScope,
|
|
2565
2481
|
requiresExplicitApproval: event.requiresExplicitApproval,
|
|
2566
|
-
|
|
2482
|
+
// Antigravity's residual asks must use a peer-started turn's
|
|
2483
|
+
// effective safe Auto mode, not its durable Full grant.
|
|
2484
|
+
// Preserve the existing policy of every other provider.
|
|
2485
|
+
nativeApproval: requiresNativeApproval(event.provider, event.provider === "antigravityAgent"
|
|
2486
|
+
? effectiveApprovalMode : approvalModeForTurn(asker)),
|
|
2567
2487
|
})
|
|
2568
2488
|
: null;
|
|
2569
2489
|
if (verdict?.approve && asker && event.requestId) {
|
|
@@ -2621,9 +2541,12 @@ bus.subscribe((event) => {
|
|
|
2621
2541
|
scope: event.approvalScope,
|
|
2622
2542
|
requiresExplicitApproval: event.requiresExplicitApproval,
|
|
2623
2543
|
}),
|
|
2624
|
-
held: verdict.source === "full-access"
|
|
2625
|
-
? "
|
|
2626
|
-
: "
|
|
2544
|
+
held: HELD_NOTE[verdict.source === "full-access"
|
|
2545
|
+
? "approval.held.undeliveredFull"
|
|
2546
|
+
: "approval.held.undelivered"],
|
|
2547
|
+
heldCode: verdict.source === "full-access"
|
|
2548
|
+
? "approval.held.undeliveredFull"
|
|
2549
|
+
: "approval.held.undelivered",
|
|
2627
2550
|
approvalScope: event.approvalScope,
|
|
2628
2551
|
},
|
|
2629
2552
|
});
|
|
@@ -2643,6 +2566,18 @@ bus.subscribe((event) => {
|
|
|
2643
2566
|
})();
|
|
2644
2567
|
break;
|
|
2645
2568
|
}
|
|
2569
|
+
// A card can outlive the bot record that raised it. Without one there is
|
|
2570
|
+
// no mode to explain, but the sandbox note still applies.
|
|
2571
|
+
const heldContext = {
|
|
2572
|
+
source: verdict?.source,
|
|
2573
|
+
permission,
|
|
2574
|
+
requiresExplicitApproval: event.requiresExplicitApproval,
|
|
2575
|
+
mode: asker ? approvalModeForOrigin(approvalModeFor(asker), { peerInitiated: isInternalTurn(event.threadId) }) : "ask",
|
|
2576
|
+
unattended: Boolean(unattended),
|
|
2577
|
+
fullAccessAvailable: asker && !isInternalTurn(event.threadId)
|
|
2578
|
+
? supportsApprovalMode(registry.cliTarget(asker.modelSelection.instanceId)?.driverKind, "full")
|
|
2579
|
+
: false,
|
|
2580
|
+
};
|
|
2646
2581
|
const message = pushMessage({
|
|
2647
2582
|
role: "bot",
|
|
2648
2583
|
kind: "options",
|
|
@@ -2665,18 +2600,10 @@ bus.subscribe((event) => {
|
|
|
2665
2600
|
requiresExplicitApproval: event.requiresExplicitApproval,
|
|
2666
2601
|
})
|
|
2667
2602
|
: undefined,
|
|
2668
|
-
//
|
|
2669
|
-
//
|
|
2670
|
-
held: approvalHeldReason(
|
|
2671
|
-
|
|
2672
|
-
permission,
|
|
2673
|
-
requiresExplicitApproval: event.requiresExplicitApproval,
|
|
2674
|
-
mode: asker ? approvalModeForOrigin(approvalModeFor(asker), { peerInitiated: isInternalTurn(event.threadId) }) : "ask",
|
|
2675
|
-
unattended: Boolean(unattended),
|
|
2676
|
-
fullAccessAvailable: asker && !isInternalTurn(event.threadId)
|
|
2677
|
-
? supportsApprovalMode(registry.cliTarget(asker.modelSelection.instanceId)?.driverKind, "full")
|
|
2678
|
-
: false,
|
|
2679
|
-
}),
|
|
2603
|
+
// The text stays for cards saved before heldCode existed, and for
|
|
2604
|
+
// clients that do not know the key yet.
|
|
2605
|
+
held: approvalHeldReason(heldContext),
|
|
2606
|
+
heldCode: approvalHeldNote(heldContext),
|
|
2680
2607
|
approvalScope: event.approvalScope,
|
|
2681
2608
|
},
|
|
2682
2609
|
});
|
|
@@ -3949,13 +3876,7 @@ async function startTurn(botId, text, opts) {
|
|
|
3949
3876
|
liveBot.browser !== false &&
|
|
3950
3877
|
instance.adapter.capabilities.browserMcp === true) {
|
|
3951
3878
|
const selectedProfile = liveBot.browserProfile;
|
|
3952
|
-
browser =
|
|
3953
|
-
const current = store.bot(bot.id);
|
|
3954
|
-
return (directTurnClaimIsCurrent(bot.id, dispatchClaimId, threadId) &&
|
|
3955
|
-
builtInBrowserEnabled(cfg) &&
|
|
3956
|
-
current?.browser !== false &&
|
|
3957
|
-
current?.browserProfile === selectedProfile);
|
|
3958
|
-
}, dispatchClaimId);
|
|
3879
|
+
browser = browserIntegration(bot.id, selectedProfile);
|
|
3959
3880
|
if (browser)
|
|
3960
3881
|
integrations.browser = browser.integration;
|
|
3961
3882
|
}
|
|
@@ -4040,10 +3961,6 @@ async function startTurn(botId, text, opts) {
|
|
|
4040
3961
|
// after its own turn.completed would never be torn down — it would
|
|
4041
3962
|
// keep polling the box forever, carrying dead per-turn state. busy
|
|
4042
3963
|
// is flipped false in the fold, so it is the honest "still running".
|
|
4043
|
-
if (!previewCapture && browser?.connection && browser.capability) {
|
|
4044
|
-
const { connection, capability } = browser;
|
|
4045
|
-
previewCapture = () => browserScreenshot(connection, capability, fetch);
|
|
4046
|
-
}
|
|
4047
3964
|
if (previewCapture && store.bot(bot.id)?.busy) {
|
|
4048
3965
|
startScreenPoller(bot.id, previewCapture, { screenIsTheWork: instance.driverKind === "boxAgent" });
|
|
4049
3966
|
}
|
|
@@ -4058,7 +3975,6 @@ async function startTurn(botId, text, opts) {
|
|
|
4058
3975
|
clearCancelledProviderHandshake(threadId, `direct:${dispatchClaimId}`);
|
|
4059
3976
|
clearDirectTurnDispatch(bot.id, dispatchClaimId);
|
|
4060
3977
|
revokeInternalCapabilityGeneration(threadId, dispatchClaimId);
|
|
4061
|
-
await releaseBrowserCapabilityForThread(threadId, dispatchClaimId);
|
|
4062
3978
|
const ownsLatestGeneration = directTurnGenerationByBot.get(bot.id) === dispatchClaimId;
|
|
4063
3979
|
if (ownsLatestGeneration) {
|
|
4064
3980
|
releaseLocalVmThread(threadId);
|
|
@@ -4222,7 +4138,6 @@ async function interruptRoutineGroupGoal(groupId, threadId, outcome) {
|
|
|
4222
4138
|
const bot = speaker ? store.bot(speaker.botId) : undefined;
|
|
4223
4139
|
cancelGroupTurnOperations(groupId, threadId, outcome);
|
|
4224
4140
|
revokeInternalCapabilitiesForThread(threadId);
|
|
4225
|
-
await releaseBrowserCapabilityForThread(threadId);
|
|
4226
4141
|
await (bot ? registry.get(bot.modelSelection.instanceId) : undefined)
|
|
4227
4142
|
?.adapter.interruptTurn(threadId)
|
|
4228
4143
|
.catch(() => { });
|
|
@@ -4251,7 +4166,6 @@ async function stopBotForEmergencyApprovalDowngrade(botId) {
|
|
|
4251
4166
|
revokeInternalCapabilitiesForThread(groupTurn.threadId);
|
|
4252
4167
|
cancelGroupTurnOperations(groupTurn.group.id, groupTurn.threadId);
|
|
4253
4168
|
const results = await Promise.allSettled([
|
|
4254
|
-
releaseBrowserCapabilityForThread(groupTurn.threadId),
|
|
4255
4169
|
registry.get(bot.modelSelection.instanceId)?.adapter.interruptTurn(groupTurn.threadId),
|
|
4256
4170
|
]);
|
|
4257
4171
|
closeOpenApprovals(groupTurn.threadId);
|
|
@@ -4264,7 +4178,6 @@ async function stopBotForEmergencyApprovalDowngrade(botId) {
|
|
|
4264
4178
|
const threadId = directClaim?.threadId ?? bot.threadId;
|
|
4265
4179
|
revokeInternalCapabilitiesForThread(threadId);
|
|
4266
4180
|
const results = await Promise.allSettled([
|
|
4267
|
-
releaseBrowserCapabilityForThread(threadId),
|
|
4268
4181
|
registry.get(bot.modelSelection.instanceId)?.adapter.interruptTurn(threadId),
|
|
4269
4182
|
]);
|
|
4270
4183
|
closeOpenApprovals(threadId);
|
|
@@ -4318,7 +4231,6 @@ routines = new RoutineManager({
|
|
|
4318
4231
|
? registry.get(bot.modelSelection.instanceId)
|
|
4319
4232
|
: null;
|
|
4320
4233
|
try {
|
|
4321
|
-
await releaseBrowserCapabilityForThread(threadId);
|
|
4322
4234
|
await instance?.adapter.interruptTurn(threadId);
|
|
4323
4235
|
}
|
|
4324
4236
|
finally {
|
|
@@ -4730,6 +4642,15 @@ setupRetry = 0) {
|
|
|
4730
4642
|
return true;
|
|
4731
4643
|
}
|
|
4732
4644
|
const internalGeneration = beginInternalCapabilityGeneration(threadId);
|
|
4645
|
+
let roomVmTarget = null;
|
|
4646
|
+
let retainRoomVmLease = false;
|
|
4647
|
+
let roomSpeaker;
|
|
4648
|
+
let providerDispatched = false;
|
|
4649
|
+
const releaseRoomVmLease = () => {
|
|
4650
|
+
if (roomVmTarget && localVmThreadTargets.get(threadId) === roomVmTarget)
|
|
4651
|
+
releaseLocalVmThread(threadId);
|
|
4652
|
+
roomVmTarget = null;
|
|
4653
|
+
};
|
|
4733
4654
|
try {
|
|
4734
4655
|
const integrations = {};
|
|
4735
4656
|
const skillAuthoring = skillRecorderEnabled(cfg) &&
|
|
@@ -4879,19 +4800,7 @@ setupRetry = 0) {
|
|
|
4879
4800
|
readyBot.browser !== false &&
|
|
4880
4801
|
instance.adapter.capabilities.browserMcp === true) {
|
|
4881
4802
|
const selectedProfile = readyBot.browserProfile;
|
|
4882
|
-
const browser =
|
|
4883
|
-
const currentBot = store.bot(readyBot.id);
|
|
4884
|
-
const currentGroup = store.group(readyGroup.id);
|
|
4885
|
-
const stillOwnsThread = currentGroup?.dm
|
|
4886
|
-
? currentGroup.threadId === threadId
|
|
4887
|
-
: Boolean(currentGroup && store.groupTaskByThread(currentGroup.id, threadId));
|
|
4888
|
-
return (!isCancelled?.() &&
|
|
4889
|
-
stillOwnsThread &&
|
|
4890
|
-
currentBot?.busy === true &&
|
|
4891
|
-
builtInBrowserEnabled(cfg) &&
|
|
4892
|
-
currentBot.browser !== false &&
|
|
4893
|
-
currentBot.browserProfile === selectedProfile);
|
|
4894
|
-
});
|
|
4803
|
+
const browser = browserIntegration(readyBot.id, selectedProfile);
|
|
4895
4804
|
if (browser)
|
|
4896
4805
|
integrations.browser = browser.integration;
|
|
4897
4806
|
}
|
|
@@ -4900,7 +4809,6 @@ setupRetry = 0) {
|
|
|
4900
4809
|
// room's setup claim so no provider turn starts after Stop returned.
|
|
4901
4810
|
const browserReadyBot = store.bot(readyBot.id);
|
|
4902
4811
|
if (isCancelled?.() || !browserReadyBot || !browserReadyBot.busy) {
|
|
4903
|
-
await releaseBrowserCapabilityForThread(threadId);
|
|
4904
4812
|
if (browserReadyBot?.busy) {
|
|
4905
4813
|
store.setActivity(browserReadyBot.id, "idle");
|
|
4906
4814
|
retryDelegationsWaitingOn(browserReadyBot.id);
|
|
@@ -4908,7 +4816,44 @@ setupRetry = 0) {
|
|
|
4908
4816
|
return false;
|
|
4909
4817
|
}
|
|
4910
4818
|
store.patchGroup(readyGroup.id, { busyBotId: bot.id }); // the store's change stream carries the frame
|
|
4911
|
-
|
|
4819
|
+
roomSpeaker = { botId: bot.id, name: bot.name, color: bot.color };
|
|
4820
|
+
groupSpeakers.set(threadId, roomSpeaker);
|
|
4821
|
+
// Room and Goal turns use the speaker's desktop, never the coordinator's.
|
|
4822
|
+
// Claim the same lease as direct turns before asynchronous VM setup.
|
|
4823
|
+
if (readyBot.computer === "vm") {
|
|
4824
|
+
if (instance.adapter.capabilities.computerMcp !== true || instance.driverKind === "boxAgent") {
|
|
4825
|
+
throw new Error("this model engine cannot use the Local VM");
|
|
4826
|
+
}
|
|
4827
|
+
// A distinct identity fences cleanup even in shared mode on the same room thread.
|
|
4828
|
+
const target = { ...localVmTargetForBot(readyBot.id) };
|
|
4829
|
+
if (localVmImageBusy || localVmModeChangeBusy || localVmLifecycleBusy.has(target.key)) {
|
|
4830
|
+
throw new Error("this Local VM is being started, stopped, or replaced");
|
|
4831
|
+
}
|
|
4832
|
+
if (!localVmLeaseFor(target).claim(threadId, readyBot.id, localVmOwnerBusy)) {
|
|
4833
|
+
throw new Error("this Local VM is already being used by another turn");
|
|
4834
|
+
}
|
|
4835
|
+
roomVmTarget = target;
|
|
4836
|
+
localVmThreadTargets.set(threadId, target);
|
|
4837
|
+
localVmActiveThreads.set(target.key, threadId);
|
|
4838
|
+
localVmIdleFor(target).touch();
|
|
4839
|
+
const setupIsCurrent = () => !isCancelled?.() &&
|
|
4840
|
+
groupSpeakers.get(threadId) === roomSpeaker &&
|
|
4841
|
+
activeInternalGenerationByThread.get(threadId) === internalGeneration &&
|
|
4842
|
+
store.group(readyGroup.id)?.memberIds.includes(readyBot.id) === true &&
|
|
4843
|
+
store.bot(readyBot.id)?.busy === true &&
|
|
4844
|
+
store.group(readyGroup.id)?.busyBotId === readyBot.id;
|
|
4845
|
+
const vm = await readyLocalVmForTurn(readyBot.id, target, setupIsCurrent);
|
|
4846
|
+
if (!setupIsCurrent()) {
|
|
4847
|
+
return false;
|
|
4848
|
+
}
|
|
4849
|
+
if (!vm.ready || !vm.runtime)
|
|
4850
|
+
throw new Error(vm.problem ?? "the Local VM is not ready");
|
|
4851
|
+
const owner = localVmLeaseFor(target).current(localVmOwnerBusy);
|
|
4852
|
+
if (owner?.threadId !== threadId || owner.botId !== readyBot.id) {
|
|
4853
|
+
throw new Error("the Local VM lease expired while preparing the turn");
|
|
4854
|
+
}
|
|
4855
|
+
integrations.localComputer = containerComputerMcp(vm.runtime, controlIntegration(readyBot.id, threadId, internalGeneration, target), target);
|
|
4856
|
+
}
|
|
4912
4857
|
const roster = readyGroup.memberIds
|
|
4913
4858
|
.map((id) => store.bot(id))
|
|
4914
4859
|
.filter((b) => Boolean(b))
|
|
@@ -4959,6 +4904,7 @@ setupRetry = 0) {
|
|
|
4959
4904
|
store.patchBot(bot.id, { soulDrift: drift.drift });
|
|
4960
4905
|
}
|
|
4961
4906
|
const roomSystem = buildSystemPrompt(system, store.bot(bot.id)?.soul ?? bot.soul ?? "", [
|
|
4907
|
+
{ id: "computer", label: "Computer", text: computerPrompt(roomVmTarget ? localVmMode(cfg) === "per-bot" ? "vm-private" : "vm-shared" : null) },
|
|
4962
4908
|
{ id: "browser", label: "Browser", text: integrations.browser ? BUILT_IN_BROWSER_SYSTEM_PROMPT : "" },
|
|
4963
4909
|
{ id: "recall", label: "Recall", text: integrations.agents ? SESSION_SEARCH_SYSTEM_PROMPT : "" },
|
|
4964
4910
|
{ id: "section-context", label: "Section context", text: sectionContextSystemPrompt(bot.section) },
|
|
@@ -4984,7 +4930,6 @@ setupRetry = 0) {
|
|
|
4984
4930
|
if (isCancelled?.() ||
|
|
4985
4931
|
store.group(group.id)?.busyBotId !== bot.id ||
|
|
4986
4932
|
store.bot(bot.id)?.busy !== true) {
|
|
4987
|
-
await releaseBrowserCapabilityForThread(threadId);
|
|
4988
4933
|
if (store.group(group.id)?.busyBotId === bot.id) {
|
|
4989
4934
|
groupSpeakers.delete(threadId);
|
|
4990
4935
|
store.patchGroup(group.id, { busyBotId: null, unread: true });
|
|
@@ -5016,7 +4961,6 @@ setupRetry = 0) {
|
|
|
5016
4961
|
let unregisterStall = () => { };
|
|
5017
4962
|
const deadline = new RoomTurnDeadline(timeoutMinutes, () => {
|
|
5018
4963
|
abandonProviderTurn();
|
|
5019
|
-
void releaseBrowserCapabilityForThread(threadId);
|
|
5020
4964
|
void instance.adapter.interruptTurn(threadId).catch(() => { });
|
|
5021
4965
|
store.appendMessage(threadId, {
|
|
5022
4966
|
role: "bot",
|
|
@@ -5068,6 +5012,7 @@ setupRetry = 0) {
|
|
|
5068
5012
|
});
|
|
5069
5013
|
watchdog.watch(threadId, bot.id);
|
|
5070
5014
|
onProviderHandshakeStarted?.();
|
|
5015
|
+
providerDispatched = true;
|
|
5071
5016
|
guardTurnDispatch(instance.adapter.sendTurn({
|
|
5072
5017
|
threadId,
|
|
5073
5018
|
text,
|
|
@@ -5081,7 +5026,6 @@ setupRetry = 0) {
|
|
|
5081
5026
|
// Stop may have landed while the adapter was authenticating, before
|
|
5082
5027
|
// it had an active process for the first interrupt to reach. Now that
|
|
5083
5028
|
// sendTurn completed setup, revoke again and interrupt the real turn.
|
|
5084
|
-
await releaseBrowserCapabilityForThread(threadId);
|
|
5085
5029
|
await instance.adapter.interruptTurn(threadId).catch(() => { });
|
|
5086
5030
|
})
|
|
5087
5031
|
.then((dispatch) => {
|
|
@@ -5121,6 +5065,9 @@ setupRetry = 0) {
|
|
|
5121
5065
|
// work so a retained proxy from this member cannot act during the next
|
|
5122
5066
|
// member's generation.
|
|
5123
5067
|
revokeInternalCapabilityGeneration(threadId, internalGeneration);
|
|
5068
|
+
retainRoomVmLease = outcome === "timed_out" || outcome === "stalled";
|
|
5069
|
+
if (!retainRoomVmLease)
|
|
5070
|
+
releaseRoomVmLease();
|
|
5124
5071
|
if (orchestration) {
|
|
5125
5072
|
orchestration.result.replyText = replyText.trim();
|
|
5126
5073
|
orchestration.result.outcome = outcome;
|
|
@@ -5160,6 +5107,7 @@ setupRetry = 0) {
|
|
|
5160
5107
|
retry.unref?.();
|
|
5161
5108
|
return;
|
|
5162
5109
|
}
|
|
5110
|
+
releaseRoomVmLease();
|
|
5163
5111
|
const currentGroup = store.group(group.id);
|
|
5164
5112
|
const speaker = groupSpeakers.get(threadId);
|
|
5165
5113
|
if (currentGroup?.busyBotId === bot.id && speaker?.botId === bot.id) {
|
|
@@ -5195,7 +5143,6 @@ setupRetry = 0) {
|
|
|
5195
5143
|
if (outcome === "dispatch_failed") {
|
|
5196
5144
|
if (skillAuthoring)
|
|
5197
5145
|
skillAuthoringClaim.claimed = false;
|
|
5198
|
-
await releaseBrowserCapabilityForThread(threadId);
|
|
5199
5146
|
// No turn.completed follows a rejected room dispatch. Anything that was
|
|
5200
5147
|
// queued while this bot briefly owned the room must be retried now.
|
|
5201
5148
|
drainQueuedSends();
|
|
@@ -5255,10 +5202,38 @@ setupRetry = 0) {
|
|
|
5255
5202
|
}
|
|
5256
5203
|
return true;
|
|
5257
5204
|
}
|
|
5205
|
+
catch (error) {
|
|
5206
|
+
if (providerDispatched || !roomSpeaker)
|
|
5207
|
+
throw error;
|
|
5208
|
+
const message = error instanceof Error ? error.message : "Local VM setup failed";
|
|
5209
|
+
store.appendMessage(threadId, {
|
|
5210
|
+
role: "bot", kind: "activity",
|
|
5211
|
+
from: { botId: bot.id, name: bot.name, color: bot.color },
|
|
5212
|
+
tool: { name: `error: ${message}`, ok: false },
|
|
5213
|
+
});
|
|
5214
|
+
onDispatchError?.(message);
|
|
5215
|
+
if (orchestration)
|
|
5216
|
+
orchestration.result.outcome = "dispatch_failed";
|
|
5217
|
+
return false;
|
|
5218
|
+
}
|
|
5258
5219
|
finally {
|
|
5259
5220
|
// Covers connector/setup failures, cancellation before dispatch, and all
|
|
5260
5221
|
// other early returns that never produce a provider terminal event.
|
|
5261
5222
|
revokeInternalCapabilityGeneration(threadId, internalGeneration);
|
|
5223
|
+
if (!retainRoomVmLease)
|
|
5224
|
+
releaseRoomVmLease();
|
|
5225
|
+
if (!providerDispatched && roomSpeaker && groupSpeakers.get(threadId) === roomSpeaker) {
|
|
5226
|
+
groupSpeakers.delete(threadId);
|
|
5227
|
+
if (store.group(group.id)?.busyBotId === bot.id)
|
|
5228
|
+
store.patchGroup(group.id, { busyBotId: null });
|
|
5229
|
+
if (store.bot(bot.id)?.busy) {
|
|
5230
|
+
store.setActivity(bot.id, "idle");
|
|
5231
|
+
retryDelegationsWaitingOn(bot.id);
|
|
5232
|
+
}
|
|
5233
|
+
drainQueuedSends();
|
|
5234
|
+
drainConnectorResumes();
|
|
5235
|
+
drainSecretResumes();
|
|
5236
|
+
}
|
|
5262
5237
|
}
|
|
5263
5238
|
}
|
|
5264
5239
|
async function runGroupGoalStep(args) {
|
|
@@ -6493,13 +6468,15 @@ async function localVmPayload(target) {
|
|
|
6493
6468
|
* instance cap; creating past it would quietly do what the lifecycle route
|
|
6494
6469
|
* refuses.
|
|
6495
6470
|
*/
|
|
6496
|
-
async function readyLocalVmForTurn(botId, target) {
|
|
6471
|
+
async function readyLocalVmForTurn(botId, target, isCurrent = () => true) {
|
|
6497
6472
|
let status = await containerComputerStatus(undefined, undefined, target);
|
|
6473
|
+
if (!isCurrent())
|
|
6474
|
+
return status;
|
|
6498
6475
|
if (status.ready || !localVmRecreatableOnDemand(status))
|
|
6499
6476
|
return status;
|
|
6500
6477
|
if (target.key !== SHARED_LOCAL_VM_TARGET.key) {
|
|
6501
6478
|
const count = await existingPerBotLocalVmCount(status.runtime);
|
|
6502
|
-
if (count >= localVmMaxInstances(cfg))
|
|
6479
|
+
if (!isCurrent() || count >= localVmMaxInstances(cfg))
|
|
6503
6480
|
return status;
|
|
6504
6481
|
}
|
|
6505
6482
|
broadcast({ kind: "computer", botId, state: "provisioning" });
|
|
@@ -6522,8 +6499,10 @@ async function readyLocalVmForTurn(botId, target) {
|
|
|
6522
6499
|
// the turn is the whole point: a person who has been away eight hours should
|
|
6523
6500
|
// not have to send their message twice.
|
|
6524
6501
|
const deadline = Date.now() + LOCAL_VM_DESKTOP_WAIT_MS;
|
|
6525
|
-
while (!status.ready && status.container === "running" && Date.now() < deadline) {
|
|
6502
|
+
while (isCurrent() && !status.ready && status.container === "running" && Date.now() < deadline) {
|
|
6526
6503
|
await new Promise((resolve) => setTimeout(resolve, 2_000));
|
|
6504
|
+
if (!isCurrent())
|
|
6505
|
+
break;
|
|
6527
6506
|
status = await containerComputerStatus(undefined, undefined, target);
|
|
6528
6507
|
}
|
|
6529
6508
|
return status;
|
|
@@ -6599,7 +6578,6 @@ async function reloadProviders() {
|
|
|
6599
6578
|
// one synchronous step before the first teardown await, including room/task
|
|
6600
6579
|
// threads that are not a bot's default DM.
|
|
6601
6580
|
revokeAllInternalCapabilities();
|
|
6602
|
-
await releaseAllBrowserCapabilities();
|
|
6603
6581
|
bus.detachAll();
|
|
6604
6582
|
await registry.disposeAll();
|
|
6605
6583
|
await registry.load(instanceConfigs(cfg));
|
|
@@ -9257,7 +9235,6 @@ const handleRequest = async (req, res) => {
|
|
|
9257
9235
|
cancelGroupTurnOperations(group.id, threadId);
|
|
9258
9236
|
for (const { threadId, instance } of interruptTargets) {
|
|
9259
9237
|
revokeInternalCapabilitiesForThread(threadId);
|
|
9260
|
-
await releaseBrowserCapabilityForThread(threadId);
|
|
9261
9238
|
await instance?.adapter.interruptTurn(threadId).catch(() => { });
|
|
9262
9239
|
closeOpenApprovals(threadId);
|
|
9263
9240
|
}
|
|
@@ -9430,7 +9407,7 @@ const handleRequest = async (req, res) => {
|
|
|
9430
9407
|
(!supportsApprovalMode(registry.cliTarget(checked.selection.instanceId)?.driverKind, existingApprovalMode) ||
|
|
9431
9408
|
registry.cliTarget(checked.selection.instanceId)?.driverKind !== registry.cliTarget(existing.modelSelection.instanceId)?.driverKind)) {
|
|
9432
9409
|
return json(res, 400, {
|
|
9433
|
-
error: "Changing providers with elevated permissions requires choosing Ask
|
|
9410
|
+
error: "Changing providers with elevated permissions requires choosing Ask first",
|
|
9434
9411
|
});
|
|
9435
9412
|
}
|
|
9436
9413
|
// patchBot persists first and emits the canonical bot change, which the
|
|
@@ -9682,7 +9659,7 @@ const handleRequest = async (req, res) => {
|
|
|
9682
9659
|
(!targetSelection || !supportsApprovalMode(registry.cliTarget(targetSelection.instanceId)?.driverKind, requestedApprovalMode) ||
|
|
9683
9660
|
(existingBot && normalizedSelection && registry.cliTarget(normalizedSelection.instanceId)?.driverKind !== registry.cliTarget(existingBot.modelSelection.instanceId)?.driverKind))) {
|
|
9684
9661
|
return json(res, 400, {
|
|
9685
|
-
error: "This provider does not support the selected approval level, or changing providers requires choosing Ask
|
|
9662
|
+
error: "This provider does not support the selected approval level, or changing providers requires choosing Ask first",
|
|
9686
9663
|
});
|
|
9687
9664
|
}
|
|
9688
9665
|
const requiresPrivateApprovalTransition = ((requestedApprovalMode === "full" || requestedApprovalMode === "custom") &&
|
|
@@ -9855,7 +9832,6 @@ const handleRequest = async (req, res) => {
|
|
|
9855
9832
|
cancelDirectTurnDispatch(bot.id, routineRun.threadId);
|
|
9856
9833
|
if (routineRun.threadId) {
|
|
9857
9834
|
revokeInternalCapabilitiesForThread(routineRun.threadId);
|
|
9858
|
-
await releaseBrowserCapabilityForThread(routineRun.threadId);
|
|
9859
9835
|
}
|
|
9860
9836
|
await routines.cancelRun(routineRun.id);
|
|
9861
9837
|
return;
|
|
@@ -9865,7 +9841,6 @@ const handleRequest = async (req, res) => {
|
|
|
9865
9841
|
if (groupTurn) {
|
|
9866
9842
|
cancelGroupTurnOperations(groupTurn.group.id, groupTurn.threadId);
|
|
9867
9843
|
revokeInternalCapabilitiesForThread(groupTurn.threadId);
|
|
9868
|
-
await releaseBrowserCapabilityForThread(groupTurn.threadId);
|
|
9869
9844
|
await instance?.adapter.interruptTurn(groupTurn.threadId).catch(() => { });
|
|
9870
9845
|
closeOpenApprovals(groupTurn.threadId);
|
|
9871
9846
|
return;
|
|
@@ -9873,7 +9848,6 @@ const handleRequest = async (req, res) => {
|
|
|
9873
9848
|
const directClaim = cancelDirectTurnDispatch(bot.id);
|
|
9874
9849
|
const threadId = directClaim?.threadId ?? bot.threadId;
|
|
9875
9850
|
revokeInternalCapabilitiesForThread(threadId);
|
|
9876
|
-
await releaseBrowserCapabilityForThread(threadId);
|
|
9877
9851
|
await instance?.adapter.interruptTurn(threadId).catch(() => { });
|
|
9878
9852
|
closeOpenApprovals(threadId);
|
|
9879
9853
|
}));
|
|
@@ -10000,7 +9974,7 @@ const handleRequest = async (req, res) => {
|
|
|
10000
9974
|
// or unreadable journal therefore rejects the delete with the bot and
|
|
10001
9975
|
// all of its live work untouched. The intent is aborted if a later
|
|
10002
9976
|
// pre-delete side effect fails, and committed only after Store deletion.
|
|
10003
|
-
const browserCleanupRequest =
|
|
9977
|
+
const browserCleanupRequest = browserCleanup.prepare("bot", bot.id);
|
|
10004
9978
|
try {
|
|
10005
9979
|
// a running turn dies with its bot
|
|
10006
9980
|
const directClaim = cancelDirectTurnDispatch(bot.id);
|
|
@@ -10010,7 +9984,6 @@ const handleRequest = async (req, res) => {
|
|
|
10010
9984
|
// check is revalidated after its body arrives and must fail closed.
|
|
10011
9985
|
revokeInternalCapabilitiesForThread(directThreadId);
|
|
10012
9986
|
directTurnGenerationByBot.delete(bot.id);
|
|
10013
|
-
await releaseBrowserCapabilitiesForBot(bot.id);
|
|
10014
9987
|
await registry.get(bot.modelSelection.instanceId)?.adapter.interruptTurn(directThreadId).catch(() => { });
|
|
10015
9988
|
closeOpenApprovals(directThreadId);
|
|
10016
9989
|
// Deletion removes the thread before a late turn.completed can fold
|
|
@@ -10771,7 +10744,6 @@ const handleRequest = async (req, res) => {
|
|
|
10771
10744
|
cancelDirectTurnDispatch(bot.id, routineRun.threadId ?? expectedThreadId);
|
|
10772
10745
|
if (routineRun.threadId) {
|
|
10773
10746
|
revokeInternalCapabilitiesForThread(routineRun.threadId);
|
|
10774
|
-
await releaseBrowserCapabilityForThread(routineRun.threadId);
|
|
10775
10747
|
}
|
|
10776
10748
|
await routines.cancelRun(routineRun.id);
|
|
10777
10749
|
return json(res, 200, { ok: true });
|
|
@@ -10786,7 +10758,6 @@ const handleRequest = async (req, res) => {
|
|
|
10786
10758
|
}
|
|
10787
10759
|
cancelGroupTurnOperations(busyGroup.group.id, busyGroup.threadId);
|
|
10788
10760
|
revokeInternalCapabilitiesForThread(busyGroup.threadId);
|
|
10789
|
-
await releaseBrowserCapabilityForThread(busyGroup.threadId);
|
|
10790
10761
|
await instance?.adapter.interruptTurn(busyGroup.threadId).catch(() => { });
|
|
10791
10762
|
closeOpenApprovals(busyGroup.threadId);
|
|
10792
10763
|
return json(res, 200, { ok: true });
|
|
@@ -10800,7 +10771,6 @@ const handleRequest = async (req, res) => {
|
|
|
10800
10771
|
const cancelledDirect = cancelDirectTurnDispatch(bot.id, expectedThreadId);
|
|
10801
10772
|
const directThreadId = cancelledDirect?.threadId ?? bot.threadId;
|
|
10802
10773
|
revokeInternalCapabilitiesForThread(directThreadId);
|
|
10803
|
-
await releaseBrowserCapabilityForThread(directThreadId);
|
|
10804
10774
|
await instance?.adapter.interruptTurn(directThreadId).catch(() => { });
|
|
10805
10775
|
closeOpenApprovals(directThreadId);
|
|
10806
10776
|
return json(res, 200, { ok: true });
|
|
@@ -11088,6 +11058,26 @@ const handleRequest = async (req, res) => {
|
|
|
11088
11058
|
if (method === "GET" && path === "/api/health") {
|
|
11089
11059
|
return json(res, 200, { app: "openmausbot", pid: process.pid, static: Boolean(STATIC_DIR) });
|
|
11090
11060
|
}
|
|
11061
|
+
// The bots' browser engine: install it on this machine (agent-browser +
|
|
11062
|
+
// a Chrome for Testing, a one-time download), or ask how that is going.
|
|
11063
|
+
// One install at a time; the config frame's browserEngine tells the rest.
|
|
11064
|
+
if (method === "POST" && path === "/api/browser-engine/install") {
|
|
11065
|
+
if (!browserEngineInstall) {
|
|
11066
|
+
const status = browserEngineStatus();
|
|
11067
|
+
if (status.kind === "unavailable" && !status.installable)
|
|
11068
|
+
return json(res, 409, { error: status.reason });
|
|
11069
|
+
browserEngineInstallError = null;
|
|
11070
|
+
browserEngineInstall = (async () => {
|
|
11071
|
+
const binary = resolveAgentBrowserBinary() ?? await installAgentBrowserBinary({ log: (line) => console.log(line) });
|
|
11072
|
+
await ensureChrome(binary, { log: (line) => console.log(line) });
|
|
11073
|
+
})().then(() => { browserEngineInstallError = null; }, (error) => { browserEngineInstallError = error instanceof Error ? error.message : String(error); }).finally(() => {
|
|
11074
|
+
browserEngineInstall = null;
|
|
11075
|
+
broadcast({ kind: "config", ...configStatus() });
|
|
11076
|
+
});
|
|
11077
|
+
broadcast({ kind: "config", ...configStatus() });
|
|
11078
|
+
}
|
|
11079
|
+
return json(res, 202, { installing: true });
|
|
11080
|
+
}
|
|
11091
11081
|
// Which edition this server runs and why (see server/enterprise.ts). Read-only.
|
|
11092
11082
|
if (method === "GET" && path === "/api/edition") {
|
|
11093
11083
|
return json(res, 200, editionStatus());
|
|
@@ -11649,13 +11639,11 @@ const handleRequest = async (req, res) => {
|
|
|
11649
11639
|
}
|
|
11650
11640
|
const browserCleanupRequests = [];
|
|
11651
11641
|
try {
|
|
11652
|
-
|
|
11653
|
-
|
|
11654
|
-
|
|
11655
|
-
|
|
11656
|
-
|
|
11657
|
-
browserCleanupRequests.push(browserCleanup.prepare("profile", target.profileId, target.partitionId));
|
|
11658
|
-
}
|
|
11642
|
+
for (const profileId of removedBrowserProfileIds) {
|
|
11643
|
+
const target = browserProfilePartitionTarget(cfg, profileId);
|
|
11644
|
+
if (!target)
|
|
11645
|
+
throw new Error(`browser profile cleanup target “${profileId}” is unavailable`);
|
|
11646
|
+
browserCleanupRequests.push(browserCleanup.prepare("profile", target.profileId, target.partitionId));
|
|
11659
11647
|
}
|
|
11660
11648
|
}
|
|
11661
11649
|
catch (error) {
|
|
@@ -11763,7 +11751,6 @@ const handleRequest = async (req, res) => {
|
|
|
11763
11751
|
let mandatoryError = null;
|
|
11764
11752
|
if (disablingBuiltInBrowser) {
|
|
11765
11753
|
try {
|
|
11766
|
-
await releaseAllBrowserCapabilities();
|
|
11767
11754
|
}
|
|
11768
11755
|
catch (error) {
|
|
11769
11756
|
mandatoryError = error;
|
|
@@ -12241,7 +12228,6 @@ const gracefulShutdown = createGracefulShutdown({
|
|
|
12241
12228
|
webhookIngress?.server.close();
|
|
12242
12229
|
tunnelListener?.close();
|
|
12243
12230
|
},
|
|
12244
|
-
() => releaseAllBrowserCapabilities(),
|
|
12245
12231
|
() => registry.disposeAll(),
|
|
12246
12232
|
() => flushAllProfileHistory(),
|
|
12247
12233
|
],
|