diff --git a/apps/server/src/provider/CodexDeveloperInstructions.ts b/apps/server/src/provider/CodexDeveloperInstructions.ts index b46a4ce1ba3..2c059156644 100644 --- a/apps/server/src/provider/CodexDeveloperInstructions.ts +++ b/apps/server/src/provider/CodexDeveloperInstructions.ts @@ -9,7 +9,15 @@ For browser work, first call \`preview_status\`. If no automation-capable previe Do not switch to global browser skills, Chrome, Node REPL browser automation, standalone Playwright, or agent-browser merely because the preview is initially closed or a first call fails. Use an alternative browser system only when the T3 preview tools are absent, the user explicitly requests another browser, or \`preview_open\` returns an explicit unsupported/unavailable error. A failed T3 preview tool call should be inspected and retried with corrected arguments when the error is actionable. `; -export const CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS = `# Plan Mode (Conversational) +const withBrowserToolInstructions = (instructions: string, enabled: boolean): string => + enabled + ? instructions.replace( + "\n", + `${T3_CODE_BROWSER_TOOL_INSTRUCTIONS}\n`, + ) + : instructions; + +const CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS_BASE = `# Plan Mode (Conversational) You work in 3 phases, and you should *chat your way* to a great plan before finalizing it. A great plan is very detailed-intent- and implementation-wise-so that it can be handed to another engineer or agent to be implemented right away. It must be **decision complete**, where the implementer does not need to make any decisions. @@ -129,10 +137,9 @@ plan content should be human and agent digestible. The final plan must be plan-o Do not ask "should I proceed?" in the final output. The user can easily switch out of Plan mode and request implementation if you have included a \`\` block in your response. Alternatively, they can decide to stay in Plan mode and continue refining the plan. Only produce at most one \`\` block per turn, and only when you are presenting a complete spec. -${T3_CODE_BROWSER_TOOL_INSTRUCTIONS} `; -export const CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS = `# Collaboration Mode: Default +const CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS_BASE = `# Collaboration Mode: Default You are now in Default mode. Any previous instructions for other modes (e.g. Plan mode) are no longer active. @@ -143,5 +150,21 @@ Your active mode changes only when new developer instructions with a different \ The \`request_user_input\` tool is unavailable in Default mode. If you call it while in Default mode, it will return an error. In Default mode, strongly prefer making reasonable assumptions and executing the user's request rather than stopping to ask questions. If you absolutely must ask a question because the answer cannot be discovered from local context and a reasonable assumption would be risky, ask the user directly with a concise plain-text question. Never write a multiple choice question as a textual assistant message. -${T3_CODE_BROWSER_TOOL_INSTRUCTIONS} `; + +export const buildCodexDeveloperInstructions = ( + mode: "default" | "plan", + browserEnabled: boolean, +): string => + withBrowserToolInstructions( + mode === "plan" + ? CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS_BASE + : CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS_BASE, + browserEnabled, + ); + +export const CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS = buildCodexDeveloperInstructions("plan", true); +export const CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS = buildCodexDeveloperInstructions( + "default", + true, +); diff --git a/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts b/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts index 8aeacd870cc..82d9e7c5d2e 100644 --- a/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts +++ b/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts @@ -9,6 +9,7 @@ import * as CodexErrors from "effect-codex-app-server/errors"; import * as CodexRpc from "effect-codex-app-server/rpc"; import { + buildCodexDeveloperInstructions, CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS, CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS, } from "../CodexDeveloperInstructions.ts"; @@ -206,6 +207,15 @@ describe("T3 browser developer instructions", () => { NodeAssert.match(instructions, /Do not switch to global browser skills/); } }); + + it("omits browser instructions when browser access is disabled", () => { + for (const mode of ["default", "plan"] as const) { + const instructions = buildCodexDeveloperInstructions(mode, false); + NodeAssert.doesNotMatch(instructions, /t3-code/); + NodeAssert.doesNotMatch(instructions, /preview_status/); + NodeAssert.doesNotMatch(instructions, /preview_open/); + } + }); }); describe("hasConfiguredMcpServer", () => { diff --git a/apps/server/src/provider/Layers/CodexSessionRuntime.ts b/apps/server/src/provider/Layers/CodexSessionRuntime.ts index 99ac498f0c3..0b1b81f31ef 100644 --- a/apps/server/src/provider/Layers/CodexSessionRuntime.ts +++ b/apps/server/src/provider/Layers/CodexSessionRuntime.ts @@ -37,10 +37,7 @@ import * as EffectCodexSchema from "effect-codex-app-server/schema"; import { buildCodexInitializeParams } from "./CodexProvider.ts"; import { expandHomePath } from "../../pathExpansion.ts"; -import { - CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS, - CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS, -} from "../CodexDeveloperInstructions.ts"; +import { buildCodexDeveloperInstructions } from "../CodexDeveloperInstructions.ts"; const decodeV2TurnStartResponse = Schema.decodeUnknownEffect(EffectCodexSchema.V2TurnStartResponse); const PROVIDER = ProviderDriverKind.make("codex"); @@ -326,6 +323,7 @@ function buildCodexCollaborationMode(input: { readonly interactionMode?: ProviderInteractionMode; readonly model?: string; readonly effort?: EffectCodexSchema.V2TurnStartParams__ReasoningEffort; + readonly browserEnabled?: boolean; }): EffectCodexSchema.V2TurnStartParams__CollaborationMode | undefined { if (input.interactionMode === undefined) { return undefined; @@ -336,10 +334,10 @@ function buildCodexCollaborationMode(input: { settings: { model, reasoning_effort: input.effort ?? "medium", - developer_instructions: - input.interactionMode === "plan" - ? CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS - : CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS, + developer_instructions: buildCodexDeveloperInstructions( + input.interactionMode, + input.browserEnabled ?? true, + ), }, }; } @@ -356,6 +354,7 @@ export function buildTurnStartParams(input: { readonly serviceTier?: CodexServiceTier; readonly effort?: EffectCodexSchema.V2TurnStartParams__ReasoningEffort; readonly interactionMode?: ProviderInteractionMode; + readonly browserEnabled?: boolean; }): Effect.Effect< CodexTurnStartParamsWithCollaborationMode, CodexErrors.CodexAppServerProtocolParseError @@ -376,6 +375,7 @@ export function buildTurnStartParams(input: { ...(input.interactionMode ? { interactionMode: input.interactionMode } : {}), ...(input.model ? { model: input.model } : {}), ...(input.effort ? { effort: input.effort } : {}), + ...(input.browserEnabled !== undefined ? { browserEnabled: input.browserEnabled } : {}), }); return decodeCodexTurnStartParamsWithCollaborationMode({ @@ -1286,6 +1286,7 @@ export const makeCodexSessionRuntime = ( ...(input.serviceTier ? { serviceTier: input.serviceTier } : {}), ...(input.effort ? { effort: input.effort } : {}), ...(input.interactionMode ? { interactionMode: input.interactionMode } : {}), + browserEnabled: hasConfiguredMcpServer(options.appServerArgs), }); const rawResponse = yield* client.raw.request("turn/start", params); const response = yield* decodeV2TurnStartResponse(rawResponse).pipe( diff --git a/apps/server/src/provider/Layers/ProviderService.ts b/apps/server/src/provider/Layers/ProviderService.ts index 2eaaeb8ce3c..007717d68fc 100644 --- a/apps/server/src/provider/Layers/ProviderService.ts +++ b/apps/server/src/provider/Layers/ProviderService.ts @@ -55,6 +55,7 @@ import * as ProviderEventLoggers from "./ProviderEventLoggers.ts"; import * as AnalyticsService from "../../telemetry/AnalyticsService.ts"; import * as McpProviderSession from "../../mcp/McpProviderSession.ts"; import * as McpSessionRegistry from "../../mcp/McpSessionRegistry.ts"; +import * as ServerSettings from "../../serverSettings.ts"; const isModelSelection = Schema.is(ModelSelection); /** @@ -212,16 +213,28 @@ const makeProviderService = Effect.fn("makeProviderService")(function* ( const registry = yield* ProviderAdapterRegistry.ProviderAdapterRegistry; const directory = yield* ProviderSessionDirectory.ProviderSessionDirectory; + const serverSettings = yield* ServerSettings.ServerSettingsService; const runtimeEventPubSub = yield* PubSub.unbounded(); const nowIso = Effect.map(DateTime.now, DateTime.formatIso); - const prepareMcpSession = (threadId: ThreadId, providerInstanceId: ProviderInstanceId) => - McpSessionRegistry.issueActiveMcpCredential({ threadId, providerInstanceId }).pipe( - Effect.tap((credential) => - credential - ? Effect.sync(() => McpProviderSession.setMcpProviderSession(credential.config)) - : Effect.void, - ), - ); + const prepareMcpSession = Effect.fn("ProviderService.prepareMcpSession")(function* ( + threadId: ThreadId, + providerInstanceId: ProviderInstanceId, + ) { + const settings = yield* serverSettings.getSettings.pipe(Effect.orDie); + if (!settings.enableBuiltInBrowser) { + yield* McpSessionRegistry.revokeActiveMcpThread(threadId); + McpProviderSession.clearMcpProviderSession(threadId); + return; + } + + const credential = yield* McpSessionRegistry.issueActiveMcpCredential({ + threadId, + providerInstanceId, + }); + if (credential) { + McpProviderSession.setMcpProviderSession(credential.config); + } + }); const clearMcpSession = (threadId: ThreadId) => McpSessionRegistry.revokeActiveMcpThread(threadId).pipe( Effect.tap(() => Effect.sync(() => McpProviderSession.clearMcpProviderSession(threadId))), diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx index 40017d56314..173aea2408b 100644 --- a/apps/web/src/components/settings/SettingsPanels.tsx +++ b/apps/web/src/components/settings/SettingsPanels.tsx @@ -401,6 +401,9 @@ export function useSettingsRestore(onRestored?: () => void) { ...(settings.enableAssistantStreaming !== DEFAULT_UNIFIED_SETTINGS.enableAssistantStreaming ? ["Assistant output"] : []), + ...(settings.enableBuiltInBrowser !== DEFAULT_UNIFIED_SETTINGS.enableBuiltInBrowser + ? ["Built-in browser"] + : []), ...(Duration.toMillis(settings.automaticGitFetchInterval) !== Duration.toMillis(DEFAULT_UNIFIED_SETTINGS.automaticGitFetchInterval) ? ["Automatic Git fetch interval"] @@ -434,6 +437,7 @@ export function useSettingsRestore(onRestored?: () => void) { settings.diffIgnoreWhitespace, settings.automaticGitFetchInterval, settings.enableAssistantStreaming, + settings.enableBuiltInBrowser, settings.sidebarThreadPreviewCount, settings.timestampFormat, settings.wordWrap, @@ -459,6 +463,7 @@ export function useSettingsRestore(onRestored?: () => void) { sidebarThreadPreviewCount: DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount, autoOpenPlanSidebar: DEFAULT_UNIFIED_SETTINGS.autoOpenPlanSidebar, enableAssistantStreaming: DEFAULT_UNIFIED_SETTINGS.enableAssistantStreaming, + enableBuiltInBrowser: DEFAULT_UNIFIED_SETTINGS.enableBuiltInBrowser, automaticGitFetchInterval: DEFAULT_UNIFIED_SETTINGS.automaticGitFetchInterval, defaultThreadEnvMode: DEFAULT_UNIFIED_SETTINGS.defaultThreadEnvMode, newWorktreesStartFromOrigin: DEFAULT_UNIFIED_SETTINGS.newWorktreesStartFromOrigin, @@ -695,6 +700,32 @@ export function GeneralSettingsPanel() { } /> + + updateSettings({ + enableBuiltInBrowser: DEFAULT_UNIFIED_SETTINGS.enableBuiltInBrowser, + }) + } + /> + ) : null + } + control={ + + updateSettings({ enableBuiltInBrowser: Boolean(checked) }) + } + aria-label="Allow agents to use the built-in browser" + /> + } + /> + { }); describe("ServerSettings worktree defaults", () => { + it("enables the built-in browser for legacy configs", () => { + expect(decodeServerSettings({}).enableBuiltInBrowser).toBe(true); + }); + it("defaults start-from-origin off for legacy configs", () => { expect(decodeServerSettings({}).newWorktreesStartFromOrigin).toBe(false); }); diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index 6ccd65533dd..10b50e7c5e4 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -365,6 +365,7 @@ export const DEFAULT_AUTOMATIC_GIT_FETCH_INTERVAL = Duration.seconds(30); export const ServerSettings = Schema.Struct({ enableAssistantStreaming: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), + enableBuiltInBrowser: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), enableProviderUpdateChecks: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), automaticGitFetchInterval: Schema.DurationFromMillis.pipe( Schema.withDecodingDefault( @@ -504,6 +505,7 @@ const OpenCodeSettingsPatch = Schema.Struct({ export const ServerSettingsPatch = Schema.Struct({ // Server settings enableAssistantStreaming: Schema.optionalKey(Schema.Boolean), + enableBuiltInBrowser: Schema.optionalKey(Schema.Boolean), enableProviderUpdateChecks: Schema.optionalKey(Schema.Boolean), automaticGitFetchInterval: Schema.optionalKey(Schema.DurationFromMillis), defaultThreadEnvMode: Schema.optionalKey(ThreadEnvMode),