diff --git a/packages/server/src/server/agent/agent-manager.test.ts b/packages/server/src/server/agent/agent-manager.test.ts index f8fc24a7f4..5f9569fcb5 100644 --- a/packages/server/src/server/agent/agent-manager.test.ts +++ b/packages/server/src/server/agent/agent-manager.test.ts @@ -1,3 +1,4 @@ +import { mergeProviderOptions } from "./provider-options.js"; import { createIdleRuntimeCleanup } from "./idle-runtime-cleanup/index.js"; import { prepareAgentMessage, @@ -12880,6 +12881,37 @@ describe("agent options", () => { ]); }); + test("a supplied option is resolved against the configured defaults before it reaches the provider", async () => { + class DefaultsClient extends OptionsClient { + override effectiveOptions(own: ProviderOptions | undefined): ProviderOptions { + return ( + mergeProviderOptions({ sandbox: { enabled: true, failIfUnavailable: true } }, own) ?? {} + ); + } + } + const workdir = mkdtempSync(join(tmpdir(), "agent-manager-options-")); + const client = new DefaultsClient(); + const manager = new AgentManager({ + clients: { codex: client }, + registry: new AgentStorage(join(workdir, "agents"), logger), + logger, + }); + const created = await manager.createAgent({ provider: "codex", cwd: workdir }, undefined, { + workspaceId: undefined, + }); + + await manager.setAgentOptions(created.id, { sandbox: { network: { allowed: ["a.example"] } } }); + + expect(client.session?.applied).toEqual([ + { + sandbox: { enabled: true, failIfUnavailable: true, network: { allowed: ["a.example"] } }, + }, + ]); + expect(manager.getAgent(created.id)?.config.providerOptions).toEqual({ + sandbox: { network: { allowed: ["a.example"] } }, + }); + }); + test("an option without a control is settable through the patch and readable in the snapshot", async () => { const { manager, client, storage, agentId } = await createOptionsAgent(); diff --git a/packages/server/src/server/agent/agent-manager.ts b/packages/server/src/server/agent/agent-manager.ts index d9968df48b..91ede382cd 100644 --- a/packages/server/src/server/agent/agent-manager.ts +++ b/packages/server/src/server/agent/agent-manager.ts @@ -2290,9 +2290,7 @@ export class AgentManager { // a rejection leaves the record, its snapshots and its stored copy as they were. this.pendingOptionCandidates.set(agentId, candidate); try { - // Only the agent's own options: a removed key is absent, so the running provider keeps - // its setting. `config.json` defaults apply when the session is next opened. - await agent.session.applyOptions(candidate); + await agent.session.applyOptions(this.liveOptionsFor(agent.provider, candidate)); } finally { // Events the provider queued before answering must reach the candidate, not the record. await this.drainSessionEvents(agentId); @@ -2306,6 +2304,16 @@ export class AgentManager { }); } + /** + * The options a running provider is handed: the keys the agent holds, each resolved against + * the `config.json` defaults. A removed key is absent, so the provider keeps its setting; + * defaults for absent keys apply when the session is next opened. + */ + private liveOptionsFor(provider: AgentProvider, own: ProviderOptions): ProviderOptions { + const effective = this.effectiveOptionsFor(provider, own); + return Object.fromEntries(Object.keys(own).map((key) => [key, effective[key]])); + } + private effectiveOptionsFor( provider: AgentProvider, own: ProviderOptions | undefined,