fix(server): Claude and Codex merge supplied options into their running options

setAgentOptions hands the session only the agent's own options, so Claude's
applyOptions no longer replaces the restrictions merged in at open (disallowed
tools, sandbox), and Codex derives its speed from the running record so an
explicit service_tier still beats a legacy fast_mode on an unrelated patch.
This commit is contained in:
Mohamed Boudra
2026-11-01 13:11:41 +01:00
parent e4269f5059
commit d5953d2ea7
4 changed files with 49 additions and 6 deletions
@@ -867,6 +867,34 @@ describe("ClaudeAgentSession features", () => {
await session.close();
});
test("applying options before the first query keeps the session's restrictions", async () => {
const { queryFactory } = createQueryMock();
const client = new ClaudeAgentClient({
logger,
queryFactory,
resolveBinary: async () => "/test/claude/bin",
});
const session = await client.createSession({
provider: "claude",
cwd: process.cwd(),
model: "claude-opus-4-8",
providerOptions: {
disallowedTools: ["Bash"],
sandbox: { enabled: true, failIfUnavailable: true },
},
});
// The manager hands the session only the agent's own options.
await session.applyOptions?.({ fast_mode: true });
await (session as unknown as { ensureQuery(): Promise<unknown> }).ensureQuery();
expect(queryFactory.mock.calls[0]?.[0].options).toMatchObject({
disallowedTools: ["Bash"],
sandbox: { enabled: true, failIfUnavailable: true },
});
await session.close();
});
test("passes extra Claude Code CLI arguments to the SDK", async () => {
const { queryFactory, launches } = createQueryMock();
const client = new ClaudeAgentClient({
@@ -2800,9 +2800,10 @@ class ClaudeAgentSession implements AgentSession {
async applyOptions(options: ProviderOptions): Promise<void> {
const providerOptions =
validateProviderOptions("claude", ClaudeProviderOptionsSchema, options) ?? {};
// Supplied keys update the running options; absent keys keep their running value.
const running = { ...this.config.providerOptions, ...providerOptions };
const hadFast = this.config.providerOptions.fast_mode === true;
// An absent option leaves Fast as it is; only an explicit value changes it.
const wantsFast = providerOptions.fast_mode === undefined ? hadFast : providerOptions.fast_mode;
const wantsFast = running.fast_mode === true;
const modelSupportsFast = claudeModelSupportsFastMode(this.config.model);
if (wantsFast && !hadFast && !modelSupportsFast) {
throw new Error(
@@ -2817,7 +2818,7 @@ class ClaudeAgentSession implements AgentSession {
this.cachedRuntimeInfo = null;
}
// Held until the runtime accepts, so a rejected change is retried instead of skipped.
this.config.providerOptions = { ...providerOptions, ...(wantsFast ? { fast_mode: true } : {}) };
this.config.providerOptions = running;
}
getPendingPermissions(): AgentPermissionRequest[] {
@@ -559,6 +559,18 @@ describe("Codex app-server provider controls", () => {
});
});
test("an explicit tier still beats a legacy fast_mode on a patch that supplies neither", async () => {
const { session, appServer } = await createConnectedSession({
providerOptions: { service_tier: "default", fast_mode: true },
});
// The manager hands the session only the agent's own options.
await session.applyOptions?.({ fast_mode: true, plan_mode: true });
await session.startTurn("hello");
await expect(appServer.waitForTurnStart()).resolves.toMatchObject({ serviceTier: "default" });
});
test("startTurn includes serviceTier when fast mode is enabled", async () => {
const { session, appServer } = await createConnectedSession();
@@ -4661,8 +4661,11 @@ export class CodexAppServerAgentSession implements AgentSession {
async applyOptions(options: ProviderOptions): Promise<void> {
const next = validateProviderOptions("codex", CodexProviderOptionsSchema, options) ?? {};
// An absent speed or plan option leaves the session as it is.
// Supplied keys update the running options; absent keys keep their running value, so an
// explicit tier still beats a legacy `fast_mode` on a patch that supplies neither.
const running = { ...this.providerOptions, ...next };
const speedGiven = next.service_tier !== undefined || next.fast_mode !== undefined;
const requestedTier = speedGiven ? readCodexServiceTier(next) : this.requestedServiceTier;
const requestedTier = speedGiven ? readCodexServiceTier(running) : this.requestedServiceTier;
if (
requestedTier !== this.requestedServiceTier &&
!this.isServiceTierAvailable(requestedTier)
@@ -4671,8 +4674,7 @@ export class CodexAppServerAgentSession implements AgentSession {
`Codex speed '${String(requestedTier)}' is not available for model '${this.config.model ?? "default"}'`,
);
}
// Supplied keys update the running options; absent keys keep their running value.
this.providerOptions = { ...this.providerOptions, ...next };
this.providerOptions = running;
this.requestedServiceTier = requestedTier;
this.setPlanMode(next.plan_mode === undefined ? this.planModeEnabled : next.plan_mode === true);
this.cachedRuntimeInfo = null;