diff --git a/apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.ts index 0f6658093fb6..a02e9ef30a65 100644 --- a/apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.ts @@ -24,6 +24,7 @@ import { makeAcpNativeLoggerFactory } from "../../provider/acp/AcpNativeLogging. import { applyGrokAcpModelSelection, currentGrokModelIdFromSessionSetup, + grokApprovalOptions, makeGrokAcpRuntime, resolveGrokAcpBaseModelId, } from "../../provider/acp/GrokAcpSupport.ts"; @@ -280,6 +281,7 @@ export function makeGrokAcpAdapterFlavor(options: GrokAdapterV2Options): AcpAdap // what its classifier blocked, so every prompt it sends goes to the user. permissionDisposition: (policy, request) => grokLaunchRuntimeMode(policy) === "auto" ? "ask" : acpPermissionDisposition(policy, request), + approvalOptions: grokApprovalOptions, promptFailure: (cause) => makeProviderFailure({ cause, diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/grok_auto_blocked_command/output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/grok_auto_blocked_command/output.ts index d721c24250b7..d040b18e6b01 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/grok_auto_blocked_command/output.ts +++ b/apps/server/src/orchestration-v2/testkit/fixtures/grok_auto_blocked_command/output.ts @@ -84,4 +84,14 @@ export function assertGrokAutoBlockedCommandOutput( frame.method === "session/request_permission", )?.frame.result; assert.deepEqual(answer, { outcome: { outcome: "selected", optionId: "allow-once" } }); + + // Grok's bash prompt offers `always-allow`, which it saves for the whole + // project, not the session. The card must not offer it as a session choice. + const approval = projection.turnItems.find( + (item) => item.type === "approval_request" && item.requestId === request.id, + ); + assert.deepEqual( + approval?.type === "approval_request" ? approval.options?.map((option) => option.decision) : [], + ["cancel", "decline", "accept"], + ); } diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_read_only_on_request/output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_read_only_on_request/output.ts index e3b2d5359999..a302cf81796e 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_read_only_on_request/output.ts +++ b/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_read_only_on_request/output.ts @@ -85,6 +85,15 @@ export function assertToolCallReadOnlyOnRequestGrokOutput( return frame.method === "session/request_permission" ? [frame.params?.toolCall?.kind] : []; }); assert.deepEqual(permissionKinds, ["edit"], "Grok must ask T3 before its own write"); + + // Grok's edit prompt is the one whose "always" answer lasts only the session. + const approval = projectionFor(result, transcript.scenario).turnItems.find( + (item) => item.type === "approval_request", + ); + assert.deepEqual( + approval?.type === "approval_request" ? approval.options?.map((option) => option.decision) : [], + ["cancel", "decline", "acceptForSession", "accept"], + ); } function writtenContent(item: OrchestrationV2TurnItem): string | undefined { diff --git a/apps/server/src/provider/acp/GrokAcpSupport.ts b/apps/server/src/provider/acp/GrokAcpSupport.ts index 1b54b975080e..f623b253d1cf 100644 --- a/apps/server/src/provider/acp/GrokAcpSupport.ts +++ b/apps/server/src/provider/acp/GrokAcpSupport.ts @@ -1,6 +1,11 @@ import type * as EffectAcpSchema from "effect-acp/compat"; import * as NodeServices from "@effect/platform-node/NodeServices"; -import { type GrokSettings, ProviderDriverKind, type RuntimeMode } from "@t3tools/contracts"; +import { + type GrokSettings, + type ProviderApprovalOption, + ProviderDriverKind, + type RuntimeMode, +} from "@t3tools/contracts"; import * as Crypto from "effect/Crypto"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; @@ -115,6 +120,38 @@ export const GROK_ACP_CANCEL_META = { cancelTrigger: "ctrl_c" } as const; */ export const GROK_ACP_INITIALIZE_META = { clientType: "extension" } as const; +/** + * Grok's only session-scoped `allow_always` answer: "Yes, allow all edits + * during this session" on an edit prompt. Its bash, monitor and MCP + * `always-allow` rows instead save a grant for the whole project that outlives + * the session (grok-build `crates/codegen/xai-grok-workspace/src/permission/` + * `prompter.rs` `ALLOW_EDITS_SESSION_OPTION_ID`, `grants.rs` + * `record_prompt_outcome`). + */ +const GROK_ALLOW_EDITS_SESSION_OPTION_ID = "allow-edits-session"; + +/** + * The approval choices a Grok permission prompt can honor. The session choice + * appears only where Grok's answer lasts for the session. + */ +export function grokApprovalOptions( + request: EffectAcpSchema.RequestPermissionRequest, +): ReadonlyArray { + const has = (kind: EffectAcpSchema.PermissionOption["kind"], optionId?: string) => + request.options.some( + (option) => + option.kind === kind && (optionId === undefined || option.optionId.trim() === optionId), + ); + return [ + { decision: "cancel", label: "Cancel" }, + ...(has("reject_once") ? [{ decision: "decline", label: "Decline" } as const] : []), + ...(has("allow_always", GROK_ALLOW_EDITS_SESSION_OPTION_ID) + ? [{ decision: "acceptForSession", label: "Allow all edits this session" } as const] + : []), + ...(has("allow_once") ? [{ decision: "accept", label: "Approve" } as const] : []), + ]; +} + export const makeGrokAcpRuntime = ( input: GrokAcpRuntimeInput, ): Effect.Effect< diff --git a/docs/user/permission-modes.md b/docs/user/permission-modes.md index f0486a61ea5a..c7e45519acdb 100644 --- a/docs/user/permission-modes.md +++ b/docs/user/permission-modes.md @@ -25,9 +25,9 @@ Providers enforce permissions differently. Some read-only actions can proceed in including OpenCode and Antigravity, fall back to asking. On Grok, commands its review blocks come to you for approval. -Grok offers no **Auto-accept edits**. A Grok thread already set to it runs in **Supervised**. For -Grok, **Always allow this session** remembers the matching command or tool input. Other actions -still require approval. +Grok offers no **Auto-accept edits**. A Grok thread already set to it runs in **Supervised**. Grok +file-change approvals offer **Allow all edits this session**. Its command approvals have no +session-wide choice, because Grok would remember that command for the whole project. ACP Registry agents run their own tools in their own mode; T3 Code answers their approval requests by the permission mode. See [ACP Registry permissions](./providers-acp.md#permissions-and-terminals).