Use safer automatic approval modes by default (#2213)
* feat(providers): default to safer automatic approvals * fix(providers): preserve defaults on older Codex versions * test(providers): cover Claude automatic approval default * fix(providers): preserve Claude defaults on cloud transports * fix(providers): resolve defaults from launch capabilities
This commit is contained in:
@@ -575,6 +575,25 @@ describe("resolveFormState", () => {
|
|||||||
expect(resolved.modeId).toBe("workspace-write");
|
expect(resolved.modeId).toBe("workspace-write");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("falls back when the provider cannot advertise its preferred default mode", () => {
|
||||||
|
const providerMap = makeProviderMap({
|
||||||
|
...TEST_CODEX_DEFINITION,
|
||||||
|
defaultModeId: "auto-review",
|
||||||
|
modes: TEST_CODEX_DEFINITION.modes,
|
||||||
|
});
|
||||||
|
|
||||||
|
const resolved = resolveFormState(
|
||||||
|
undefined,
|
||||||
|
{ provider: "codex" },
|
||||||
|
CODEX_MODELS,
|
||||||
|
INITIAL_USER_MODIFIED,
|
||||||
|
makeState({ provider: "codex" }).form,
|
||||||
|
providerMap,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(resolved.modeId).toBe("auto");
|
||||||
|
});
|
||||||
|
|
||||||
it("ignores disabled ready providers when resolving selectable defaults", () => {
|
it("ignores disabled ready providers when resolving selectable defaults", () => {
|
||||||
const entries: ProviderSnapshotEntry[] = [
|
const entries: ProviderSnapshotEntry[] = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -171,7 +171,12 @@ function resolvePreferredModeId(input: {
|
|||||||
const preferredModeId = normalizeSelectedModeId(input.preferredModeId);
|
const preferredModeId = normalizeSelectedModeId(input.preferredModeId);
|
||||||
if (preferredModeId) return preferredModeId;
|
if (preferredModeId) return preferredModeId;
|
||||||
|
|
||||||
return input.providerDef?.defaultModeId ?? input.providerDef?.modes[0]?.id ?? "";
|
const defaultModeId = input.providerDef?.defaultModeId;
|
||||||
|
const modes = input.providerDef?.modes ?? [];
|
||||||
|
if (defaultModeId && (modes.length === 0 || modes.some((mode) => mode.id === defaultModeId))) {
|
||||||
|
return defaultModeId;
|
||||||
|
}
|
||||||
|
return modes[0]?.id ?? "";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mergeSelectedComposerPreferences(args: {
|
export function mergeSelectedComposerPreferences(args: {
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ export const AGENT_PROVIDER_DEFINITIONS: AgentProviderDefinition[] = [
|
|||||||
id: "claude",
|
id: "claude",
|
||||||
label: "Claude",
|
label: "Claude",
|
||||||
description: "Anthropic's multi-tool assistant with MCP support, streaming, and deep reasoning",
|
description: "Anthropic's multi-tool assistant with MCP support, streaming, and deep reasoning",
|
||||||
defaultModeId: "default",
|
defaultModeId: "auto",
|
||||||
modes: CLAUDE_MODES,
|
modes: CLAUDE_MODES,
|
||||||
voice: {
|
voice: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@@ -197,7 +197,7 @@ export const AGENT_PROVIDER_DEFINITIONS: AgentProviderDefinition[] = [
|
|||||||
id: "codex",
|
id: "codex",
|
||||||
label: "Codex",
|
label: "Codex",
|
||||||
description: "OpenAI's Codex workspace agent with sandbox controls and optional network access",
|
description: "OpenAI's Codex workspace agent with sandbox controls and optional network access",
|
||||||
defaultModeId: "auto",
|
defaultModeId: "auto-review",
|
||||||
modes: CODEX_MODES,
|
modes: CODEX_MODES,
|
||||||
voice: {
|
voice: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import type {
|
|||||||
AgentStreamEvent,
|
AgentStreamEvent,
|
||||||
AgentTimelineItem,
|
AgentTimelineItem,
|
||||||
ImportProviderSessionInput,
|
ImportProviderSessionInput,
|
||||||
|
ResolveAgentDefaultModeInput,
|
||||||
} from "./agent-sdk-types.js";
|
} from "./agent-sdk-types.js";
|
||||||
import type { PaseoToolCatalog } from "./tools/types.js";
|
import type { PaseoToolCatalog } from "./tools/types.js";
|
||||||
import type { ProviderDefinition } from "./provider-registry.js";
|
import type { ProviderDefinition } from "./provider-registry.js";
|
||||||
@@ -111,11 +112,15 @@ function expectArchivedAgentRecord(
|
|||||||
}
|
}
|
||||||
|
|
||||||
class TestAgentClient implements AgentClient {
|
class TestAgentClient implements AgentClient {
|
||||||
readonly provider = "codex" as const;
|
readonly provider: AgentProvider;
|
||||||
readonly capabilities = TEST_CAPABILITIES;
|
readonly capabilities = TEST_CAPABILITIES;
|
||||||
readonly createdConfigs: AgentSessionConfig[] = [];
|
readonly createdConfigs: AgentSessionConfig[] = [];
|
||||||
readonly resumeOverrides: Array<Partial<AgentSessionConfig> | undefined> = [];
|
readonly resumeOverrides: Array<Partial<AgentSessionConfig> | undefined> = [];
|
||||||
|
|
||||||
|
constructor(provider: AgentProvider = "codex") {
|
||||||
|
this.provider = provider;
|
||||||
|
}
|
||||||
|
|
||||||
async isAvailable(): Promise<boolean> {
|
async isAvailable(): Promise<boolean> {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -129,18 +134,18 @@ class TestAgentClient implements AgentClient {
|
|||||||
return {
|
return {
|
||||||
models: [
|
models: [
|
||||||
{
|
{
|
||||||
provider: "codex",
|
provider: this.provider,
|
||||||
id: "gpt-5.4",
|
id: "gpt-5.4",
|
||||||
label: "GPT-5.4",
|
label: "GPT-5.4",
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provider: "codex",
|
provider: this.provider,
|
||||||
id: "gpt-5.4-mini",
|
id: "gpt-5.4-mini",
|
||||||
label: "GPT-5.4 Mini",
|
label: "GPT-5.4 Mini",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provider: "codex",
|
provider: this.provider,
|
||||||
id: "gpt-5.2-codex",
|
id: "gpt-5.2-codex",
|
||||||
label: "GPT-5.2 Codex",
|
label: "GPT-5.2 Codex",
|
||||||
},
|
},
|
||||||
@@ -156,7 +161,7 @@ class TestAgentClient implements AgentClient {
|
|||||||
): Promise<AgentSession> {
|
): Promise<AgentSession> {
|
||||||
this.resumeOverrides.push(config);
|
this.resumeOverrides.push(config);
|
||||||
return new TestAgentSession({
|
return new TestAgentSession({
|
||||||
provider: "codex",
|
provider: this.provider,
|
||||||
cwd: config?.cwd ?? process.cwd(),
|
cwd: config?.cwd ?? process.cwd(),
|
||||||
daemonAppendSystemPrompt: config?.daemonAppendSystemPrompt,
|
daemonAppendSystemPrompt: config?.daemonAppendSystemPrompt,
|
||||||
});
|
});
|
||||||
@@ -935,9 +940,43 @@ test("normalizeConfig injects the provider default model when omitted", async ()
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(snapshot.config.model).toBe("gpt-5.4");
|
expect(snapshot.config.model).toBe("gpt-5.4");
|
||||||
|
expect(snapshot.config.modeId).toBe("auto-review");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("normalizeConfig injects Claude's automatic approval default when omitted", async () => {
|
||||||
|
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-claude-default-test-"));
|
||||||
|
const manager = new AgentManager({
|
||||||
|
clients: { claude: new TestAgentClient("claude") },
|
||||||
|
logger,
|
||||||
|
});
|
||||||
|
|
||||||
|
const snapshot = await manager.createAgent({ provider: "claude", cwd: workdir }, undefined, {
|
||||||
|
workspaceId: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
expect(snapshot.config.modeId).toBe("auto");
|
expect(snapshot.config.modeId).toBe("auto");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("normalizeConfig uses a capability-aware provider mode default", async () => {
|
||||||
|
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-mode-default-test-"));
|
||||||
|
class CapabilityAwareClient extends TestAgentClient {
|
||||||
|
override async resolveDefaultModeId(input: ResolveAgentDefaultModeInput): Promise<string> {
|
||||||
|
return input.env?.CLAUDE_CODE_USE_BEDROCK === "1" ? "default" : "auto";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const manager = new AgentManager({
|
||||||
|
clients: { codex: new CapabilityAwareClient() },
|
||||||
|
logger,
|
||||||
|
});
|
||||||
|
|
||||||
|
const snapshot = await manager.createAgent({ provider: "codex", cwd: workdir }, undefined, {
|
||||||
|
workspaceId: undefined,
|
||||||
|
env: { CLAUDE_CODE_USE_BEDROCK: "1" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(snapshot.config.modeId).toBe("default");
|
||||||
|
});
|
||||||
|
|
||||||
test("createAgent forwards request env into the spawned provider process", async () => {
|
test("createAgent forwards request env into the spawned provider process", async () => {
|
||||||
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-env-test-"));
|
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-env-test-"));
|
||||||
const client = new EnvProbeAgentClient();
|
const client = new EnvProbeAgentClient();
|
||||||
@@ -997,7 +1036,7 @@ test("normalizeConfig strips legacy 'default' model id", async () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(snapshot.config.model).toBe("gpt-5.4");
|
expect(snapshot.config.model).toBe("gpt-5.4");
|
||||||
expect(snapshot.config.modeId).toBe("auto");
|
expect(snapshot.config.modeId).toBe("auto-review");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("listDraftCommands returns no commands without guessing a missing model", async () => {
|
test("listDraftCommands returns no commands without guessing a missing model", async () => {
|
||||||
@@ -1094,7 +1133,7 @@ test("listDraftCommands uses explicit model config without default model fetchin
|
|||||||
provider: "codex",
|
provider: "codex",
|
||||||
cwd: workdir,
|
cwd: workdir,
|
||||||
model: "gpt-5.4",
|
model: "gpt-5.4",
|
||||||
modeId: "auto",
|
modeId: "auto-review",
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
@@ -1194,7 +1233,7 @@ test("listDraftFeatures uses explicit model config without default model fetchin
|
|||||||
provider: "codex",
|
provider: "codex",
|
||||||
cwd: workdir,
|
cwd: workdir,
|
||||||
model: "gpt-5.4",
|
model: "gpt-5.4",
|
||||||
modeId: "auto",
|
modeId: "auto-review",
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
@@ -1651,7 +1690,7 @@ test("createAgent passes daemon launch env through the provider launch context",
|
|||||||
provider: "codex",
|
provider: "codex",
|
||||||
cwd: workdir,
|
cwd: workdir,
|
||||||
model: "gpt-5.4",
|
model: "gpt-5.4",
|
||||||
modeId: "auto",
|
modeId: "auto-review",
|
||||||
});
|
});
|
||||||
expect(client.lastLaunchContext).toEqual({
|
expect(client.lastLaunchContext).toEqual({
|
||||||
agentId: snapshot.id,
|
agentId: snapshot.id,
|
||||||
@@ -2448,7 +2487,7 @@ test("resumeAgentFromPersistence keeps metadata config, applies overrides, and p
|
|||||||
});
|
});
|
||||||
expect(client.lastResumeOverrides).toMatchObject({
|
expect(client.lastResumeOverrides).toMatchObject({
|
||||||
model: "gpt-5.4",
|
model: "gpt-5.4",
|
||||||
modeId: "auto",
|
modeId: "auto-review",
|
||||||
systemPrompt: "new prompt",
|
systemPrompt: "new prompt",
|
||||||
mcpServers: {
|
mcpServers: {
|
||||||
paseo: {
|
paseo: {
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ interface PreparedSessionConfig {
|
|||||||
|
|
||||||
interface NormalizeConfigOptions {
|
interface NormalizeConfigOptions {
|
||||||
resolveDefaultModel?: boolean;
|
resolveDefaultModel?: boolean;
|
||||||
|
env?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TimeoutOptions {
|
interface TimeoutOptions {
|
||||||
@@ -1034,7 +1035,11 @@ export class AgentManager {
|
|||||||
this.assertAcceptingAgentRegistrations();
|
this.assertAcceptingAgentRegistrations();
|
||||||
const resolvedAgentId = validateAgentId(agentId ?? this.idFactory(), "createAgent");
|
const resolvedAgentId = validateAgentId(agentId ?? this.idFactory(), "createAgent");
|
||||||
await this.deleteAgentState(resolvedAgentId);
|
await this.deleteAgentState(resolvedAgentId);
|
||||||
const { storedConfig, launchConfig } = await this.prepareSessionConfig(config, resolvedAgentId);
|
const { storedConfig, launchConfig } = await this.prepareSessionConfig(
|
||||||
|
config,
|
||||||
|
resolvedAgentId,
|
||||||
|
options?.env,
|
||||||
|
);
|
||||||
this.requireEnabledProvider(storedConfig.provider);
|
this.requireEnabledProvider(storedConfig.provider);
|
||||||
const client = await this.requireAvailableClient({
|
const client = await this.requireAvailableClient({
|
||||||
provider: storedConfig.provider,
|
provider: storedConfig.provider,
|
||||||
@@ -4075,17 +4080,27 @@ export class AgentManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!normalized.modeId) {
|
if (!normalized.modeId) {
|
||||||
try {
|
normalized.modeId = await this.resolveDefaultModeId(normalized, options.env);
|
||||||
normalized.modeId =
|
|
||||||
getAgentProviderDefinition(normalized.provider).defaultModeId ?? undefined;
|
|
||||||
} catch {
|
|
||||||
// Unknown provider
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async resolveDefaultModeId(
|
||||||
|
config: AgentSessionConfig,
|
||||||
|
env?: Record<string, string>,
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
const providerDefault = await this.clients
|
||||||
|
.get(config.provider)
|
||||||
|
?.resolveDefaultModeId?.({ config, env });
|
||||||
|
if (providerDefault) return providerDefault;
|
||||||
|
try {
|
||||||
|
return getAgentProviderDefinition(config.provider).defaultModeId ?? undefined;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async resolveDefaultModelId(config: AgentSessionConfig): Promise<string | undefined> {
|
private async resolveDefaultModelId(config: AgentSessionConfig): Promise<string | undefined> {
|
||||||
const client = this.clients.get(config.provider);
|
const client = this.clients.get(config.provider);
|
||||||
if (!client) {
|
if (!client) {
|
||||||
@@ -4107,8 +4122,9 @@ export class AgentManager {
|
|||||||
private async prepareSessionConfig(
|
private async prepareSessionConfig(
|
||||||
config: AgentSessionConfig,
|
config: AgentSessionConfig,
|
||||||
agentId: string,
|
agentId: string,
|
||||||
|
env?: Record<string, string>,
|
||||||
): Promise<PreparedSessionConfig> {
|
): Promise<PreparedSessionConfig> {
|
||||||
const storedConfig = await this.normalizeConfig(stripInternalPaseoMcpServer(config));
|
const storedConfig = await this.normalizeConfig(stripInternalPaseoMcpServer(config), { env });
|
||||||
const launchConfig = this.applyDaemonAppendSystemPrompt(
|
const launchConfig = this.applyDaemonAppendSystemPrompt(
|
||||||
withRuntimePaseoMcpServer({
|
withRuntimePaseoMcpServer({
|
||||||
config: storedConfig,
|
config: storedConfig,
|
||||||
|
|||||||
@@ -668,6 +668,12 @@ export type FetchCatalogOptions =
|
|||||||
export interface ProviderCatalog {
|
export interface ProviderCatalog {
|
||||||
models: AgentModelDefinition[];
|
models: AgentModelDefinition[];
|
||||||
modes: AgentMode[];
|
modes: AgentMode[];
|
||||||
|
defaultModeId?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResolveAgentDefaultModeInput {
|
||||||
|
config: AgentSessionConfig;
|
||||||
|
env?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AgentClient {
|
export interface AgentClient {
|
||||||
@@ -690,6 +696,7 @@ export interface AgentClient {
|
|||||||
* The registry is responsible for merging configured model overrides.
|
* The registry is responsible for merging configured model overrides.
|
||||||
*/
|
*/
|
||||||
fetchCatalog(options: FetchCatalogOptions): Promise<ProviderCatalog>;
|
fetchCatalog(options: FetchCatalogOptions): Promise<ProviderCatalog>;
|
||||||
|
resolveDefaultModeId?(input: ResolveAgentDefaultModeInput): Promise<string | undefined>;
|
||||||
resolveCreateConfig?(input: ResolveAgentCreateConfigInput): ResolveAgentCreateConfigResult;
|
resolveCreateConfig?(input: ResolveAgentCreateConfigInput): ResolveAgentCreateConfigResult;
|
||||||
isCreateConfigUnattended?(input: AgentCreateConfigUnattendedInput): boolean;
|
isCreateConfigUnattended?(input: AgentCreateConfigUnattendedInput): boolean;
|
||||||
listCommands?(config: AgentSessionConfig): Promise<AgentSlashCommand[]>;
|
listCommands?(config: AgentSessionConfig): Promise<AgentSlashCommand[]>;
|
||||||
|
|||||||
@@ -814,7 +814,7 @@ test("enabled: false keeps provider metadata in registry", () => {
|
|||||||
id: "claude",
|
id: "claude",
|
||||||
label: "Claude",
|
label: "Claude",
|
||||||
description: "Anthropic's multi-tool assistant with MCP support, streaming, and deep reasoning",
|
description: "Anthropic's multi-tool assistant with MCP support, streaming, and deep reasoning",
|
||||||
defaultModeId: "default",
|
defaultModeId: "auto",
|
||||||
enabled: false,
|
enabled: false,
|
||||||
});
|
});
|
||||||
expect(registry.claude.modes).toEqual(
|
expect(registry.claude.modes).toEqual(
|
||||||
@@ -1487,6 +1487,31 @@ describe("fetchCatalog", () => {
|
|||||||
expect(catalog.models.map((model) => model.id)).toEqual(["profile-model", "extra-model"]);
|
expect(catalog.models.map((model) => model.id)).toEqual(["profile-model", "extra-model"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("replacement models still resolve the provider's capability-aware default mode", async () => {
|
||||||
|
const resolveDefaultModeId = vi.fn(async () => "default");
|
||||||
|
const injectedClient = {
|
||||||
|
provider: "codex",
|
||||||
|
capabilities: {},
|
||||||
|
resolveDefaultModeId,
|
||||||
|
isAvailable: vi.fn(async () => true),
|
||||||
|
} satisfies Partial<AgentClient> as AgentClient;
|
||||||
|
const registry = buildProviderRegistry(logger, {
|
||||||
|
providerOverrides: {
|
||||||
|
codex: { models: [{ id: "profile-model", label: "Profile Model" }] },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const catalog = await registry.codex.fetchCatalog(
|
||||||
|
{ scope: "workspace", cwd: "/tmp/catalog", force: false },
|
||||||
|
injectedClient,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(catalog.defaultModeId).toBe("default");
|
||||||
|
expect(resolveDefaultModeId).toHaveBeenCalledWith({
|
||||||
|
config: { provider: "codex", cwd: "/tmp/catalog" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test("additionalModels can override replacement model fields", async () => {
|
test("additionalModels can override replacement model fields", async () => {
|
||||||
const registry = buildProviderRegistry(logger, {
|
const registry = buildProviderRegistry(logger, {
|
||||||
providerOverrides: {
|
providerOverrides: {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import type {
|
|||||||
ProviderCatalog,
|
ProviderCatalog,
|
||||||
ResolveAgentCreateConfigInput,
|
ResolveAgentCreateConfigInput,
|
||||||
ResolveAgentCreateConfigResult,
|
ResolveAgentCreateConfigResult,
|
||||||
|
ResolveAgentDefaultModeInput,
|
||||||
} from "./agent-sdk-types.js";
|
} from "./agent-sdk-types.js";
|
||||||
import {
|
import {
|
||||||
isDefaultAgentCreateConfigUnattended,
|
isDefaultAgentCreateConfigUnattended,
|
||||||
@@ -427,12 +428,20 @@ function wrapClientProvider(
|
|||||||
fetchCatalog: async (options) => {
|
fetchCatalog: async (options) => {
|
||||||
const catalog = await inner.fetchCatalog(options);
|
const catalog = await inner.fetchCatalog(options);
|
||||||
return {
|
return {
|
||||||
|
...catalog,
|
||||||
models: mergeModels(provider, profileModels, additionalModels, catalog.models, {
|
models: mergeModels(provider, profileModels, additionalModels, catalog.models, {
|
||||||
profileModelsAreAdditive,
|
profileModelsAreAdditive,
|
||||||
}),
|
}),
|
||||||
modes: catalog.modes,
|
modes: catalog.modes,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
resolveDefaultModeId: inner.resolveDefaultModeId
|
||||||
|
? async ({ config, env }: ResolveAgentDefaultModeInput) =>
|
||||||
|
await inner.resolveDefaultModeId?.({
|
||||||
|
config: { ...config, provider: inner.provider },
|
||||||
|
env,
|
||||||
|
})
|
||||||
|
: undefined,
|
||||||
resolveCreateConfig: inner.resolveCreateConfig?.bind(inner),
|
resolveCreateConfig: inner.resolveCreateConfig?.bind(inner),
|
||||||
isCreateConfigUnattended: inner.isCreateConfigUnattended?.bind(inner),
|
isCreateConfigUnattended: inner.isCreateConfigUnattended?.bind(inner),
|
||||||
listFeatures: listFeatures
|
listFeatures: listFeatures
|
||||||
@@ -516,17 +525,25 @@ function createRegistryEntry(
|
|||||||
// the single catalog API; otherwise use static/empty modes with no runtime.
|
// the single catalog API; otherwise use static/empty modes with no runtime.
|
||||||
const models = mergeModelAdditions(provider, replacementModels, resolved.additionalModels);
|
const models = mergeModelAdditions(provider, replacementModels, resolved.additionalModels);
|
||||||
if (hasStaticModes) {
|
if (hasStaticModes) {
|
||||||
|
const defaultModeId = await catalogClient.resolveDefaultModeId?.({
|
||||||
|
config: {
|
||||||
|
provider,
|
||||||
|
cwd: options.scope === "workspace" ? options.cwd : process.cwd(),
|
||||||
|
},
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
models,
|
models,
|
||||||
modes: decorateModes(resolved.definition.modes),
|
modes: decorateModes(resolved.definition.modes),
|
||||||
|
defaultModeId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const catalog = await catalogClient.fetchCatalog(options);
|
const catalog = await catalogClient.fetchCatalog(options);
|
||||||
return { models, modes: decorateModes(catalog.modes) };
|
return { ...catalog, models, modes: decorateModes(catalog.modes) };
|
||||||
}
|
}
|
||||||
|
|
||||||
const catalog = await catalogClient.fetchCatalog(options);
|
const catalog = await catalogClient.fetchCatalog(options);
|
||||||
return {
|
return {
|
||||||
|
...catalog,
|
||||||
models: mergeModels(
|
models: mergeModels(
|
||||||
provider,
|
provider,
|
||||||
resolved.profileModels,
|
resolved.profileModels,
|
||||||
|
|||||||
@@ -117,9 +117,11 @@ describe("ProviderSnapshotManager public surface", () => {
|
|||||||
try {
|
try {
|
||||||
const snapshot = manager.getSnapshot("/tmp/project");
|
const snapshot = manager.getSnapshot("/tmp/project");
|
||||||
const claude = snapshot.find((entry) => entry.provider === "claude");
|
const claude = snapshot.find((entry) => entry.provider === "claude");
|
||||||
|
const codex = snapshot.find((entry) => entry.provider === "codex");
|
||||||
expect(claude?.status).toBe("loading");
|
expect(claude?.status).toBe("loading");
|
||||||
expect(claude?.label).toBe("Claude");
|
expect(claude?.label).toBe("Claude");
|
||||||
expect(claude?.defaultModeId).toBe("default");
|
expect(claude?.defaultModeId).toBe("auto");
|
||||||
|
expect(codex?.defaultModeId).toBe("auto-review");
|
||||||
} finally {
|
} finally {
|
||||||
manager.destroy();
|
manager.destroy();
|
||||||
}
|
}
|
||||||
@@ -220,6 +222,34 @@ describe("ProviderSnapshotManager public surface", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("ready snapshots publish the catalog's capability-aware default mode", async () => {
|
||||||
|
const manager = new ProviderSnapshotManager({
|
||||||
|
logger: createTestLogger(),
|
||||||
|
extraClients: {
|
||||||
|
codex: createExtraClient("codex", {
|
||||||
|
isAvailable: async () => true,
|
||||||
|
fetchCatalog: async () => ({
|
||||||
|
models: [],
|
||||||
|
modes: [{ id: "default", label: "Default", description: "Ask before running tools" }],
|
||||||
|
defaultModeId: "default",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const entry = await manager.getProvider({
|
||||||
|
cwd: "/tmp/project",
|
||||||
|
provider: "codex",
|
||||||
|
wait: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(entry).toMatchObject({ status: "ready", defaultModeId: "default" });
|
||||||
|
} finally {
|
||||||
|
manager.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test("explicit refresh re-probes only the requested warm provider", async () => {
|
test("explicit refresh re-probes only the requested warm provider", async () => {
|
||||||
const cwd = "/tmp/project";
|
const cwd = "/tmp/project";
|
||||||
const isAvailableCodex = vi.fn(async () => true);
|
const isAvailableCodex = vi.fn(async () => true);
|
||||||
|
|||||||
@@ -799,6 +799,8 @@ export class ProviderSnapshotManager {
|
|||||||
|
|
||||||
setEntry({
|
setEntry({
|
||||||
...base,
|
...base,
|
||||||
|
defaultModeId:
|
||||||
|
catalog.defaultModeId === undefined ? definition.defaultModeId : catalog.defaultModeId,
|
||||||
status: "ready",
|
status: "ready",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
models: catalog.models,
|
models: catalog.models,
|
||||||
|
|||||||
@@ -218,6 +218,37 @@ test("rejects auto mode when Claude Code uses Bedrock", async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
{ env: {}, expected: "auto" },
|
||||||
|
{ env: { CLAUDE_CODE_USE_BEDROCK: "1" }, expected: "default" },
|
||||||
|
{ env: { CLAUDE_CODE_USE_VERTEX: "true" }, expected: "default" },
|
||||||
|
])("defaults to $expected for transport environment $env", async ({ env, expected }) => {
|
||||||
|
const client = new ClaudeAgentClient({
|
||||||
|
logger: createTestLogger(),
|
||||||
|
runtimeSettings: { env },
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
client.resolveDefaultModeId({
|
||||||
|
config: { provider: "claude", cwd: process.cwd() },
|
||||||
|
}),
|
||||||
|
).resolves.toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("launch environment participates in the Claude default mode", async () => {
|
||||||
|
const client = new ClaudeAgentClient({
|
||||||
|
logger: createTestLogger(),
|
||||||
|
runtimeSettings: { env: {} },
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
client.resolveDefaultModeId({
|
||||||
|
config: { provider: "claude", cwd: process.cwd() },
|
||||||
|
env: { CLAUDE_CODE_USE_BEDROCK: "1" },
|
||||||
|
}),
|
||||||
|
).resolves.toBe("default");
|
||||||
|
});
|
||||||
|
|
||||||
test("allows launch env to disable inherited Bedrock transport for auto mode", async () => {
|
test("allows launch env to disable inherited Bedrock transport for auto mode", async () => {
|
||||||
const previousBedrock = process.env.CLAUDE_CODE_USE_BEDROCK;
|
const previousBedrock = process.env.CLAUDE_CODE_USE_BEDROCK;
|
||||||
process.env.CLAUDE_CODE_USE_BEDROCK = "1";
|
process.env.CLAUDE_CODE_USE_BEDROCK = "1";
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ import {
|
|||||||
type ListImportableSessionsOptions,
|
type ListImportableSessionsOptions,
|
||||||
type McpServerConfig,
|
type McpServerConfig,
|
||||||
type ProviderCatalog,
|
type ProviderCatalog,
|
||||||
|
type ResolveAgentDefaultModeInput,
|
||||||
} from "../../agent-sdk-types.js";
|
} from "../../agent-sdk-types.js";
|
||||||
import { importSessionFromPersistence } from "../../provider-session-import.js";
|
import { importSessionFromPersistence } from "../../provider-session-import.js";
|
||||||
import {
|
import {
|
||||||
@@ -1472,7 +1473,28 @@ export class ClaudeAgentClient implements AgentClient {
|
|||||||
async fetchCatalog(_options: FetchCatalogOptions): Promise<ProviderCatalog> {
|
async fetchCatalog(_options: FetchCatalogOptions): Promise<ProviderCatalog> {
|
||||||
// Claude exposes a global catalog here; cwd/force are intentionally irrelevant.
|
// Claude exposes a global catalog here; cwd/force are intentionally irrelevant.
|
||||||
const models = await getClaudeModelsWithSettings(this.logger, this.configDir);
|
const models = await getClaudeModelsWithSettings(this.logger, this.configDir);
|
||||||
return { models, modes: DEFAULT_MODES };
|
const modes = detectIneligibleAutoModeTransport(
|
||||||
|
createProviderEnv({ baseEnv: process.env, runtimeSettings: this.runtimeSettings }),
|
||||||
|
)
|
||||||
|
? DEFAULT_MODES.filter((mode) => mode.id !== "auto")
|
||||||
|
: DEFAULT_MODES;
|
||||||
|
return {
|
||||||
|
models,
|
||||||
|
modes,
|
||||||
|
defaultModeId: modes.some((mode) => mode.id === "auto") ? "auto" : "default",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async resolveDefaultModeId({
|
||||||
|
config,
|
||||||
|
env: launchEnv,
|
||||||
|
}: ResolveAgentDefaultModeInput): Promise<string> {
|
||||||
|
const env = createProviderEnv({
|
||||||
|
baseEnv: process.env,
|
||||||
|
runtimeSettings: this.runtimeSettings,
|
||||||
|
overlays: [config.extra?.claude?.env, launchEnv],
|
||||||
|
});
|
||||||
|
return detectIneligibleAutoModeTransport(env) ? "default" : "auto";
|
||||||
}
|
}
|
||||||
|
|
||||||
async listFeatures(config: AgentSessionConfig): Promise<AgentFeature[]> {
|
async listFeatures(config: AgentSessionConfig): Promise<AgentFeature[]> {
|
||||||
|
|||||||
@@ -6379,8 +6379,21 @@ export class CodexAppServerAgentClient implements AgentClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fetchCatalog(_options: FetchCatalogOptions): Promise<ProviderCatalog> {
|
async fetchCatalog(_options: FetchCatalogOptions): Promise<ProviderCatalog> {
|
||||||
const models = await this.fetchModelsFromAppServer();
|
const [models, autoReviewEnabled] = await Promise.all([
|
||||||
return { models, modes: CODEX_MODES };
|
this.fetchModelsFromAppServer(),
|
||||||
|
this.resolveAutoReviewEnabled(),
|
||||||
|
]);
|
||||||
|
return {
|
||||||
|
models,
|
||||||
|
defaultModeId: autoReviewEnabled ? "auto-review" : DEFAULT_CODEX_MODE_ID,
|
||||||
|
modes: autoReviewEnabled
|
||||||
|
? CODEX_MODES
|
||||||
|
: CODEX_MODES.filter((mode) => mode.id !== "auto-review"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async resolveDefaultModeId(): Promise<string> {
|
||||||
|
return (await this.resolveAutoReviewEnabled()) ? "auto-review" : DEFAULT_CODEX_MODE_ID;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async fetchModelsFromAppServer(): Promise<AgentModelDefinition[]> {
|
private async fetchModelsFromAppServer(): Promise<AgentModelDefinition[]> {
|
||||||
|
|||||||
Reference in New Issue
Block a user