From 1da44f4c59af3e52192fa36609aa99b563cf326d Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Sun, 9 Aug 2026 23:25:07 -0400 Subject: [PATCH] fix(workspaces): validate remote launch option overrides --- config/reliability-gates.jsonc | 15 +++++--- src/main/runtime/orca-runtime.test.ts | 29 ++++++++++++++++ src/main/runtime/orca-runtime.ts | 3 ++ .../runtime/rpc/methods/worktree-schemas.ts | 9 +++-- src/main/runtime/rpc/schemas.test.ts | 12 +++++++ src/shared/agent-session-option-launch.ts | 34 ++++++++++++------- .../tui-agent-startup-session-options.test.ts | 19 +++++++++++ 7 files changed, 103 insertions(+), 18 deletions(-) diff --git a/config/reliability-gates.jsonc b/config/reliability-gates.jsonc index 032df83ea75..a2993e76354 100644 --- a/config/reliability-gates.jsonc +++ b/config/reliability-gates.jsonc @@ -10279,7 +10279,7 @@ "oracle": "Send a linked-task draft and session options through the real worktree.create schema and handler. Require Git, nested SSH, and folder hosts to emit the selected model and effort in the exact startup command, return the actual applied subset with the startup-terminal receipt, and publish no client-authored command. Feed that receipt through Tasks completion and activation; require the host tab to own draft/session state, zero fallback or wake creation, and no applied cache entry when a legacy receipt omits acknowledgement. On a physical paired Windows host, compare authoritative PTY inventory before and after the fix and require Codex plus Setup only, a running Codex process, the unsent issue draft, completed Setup, and no ConPTY DA1 text.", "commands": [ "pnpm exec vitest run --config config/vitest.config.ts src/shared/tui-agent-startup-session-options.test.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/rpc/schemas.test.ts src/main/runtime/rpc/methods/worktree.test.ts src/renderer/src/hooks/useComposerState-host-context-boundaries.test.ts src/renderer/src/store/slices/worktrees.test.ts src/renderer/src/lib/worktree-creation-flow.test.ts src/renderer/src/lib/worktree-creation-flow-startup.test.ts src/renderer/src/lib/worktree-creation-agent-seeds.test.ts src/renderer/src/lib/worktree-activation.test.ts", - "Tasks -> GitHub issue -> Codex -> windows 2 paired host" + "Tasks -> GitHub issue #13320 -> create on Windows 2 paired host with Codex" ], "testFiles": [ "src/shared/tui-agent-startup-session-options.test.ts", @@ -10306,7 +10306,14 @@ { "file": "src/shared/tui-agent-startup-session-options.test.ts", "assertions": [ - "option values with the wrong catalog kind are neither encoded nor acknowledged" + "option values with the wrong catalog kind are neither encoded nor acknowledged", + "invalid requested replacements preserve valid host-configured agent arguments" + ] + }, + { + "file": "src/main/runtime/rpc/schemas.test.ts", + "assertions": [ + "draft session options cannot be combined with a competing startup agent or command" ] }, { @@ -10351,14 +10358,14 @@ "result": "passed", "command": "pnpm exec vitest run --config config/vitest.config.ts src/shared/tui-agent-startup-session-options.test.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/rpc/schemas.test.ts src/main/runtime/rpc/methods/worktree.test.ts src/renderer/src/hooks/useComposerState-host-context-boundaries.test.ts src/renderer/src/store/slices/worktrees.test.ts src/renderer/src/lib/worktree-creation-flow.test.ts src/renderer/src/lib/worktree-creation-flow-startup.test.ts src/renderer/src/lib/worktree-creation-agent-seeds.test.ts src/renderer/src/lib/worktree-activation.test.ts", "durationSeconds": 12, - "summary": "1,516 deterministic renderer, RPC, Git, SSH, folder, launch-command, compatibility, and activation tests passed; one unrelated test was skipped." + "summary": "1,518 deterministic renderer, RPC, Git, SSH, folder, launch-command, compatibility, and activation tests passed; one unrelated test was skipped." }, { "date": "2026-08-09", "runner": "manual", "platform": "windows", "result": "passed", - "command": "Tasks -> GitHub issue -> Codex -> windows 2 paired host", + "command": "Tasks -> GitHub issue #13320 -> create on Windows 2 paired host with Codex", "durationSeconds": 60, "summary": "Baseline had two blank shells plus Setup and no Codex. Candidate had Codex plus Setup only, Codex running with the issue URL drafted, Setup complete, and no DA1 text." } diff --git a/src/main/runtime/orca-runtime.test.ts b/src/main/runtime/orca-runtime.test.ts index f84ec48cd12..a221626eb24 100644 --- a/src/main/runtime/orca-runtime.test.ts +++ b/src/main/runtime/orca-runtime.test.ts @@ -42672,6 +42672,35 @@ describe('OrcaRuntimeService', () => { expect(addWorktree).not.toHaveBeenCalled() }) + it('rejects startup draft options combined with another startup command', async () => { + const runtime = new OrcaRuntimeService(store as never) + const startupSessionOptions = { + agent: 'codex' as const, + values: { model: 'gpt-5.6-sol', effort: 'high' } + } + + await expect( + runtime.createManagedWorktree({ + repoSelector: TEST_REPO_ID, + name: 'ambiguous-startup', + startupAgent: 'codex', + startupDraft: 'https://github.com/stablyai/orca/issues/123', + startupSessionOptions + }) + ).rejects.toThrow('Startup session options require a draft without another startup command.') + await expect( + runtime.createManagedWorktree({ + repoSelector: TEST_REPO_ID, + name: 'ambiguous-startup', + startup: { command: 'codex' }, + startupDraft: 'https://github.com/stablyai/orca/issues/123', + startupSessionOptions + }) + ).rejects.toThrow('Startup session options require a draft without another startup command.') + + expect(addWorktree).not.toHaveBeenCalled() + }) + it('launches explicit startup agents with prompts for CLI-created worktrees', async () => { const metaById: Record = {} const runtimeStore = { diff --git a/src/main/runtime/orca-runtime.ts b/src/main/runtime/orca-runtime.ts index 2c9d716c0d7..01b67bcd98f 100644 --- a/src/main/runtime/orca-runtime.ts +++ b/src/main/runtime/orca-runtime.ts @@ -21735,6 +21735,9 @@ export class OrcaRuntimeService { if (!this.store) { throw new Error('runtime_unavailable') } + if (args.startupSessionOptions && (!args.startupDraft || args.startupAgent || args.startup)) { + throw new Error('Startup session options require a draft without another startup command.') + } const repo = await this.resolveRepoSelector(args.repoSelector) const createSettings = this.store.getSettings() diff --git a/src/main/runtime/rpc/methods/worktree-schemas.ts b/src/main/runtime/rpc/methods/worktree-schemas.ts index 33c2b69bd68..e1df0956127 100644 --- a/src/main/runtime/rpc/methods/worktree-schemas.ts +++ b/src/main/runtime/rpc/methods/worktree-schemas.ts @@ -212,11 +212,16 @@ export const WorktreeCreate = z message: 'startupPrompt requires startupAgent' }) } - if (params.startupSessionOptions !== undefined && params.startupDraft === undefined) { + if ( + params.startupSessionOptions !== undefined && + (params.startupDraft === undefined || + params.startupAgent !== undefined || + params.startupCommand !== undefined) + ) { ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['startupSessionOptions'], - message: 'startupSessionOptions requires startupDraft' + message: 'startupSessionOptions requires startupDraft without an explicit startup command' }) } }) diff --git a/src/main/runtime/rpc/schemas.test.ts b/src/main/runtime/rpc/schemas.test.ts index 2ce98106cee..2d4026b9ee3 100644 --- a/src/main/runtime/rpc/schemas.test.ts +++ b/src/main/runtime/rpc/schemas.test.ts @@ -205,6 +205,18 @@ describe('RPC optional pipe schemas', () => { repo: 'id:repo-github', startupSessionOptions: { agent: 'codex', values: { model: 'gpt-5.6-sol' } } }) + expectRejects(create, { + repo: 'id:repo-github', + startupAgent: 'codex', + startupDraft: 'https://github.com/acme/app/pull/123', + startupSessionOptions: { agent: 'codex', values: { model: 'gpt-5.6-sol' } } + }) + expectRejects(create, { + repo: 'id:repo-github', + startupCommand: 'codex', + startupDraft: 'https://github.com/acme/app/pull/123', + startupSessionOptions: { agent: 'codex', values: { model: 'gpt-5.6-sol' } } + }) expectRejects(create, { repo: 'id:repo-github', startupDraft: 'https://github.com/acme/app/pull/123', diff --git a/src/shared/agent-session-option-launch.ts b/src/shared/agent-session-option-launch.ts index 614f5434e02..40f16e80767 100644 --- a/src/shared/agent-session-option-launch.ts +++ b/src/shared/agent-session-option-launch.ts @@ -1,5 +1,6 @@ import type { AgentType } from './agent-status-types' import { findCatalogModel, getAgentSessionOptionCatalog } from './agent-session-option-catalog' +import type { CatalogOption } from './agent-session-option-catalog-types' import type { SessionOptionValue } from './native-chat-session-options' export type ResolvedSessionOptionLaunch = { @@ -21,7 +22,12 @@ export function removeOverriddenAgentSessionArgs( const model = findCatalogModel(catalog, modelId) const modelOptions = model?.options ?? catalog.unknownModelOptions ?? [] for (const option of modelOptions) { - if (values[option.id] !== undefined && option.apply.removeAgentArgs) { + const explicitValue = values[option.id] + if ( + explicitValue !== undefined && + optionValueIsValid(option, explicitValue, Boolean(model)) && + option.apply.removeAgentArgs + ) { result = option.apply.removeAgentArgs(result) } } @@ -48,17 +54,7 @@ export function resolveAgentSessionOptionLaunch( modelOptions.flatMap((option) => { const explicitValue = values[option.id] if (explicitValue !== undefined) { - if ( - (option.kind.type === 'boolean' && typeof explicitValue !== 'boolean') || - (option.kind.type === 'select' && typeof explicitValue !== 'string') - ) { - return [] - } - if ( - !model && - option.kind.type === 'select' && - !option.kind.choices.some((choice) => choice.value === explicitValue) - ) { + if (!optionValueIsValid(option, explicitValue, Boolean(model))) { return [] } return [[option.id, explicitValue]] @@ -98,3 +94,17 @@ export function resolveAgentSessionOptionLaunch( } return { args, appliedValues } } + +function optionValueIsValid( + option: CatalogOption, + value: SessionOptionValue, + hasKnownModel: boolean +): boolean { + if (option.kind.type === 'boolean') { + return typeof value === 'boolean' + } + return ( + typeof value === 'string' && + (hasKnownModel || option.kind.choices.some((choice) => choice.value === value)) + ) +} diff --git a/src/shared/tui-agent-startup-session-options.test.ts b/src/shared/tui-agent-startup-session-options.test.ts index afcf42f7a1e..82f546651d8 100644 --- a/src/shared/tui-agent-startup-session-options.test.ts +++ b/src/shared/tui-agent-startup-session-options.test.ts @@ -201,4 +201,23 @@ describe('tui agent startup session options', () => { expect(resolved.appliedSessionOptions).toEqual({ model: 'gpt-5.3-codex' }) } }) + + it('preserves configured arguments when their requested replacement has the wrong type', () => { + const plan = buildAgentStartupPlan({ + agent: 'codex', + prompt: '', + cmdOverrides: {}, + platform: 'linux', + allowEmptyPromptLaunch: true, + sessionOptions: { model: 'gpt-5.6-sol', effort: true }, + sessionOptionsOverrideAgentArgs: true, + agentArgs: + '-m host-model -c model_reasoning_effort=low --dangerously-bypass-approvals-and-sandbox' + }) + + expect(plan?.launchCommand).toBe( + "codex '-c' 'model_reasoning_effort=low' '--dangerously-bypass-approvals-and-sandbox' '-m' 'gpt-5.6-sol'" + ) + expect(plan?.sessionOptions).toEqual({ model: 'gpt-5.6-sol' }) + }) })