fix(server): plugins reject keys they do not report as settings, and Codex opening reports the speed it runs

A plugin session applied the settings it knew and silently stored the rest; it now rejects
a patch that names a key the plugin does not report. Codex opening on a model without the
saved speed ran Normal while the stored options still said priority; opening now reports
the tier it runs, as a later model switch does.
This commit is contained in:
Mohamed Boudra
2026-11-01 16:35:18 +01:00
parent c2677a2690
commit 993eef1668
4 changed files with 49 additions and 8 deletions
@@ -1136,6 +1136,18 @@ describe("PluginAgentClientRegistry", () => {
await session.close();
});
test("rejects a key the plugin does not report as a setting, and configures none of a mixed patch", async () => {
const { harness, session } = await openStubbornVoiceSession({ launch_key: "original" });
const configureBefore = lastConfigure(harness.inputs);
await expect(session.applyOptions?.({ loud: true, launch_key: "changed" })).rejects.toThrow(
/launch_key.*cannot change on a running session/,
);
expect(lastConfigure(harness.inputs)).toBe(configureBefore);
await session.close();
});
test("applying options returns the settings that ended up different, and emits nothing", async () => {
const { session, events } = await openStubbornVoiceSession({});
@@ -64,6 +64,7 @@ import {
createProviderEnv,
type ProviderRuntimeSettings,
} from "./provider-launch-config.js";
import { ProviderOptionsValidationError } from "./provider-options.js";
import type { RegisteredProviderDefinition } from "./provider-registry.js";
import { runProviderTurn } from "./providers/provider-runner.js";
import { StaleProviderSessionError } from "./stale-provider-session-error.js";
@@ -1437,11 +1438,20 @@ class PluginAgentSession implements AgentSession {
async applyOptions(options: ProviderOptions): Promise<Record<string, JsonValue>> {
this.assertReplayComplete();
const before = this.currentSettings();
// A key the plugin does not report as a setting is one it only reads when the session opens.
const unsupported = Object.keys(options).filter((key) => !(key in before));
if (unsupported.length > 0) {
throw new ProviderOptionsValidationError(
this.provider,
unsupported.map((key) => ({
path: [key],
message: "cannot change on a running session; it applies when the agent is opened",
})),
);
}
const settings: Record<string, JsonValue> = {};
for (const setting of this.bridge.config.settings) {
const value = options[setting.id];
if (value === undefined) continue;
settings[setting.id] = toJsonValue(value, `setting '${setting.id}'`);
for (const [id, value] of Object.entries(options)) {
settings[id] = toJsonValue(value, `setting '${id}'`);
}
if (Object.keys(settings).length === 0) return {};
this.applyingOptions = true;
@@ -558,6 +558,21 @@ describe("Codex app-server provider controls", () => {
});
});
test("opening on a model without the saved tier runs Normal and the open patch says so", async () => {
const models = [
{ id: "plain", isDefault: true, defaultReasoningEffort: "medium", serviceTiers: [] },
];
const { session, appServer } = await createConnectedSession(
{ model: "plain", providerOptions: { service_tier: "priority" } },
{ models },
);
expect(session.openOptionsPatch).toEqual({ service_tier: "default", fast_mode: null });
await session.startTurn("hello");
await expect(appServer.waitForTurnStart()).resolves.toMatchObject({ serviceTier: "default" });
});
test("a saved service_tier: fast runs as the model's Fast tier and the open patch names that tier", async () => {
const { session, appServer } = await createConnectedSession({
providerOptions: { service_tier: "fast" },
@@ -3873,11 +3873,15 @@ export class CodexAppServerAgentSession implements AgentSession {
const requested = this.requestedServiceTier;
const resolved = this.resolveServiceTier(requested);
const legacy = this.providerOptions.fast_mode !== undefined || requested !== resolved;
if (!legacy || resolved === null) return;
if (!legacy) return;
// A tier the opening model lacks runs as Normal, and the options say so (as when the model
// is switched later).
const runs = resolved ?? (requested === null ? null : "default");
if (runs === null) return;
const { fast_mode: _legacy, ...rest } = this.providerOptions;
this.providerOptions = { ...rest, service_tier: resolved };
this.requestedServiceTier = resolved;
this.openOptionsPatch = { service_tier: resolved, fast_mode: null };
this.providerOptions = { ...rest, service_tier: runs };
this.requestedServiceTier = runs;
this.openOptionsPatch = { service_tier: runs, fast_mode: null };
}
private get serviceTier(): string | null {