From 5845a5a98029c0a2d284e8607ead213a07eec499 Mon Sep 17 00:00:00 2001 From: "exe.dev user" Date: Thu, 23 Apr 2026 22:47:10 +0000 Subject: [PATCH] fix(container-runner): honor agent_provider DB columns with session override MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit resolveProviderContribution read only containerConfig.provider (from each group's container.json) and ignored both agent_groups.agent_provider and sessions.agent_provider. The provider-install skills (opencode, codex) and CLAUDE.md document those DB columns as the source of truth with session-overrides-group precedence, but the code never consulted them — so setting `agent_provider = 'codex'` on a group had no effect, and the only way to route to a non-default provider was to edit the per-group JSON directly. Discovered while wiring up Codex: DB update landed but the spawned container kept running Claude. Extract a pure `resolveProviderName(session, group, containerConfig)` with the documented precedence: sessions.agent_provider → agent_groups.agent_provider → container.json `provider` → 'claude' `resolveProviderContribution` now calls it. The container.json fallback stays so existing installs that only set provider in JSON keep working. Empty strings treated as unset to avoid footguns when a DB-backed form writes '' for "no override." Added unit tests covering precedence, null-fallthrough, empty-string fallthrough, and case normalization. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/container-runner.test.ts | 32 ++++++++++++++++++++++++++++++++ src/container-runner.ts | 21 ++++++++++++++++++++- 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 src/container-runner.test.ts diff --git a/src/container-runner.test.ts b/src/container-runner.test.ts new file mode 100644 index 000000000..cd18a7289 --- /dev/null +++ b/src/container-runner.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from 'vitest'; + +import { resolveProviderName } from './container-runner.js'; + +describe('resolveProviderName', () => { + it('prefers session over group and container.json', () => { + expect(resolveProviderName('codex', 'opencode', 'claude')).toBe('codex'); + }); + + it('falls back to group when session is null', () => { + expect(resolveProviderName(null, 'codex', 'claude')).toBe('codex'); + }); + + it('falls back to container.json when session and group are null', () => { + expect(resolveProviderName(null, null, 'opencode')).toBe('opencode'); + }); + + it('defaults to claude when nothing is set', () => { + expect(resolveProviderName(null, null, undefined)).toBe('claude'); + }); + + it('lowercases the resolved name', () => { + expect(resolveProviderName('CODEX', null, null)).toBe('codex'); + expect(resolveProviderName(null, 'OpenCode', null)).toBe('opencode'); + expect(resolveProviderName(null, null, 'Claude')).toBe('claude'); + }); + + it('treats empty string as unset (falls through)', () => { + expect(resolveProviderName('', 'codex', null)).toBe('codex'); + expect(resolveProviderName(null, '', 'opencode')).toBe('opencode'); + }); +}); diff --git a/src/container-runner.ts b/src/container-runner.ts index fca88c490..029b5fe3d 100644 --- a/src/container-runner.ts +++ b/src/container-runner.ts @@ -191,12 +191,31 @@ export function killContainer(sessionId: string, reason: string): void { } } +/** + * Resolve the provider name for a session using the precedence documented in + * the provider-install skills: + * + * sessions.agent_provider + * → agent_groups.agent_provider + * → container.json `provider` + * → 'claude' + * + * Pure so the precedence can be unit-tested without a DB or filesystem. + */ +export function resolveProviderName( + sessionProvider: string | null | undefined, + agentGroupProvider: string | null | undefined, + containerConfigProvider: string | null | undefined, +): string { + return (sessionProvider || agentGroupProvider || containerConfigProvider || 'claude').toLowerCase(); +} + function resolveProviderContribution( session: Session, agentGroup: AgentGroup, containerConfig: import('./container-config.js').ContainerConfig, ): { provider: string; contribution: ProviderContainerContribution } { - const provider = (containerConfig.provider || 'claude').toLowerCase(); + const provider = resolveProviderName(session.agent_provider, agentGroup.agent_provider, containerConfig.provider); const fn = getProviderContainerConfig(provider); const contribution = fn ? fn({