From f5546efbaae982bd6c353df500b5a398cc470ce2 Mon Sep 17 00:00:00 2001 From: Dotta Date: Tue, 29 Sep 2026 16:24:48 -0500 Subject: [PATCH] Reject Cursor mode drift in native configuration updates Co-Authored-By: Paperclip --- .../src/drivers/acpx/cursor-mode.test.ts | 18 ++++++++++++++++++ .../src/drivers/acpx/cursor-mode.ts | 10 ++++++++++ 2 files changed, 28 insertions(+) diff --git a/packages/paperclip-runner/src/drivers/acpx/cursor-mode.test.ts b/packages/paperclip-runner/src/drivers/acpx/cursor-mode.test.ts index d941429f44..34b7fcb986 100644 --- a/packages/paperclip-runner/src/drivers/acpx/cursor-mode.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/cursor-mode.test.ts @@ -60,6 +60,24 @@ describe("Cursor native mode admission", () => { const other = opened("plan").guard; expect(() => other("outbound", { id: 3, method: "session/set_config_option", params: { sessionId: "other", configId: "mode", value: "plan" } })).toThrow(/different session/); }); + it.each([false, true])("rejects config-option mode drift with active prompt=%s", active => { + const { admission, guard } = opened("plan"); + if (active) guard("outbound", prompt); + expect(() => guard("inbound", { method: "session/update", params: { sessionId: "native", update: { sessionUpdate: "config_option_update", configOptions: config("agent") } } })).toThrow(/drifted/); + expect(admission.assertReady).toThrow(/drifted/); + }); + it.each(["foreign", "duplicate", "invalid"])("rejects %s mode configuration notifications", kind => { + const { guard } = opened("plan"); + const options = kind === "duplicate" ? [...config("plan"), ...config("plan")] : config(kind === "invalid" ? "architect" : "plan"); + expect(() => guard("inbound", { method: "session/update", params: { sessionId: kind === "foreign" ? "other" : "native", update: { sessionUpdate: "config_option_update", configOptions: options } } })).toThrow(/mode admission/); + }); + it("ignores model-only partial updates and cannot admit from a mode notification", () => { + const { admission, guard } = opened("plan", "agent"); + for (const options of [[{ id: "model", currentValue: "fixture" }], config("plan")]) { + guard("inbound", { method: "session/update", params: { sessionId: "native", update: { sessionUpdate: "config_option_update", configOptions: options } } }); + expect(admission.isReady()).toBe(false); + } + }); it("does not mistake a bare set_mode response for configuration proof", () => { const { admission, guard } = opened("plan", "agent"); guard("outbound", { id: 4, method: "session/set_mode", params: { sessionId: "native", modeId: "plan" } }); diff --git a/packages/paperclip-runner/src/drivers/acpx/cursor-mode.ts b/packages/paperclip-runner/src/drivers/acpx/cursor-mode.ts index 358fd86281..80b5de0189 100644 --- a/packages/paperclip-runner/src/drivers/acpx/cursor-mode.ts +++ b/packages/paperclip-runner/src/drivers/acpx/cursor-mode.ts @@ -69,6 +69,16 @@ export function createCursorModeAdmission(expected: CursorSessionMode) { } return; } + if (message.method === "session/update" && object(params.update).sessionUpdate === "config_option_update") { + const update = object(params.update); + // Config updates may contain only model options. A mode entry is + // authoritative observation, but never an admission acknowledgement. + if (Array.isArray(update.configOptions) && update.configOptions.some(value => object(value).id === "mode")) { + if (!state.sessionId || params.sessionId !== state.sessionId) throw failure("mode update belongs to a different session"); + if (readMode(update, false) !== expected) throw failure("native mode drifted from the selected mode"); + } + return; + } if (message.method === "session/update" && object(params.update).sessionUpdate === "current_mode_update") { if (!state.sessionId || params.sessionId !== state.sessionId) throw failure("mode update belongs to a different session"); if (object(params.update).currentModeId !== expected) throw failure("native mode drifted from the selected mode");