Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { makeAcpNativeLoggerFactory } from "../../provider/acp/AcpNativeLogging.
import {
applyGrokAcpModelSelection,
currentGrokModelIdFromSessionSetup,
grokApprovalOptions,
makeGrokAcpRuntime,
resolveGrokAcpBaseModelId,
} from "../../provider/acp/GrokAcpSupport.ts";
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
39 changes: 38 additions & 1 deletion apps/server/src/provider/acp/GrokAcpSupport.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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<ProviderApprovalOption> {
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<
Expand Down
6 changes: 3 additions & 3 deletions docs/user/permission-modes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
Loading