authorgravatar for maria@kuuro.netmaria <maria@kuuro.net> 2026-09-02 19:27:53-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-02 19:27:53-04:00
log994bd7373cf3a335c204a617604e690ed4c00cba
tree8f9bf41d33c6e09a0a8fd7caddf630ebcdc5db73
parentc742edd46c5b6792ec8647f934a4703f9103aa82
signature Signed by PGP key B5690EEEBB952194

fix(cursor): honor auto and full access modes (#9283)

Co-authored-by: maria-rcks <254055478+maria-rcks@users.noreply.github.com>

5 files changed, 59 insertions(+), 5 deletions(-)

apps/server/src/provider/Layers/CursorAdapter.test.ts+4-1
......@@ -820,6 +820,9 @@ cursorAdapterTestLayer("CursorAdapterLive", (it) => {
820820 );
821821 assert.isDefined(permissionResponse);
822822
823 const argvRuns = yield* Effect.promise(() => readArgvLog(argvLogPath));
824 assert.deepStrictEqual(argvRuns, [["--force", "acp"]]);
825
823826 yield* adapter.stopSession(threadId);
824827 }),
825828 );
......@@ -1260,7 +1263,7 @@ cursorAdapterTestLayer("CursorAdapterLive", (it) => {
12601263
12611264 const argvRuns = yield* Effect.promise(() => readArgvLog(argvLogPath));
12621265 assert.lengthOf(argvRuns, 1, "session should not restart — only one spawn");
1263 assert.deepStrictEqual(argvRuns[0], ["acp"]);
1266 assert.deepStrictEqual(argvRuns[0], ["--force", "acp"]);
12641267
12651268 const requests = yield* Effect.promise(() => readJsonLines(requestLogPath));
12661269 const setConfigRequests = requests.filter(
apps/server/src/provider/Layers/CursorAdapter.ts+1
......@@ -537,6 +537,7 @@ export function makeCursorAdapter(
537537 ...(options?.environment ? { environment: options.environment } : {}),
538538 childProcessSpawner,
539539 cwd,
540 runtimeMode: input.runtimeMode,
540541 ...(resumeSessionId ? { resumeSessionId } : {}),
541542 clientInfo: { name: "t3-code", version: "0.0.0" },
542543 ...(mcpSession
apps/server/src/provider/acp/CursorAcpSupport.test.ts+27
......@@ -74,6 +74,33 @@ describe("buildCursorAcpSpawnInput", () => {
7474 cwd: "/tmp/project",
7575 });
7676 });
77
78 it("forces approval in full-access mode", () => {
79 expect(buildCursorAcpSpawnInput(undefined, "/tmp/project", undefined, "full-access")).toEqual({
80 command: "cursor-agent",
81 args: ["--force", "acp"],
82 cwd: "/tmp/project",
83 });
84 });
85
86 it("uses Cursor auto-review in auto mode", () => {
87 expect(buildCursorAcpSpawnInput(undefined, "/tmp/project", undefined, "auto")).toEqual({
88 command: "cursor-agent",
89 args: ["--auto-review", "acp"],
90 cwd: "/tmp/project",
91 });
92 });
93
94 it.each(["approval-required", "auto-accept-edits"] as const)(
95 "does not relax approval in %s mode",
96 (runtimeMode) => {
97 expect(buildCursorAcpSpawnInput(undefined, "/tmp/project", undefined, runtimeMode)).toEqual({
98 command: "cursor-agent",
99 args: ["acp"],
100 cwd: "/tmp/project",
101 });
102 },
103 );
77104});
78105
79106describe("applyCursorAcpModelSelection", () => {
apps/server/src/provider/acp/CursorAcpSupport.ts+25-2
......@@ -1,4 +1,8 @@
1import { type CursorSettings, type ProviderOptionSelection } from "@t3tools/contracts";
1import {
2 type CursorSettings,
3 type ProviderOptionSelection,
4 type RuntimeMode,
5} from "@t3tools/contracts";
26import * as Crypto from "effect/Crypto";
37import * as Effect from "effect/Effect";
48import * as Layer from "effect/Layer";
......@@ -15,6 +19,17 @@ import * as AcpSessionRuntime from "./AcpSessionRuntime.ts";
1519
1620type CursorAcpRuntimeCursorSettings = Pick<CursorSettings, "apiEndpoint" | "binaryPath">;
1721
22function cursorAcpPermissionArgs(runtimeMode?: RuntimeMode): ReadonlyArray<string> {
23 switch (runtimeMode) {
24 case "auto":
25 return ["--auto-review"];
26 case "full-access":
27 return ["--force"];
28 default:
29 return [];
30 }
31}
32
1833export interface CursorAcpRuntimeInput extends Omit<
1934 AcpSessionRuntime.AcpSessionRuntimeOptions,
2035 "authMethodId" | "clientCapabilities" | "spawn"
......@@ -22,6 +37,7 @@ export interface CursorAcpRuntimeInput extends Omit<
2237 readonly childProcessSpawner: ChildProcessSpawner.ChildProcessSpawner["Service"];
2338 readonly cursorSettings: CursorAcpRuntimeCursorSettings | null | undefined;
2439 readonly environment?: NodeJS.ProcessEnv;
40 readonly runtimeMode?: RuntimeMode;
2541}
2642
2743export interface CursorAcpModelSelectionErrorContext {
......@@ -34,11 +50,13 @@ export function buildCursorAcpSpawnInput(
3450 cursorSettings: CursorAcpRuntimeCursorSettings | null | undefined,
3551 cwd: string,
3652 environment?: NodeJS.ProcessEnv,
53 runtimeMode?: RuntimeMode,
3754): AcpSessionRuntime.AcpSpawnInput {
3855 return {
3956 command: cursorSettings?.binaryPath || "cursor-agent",
4057 args: [
4158 ...(cursorSettings?.apiEndpoint ? (["-e", cursorSettings.apiEndpoint] as const) : []),
59 ...cursorAcpPermissionArgs(runtimeMode),
4260 "acp",
4361 ],
4462 cwd,
......@@ -57,7 +75,12 @@ export const makeCursorAcpRuntime = (
5775 const acpContext = yield* Layer.build(
5876 AcpSessionRuntime.layer({
5977 ...input,
60 spawn: buildCursorAcpSpawnInput(input.cursorSettings, input.cwd, input.environment),
78 spawn: buildCursorAcpSpawnInput(
79 input.cursorSettings,
80 input.cwd,
81 input.environment,
82 input.runtimeMode,
83 ),
6184 authMethodId: "cursor_login",
6285 clientCapabilities: CURSOR_PARAMETERIZED_MODEL_PICKER_CAPABILITIES,
6386 }).pipe(
docs/user/permission-modes.md+2-2
......@@ -17,8 +17,8 @@ without prompting; commands and anything else still stop for approval.
1717
1818**Auto**: routine actions proceed without you; risky ones still ask. How this is enforced depends
1919on the provider: Codex delegates routine approvals to an AI reviewer, Claude uses its own auto
20permission mode, and providers without an equivalent (such as OpenCode) fall back to asking, like
21Supervised.
20permission mode, Cursor uses Smart Auto review, and providers without an equivalent (such as
21OpenCode) fall back to asking, like Supervised.
2222
2323**Full access**: allow commands and edits without prompts. The default. The agent runs
2424unattended until it finishes or asks a question of its own.