diff --git a/src/main/orcad/browser-providers/chromium/external-chromium-agent-browser-environment.ts b/src/main/orcad/browser-providers/chromium/external-chromium-agent-browser-environment.ts new file mode 100644 index 00000000000..5bd2c0dd856 --- /dev/null +++ b/src/main/orcad/browser-providers/chromium/external-chromium-agent-browser-environment.ts @@ -0,0 +1,33 @@ +// Both pick the socket directory, so dropping either loses a running helper and its live tabs. +const SOCKET_LOCATION_KEYS = new Set(['AGENT_BROWSER_SOCKET_DIR', 'AGENT_BROWSER_NAMESPACE']) + +// No idle timeout: this daemon owns the user’s remote Chromium and all its live tabs. +// agent-browser 0.33+ reads an unset timeout as one hour, so only "0" keeps it unbounded. +export function externalChromiumAgentBrowserEnvironment(options: { + inheritedEnv: NodeJS.ProcessEnv + executablePath: string + profilePath: string + sessionName: string + configPath: string + browserArgs?: readonly string[] +}): NodeJS.ProcessEnv { + // Only the socket location is inherited: a user's AGENT_BROWSER_HEADED or _EXTENSIONS would open a + // visible window. Windows env keys ignore case. + const env = Object.fromEntries( + Object.entries(options.inheritedEnv).filter(([key]) => { + const upper = key.toUpperCase() + return !upper.startsWith('AGENT_BROWSER_') || SOCKET_LOCATION_KEYS.has(upper) + }) + ) + return { + ...env, + AGENT_BROWSER_IDLE_TIMEOUT_MS: + options.inheritedEnv.AGENT_BROWSER_IDLE_TIMEOUT_MS?.trim() || '0', + AGENT_BROWSER_EXECUTABLE_PATH: options.executablePath, + AGENT_BROWSER_PROFILE: options.profilePath, + AGENT_BROWSER_SESSION: options.sessionName, + // An explicit config skips ~/.agent-browser/config.json and ./agent-browser.json. + AGENT_BROWSER_CONFIG: options.configPath, + AGENT_BROWSER_ARGS: options.browserArgs?.join('\n') ?? '' + } +} diff --git a/src/main/orcad/browser-providers/chromium/external-chromium-browser-session.ts b/src/main/orcad/browser-providers/chromium/external-chromium-browser-session.ts index e322b11efb5..fdcfc730f51 100644 --- a/src/main/orcad/browser-providers/chromium/external-chromium-browser-session.ts +++ b/src/main/orcad/browser-providers/chromium/external-chromium-browser-session.ts @@ -17,6 +17,7 @@ import { } from '../../../browser/agent-browser/observation/agent-browser-screenshot-arguments' import { runProcess } from '@orca/process-host' import { resolveExternalChromiumProfilePath } from './external-chromium-profile-path' +import { externalChromiumAgentBrowserEnvironment } from './external-chromium-agent-browser-environment' const COMMAND_TIMEOUT_MS = 90_000 const MAX_OUTPUT_BYTES = 50 * 1024 * 1024 @@ -73,40 +74,6 @@ function classifyAgentBrowserError(message: string): string { return 'browser_error' } -// Both pick the socket directory, so dropping either loses a running helper and its live tabs. -const SOCKET_LOCATION_KEYS = new Set(['AGENT_BROWSER_SOCKET_DIR', 'AGENT_BROWSER_NAMESPACE']) - -// No idle timeout: this daemon owns the user’s remote Chromium and all its live tabs. -// agent-browser 0.33+ reads an unset timeout as one hour, so only "0" keeps it unbounded. -export function externalChromiumAgentBrowserEnvironment(options: { - inheritedEnv: NodeJS.ProcessEnv - executablePath: string - profilePath: string - sessionName: string - configPath: string - browserArgs?: readonly string[] -}): NodeJS.ProcessEnv { - // Only the socket location is inherited: a user's AGENT_BROWSER_HEADED or _EXTENSIONS would open a - // visible window. Windows env keys ignore case. - const env = Object.fromEntries( - Object.entries(options.inheritedEnv).filter(([key]) => { - const upper = key.toUpperCase() - return !upper.startsWith('AGENT_BROWSER_') || SOCKET_LOCATION_KEYS.has(upper) - }) - ) - return { - ...env, - AGENT_BROWSER_IDLE_TIMEOUT_MS: - options.inheritedEnv.AGENT_BROWSER_IDLE_TIMEOUT_MS?.trim() || '0', - AGENT_BROWSER_EXECUTABLE_PATH: options.executablePath, - AGENT_BROWSER_PROFILE: options.profilePath, - AGENT_BROWSER_SESSION: options.sessionName, - // An explicit config skips ~/.agent-browser/config.json and ./agent-browser.json. - AGENT_BROWSER_CONFIG: options.configPath, - AGENT_BROWSER_ARGS: options.browserArgs?.join('\n') ?? '' - } -} - // A profile held by another Chromium also fails ProcessSingleton; only an access failure means unwritable. function unwritableProfileLockLine(message: string): string | undefined { return message @@ -144,12 +111,21 @@ export class ExternalChromiumBrowserSession { // Empty, so agent-browser's defaults (headless, no extensions) apply instead of a user's config. private configPath(): Promise { - this.configPathWrite ??= (async () => { - const path = join(this.statePath, 'agent-browser-config.json') - await mkdir(this.statePath, { recursive: true, mode: 0o700 }) - await writeFile(path, '{}\n', { mode: 0o600 }) - return path - })() + if (!this.configPathWrite) { + const write = (async () => { + const path = join(this.statePath, 'agent-browser-config.json') + await mkdir(this.statePath, { recursive: true, mode: 0o700 }) + await writeFile(path, '{}\n', { mode: 0o600 }) + return path + })() + // A failed write retries on the next command instead of failing every later one. + write.catch(() => { + if (this.configPathWrite === write) { + this.configPathWrite = null + } + }) + this.configPathWrite = write + } return this.configPathWrite }