1import type * as EffectAcpSchema from "effect-acp/compat";
2import * as NodeServices from "@effect/platform-node/NodeServices";
3import {
4 type ProviderApprovalOption,
5 ProviderDriverKind,
6 type RuntimeMode,
7} from "@t3tools/contracts";
8import type { GrokSettings } from "../settings.ts";
9import * as SecureRandom from "effect/crypto/SecureRandom";
10import * as Effect from "effect/Effect";
11import * as Layer from "effect/Layer";
12import * as Scope from "effect/Scope";
13import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
14import * as EffectAcpErrors from "effect-acp/errors";
15import * as HostProcess from "@t3tools/shared/HostProcess";
16import { normalizeModelSlug } from "@t3tools/shared/model";
17
18import * as AcpSessionRuntime from "@t3tools/provider-acp/server/AcpSessionRuntime";
19import { makeXAiPromptCompletionRuntime } from "./xaiAcpExtension.ts";
20
21const GROK_API_KEY_ENV = "XAI_API_KEY";
22const GROK_OAUTH2_REFERRER_ENV = "GROK_OAUTH2_REFERRER";
23const T3_CODE_OAUTH_REFERRER = "t3code";
24const GROK_AUTH_METHOD_API_KEY = "xai.api_key";
25const GROK_AUTH_METHOD_CACHED_TOKEN = "cached_token";
26const GROK_DRIVER_KIND = ProviderDriverKind.make("grok");
27
28type GrokAcpRuntimeGrokSettings = Pick<GrokSettings, "binaryPath">;
29
30interface GrokAcpRuntimeInput extends Omit<
31 AcpSessionRuntime.AcpSessionRuntimeOptions,
32 "authMethodId" | "clientCapabilities" | "spawn"
33> {
34 readonly grokSettings: GrokAcpRuntimeGrokSettings | null | undefined;
35 readonly environment?: NodeJS.ProcessEnv;
36 readonly runtimeMode?: RuntimeMode;
37}
38
39// grok agent treats settings-only acceptEdits as ask; it has no Auto-accept edits launch mode.
40export const GROK_SUPPORTED_RUNTIME_MODES = [
41 "approval-required",
42 "auto",
43 "full-access",
44] as const satisfies ReadonlyArray<RuntimeMode>;
45
46// argv overrides Grok config so Supervised cannot inherit always-approve.
47export function grokAcpSpawnArgs(runtimeMode?: RuntimeMode): ReadonlyArray<string> {
48 switch (runtimeMode) {
49 case undefined:
50 return ["agent", "stdio"];
51 case "auto":
52 return ["--permission-mode", "auto", "agent", "stdio"];
53 case "full-access":
54 return ["agent", "--always-approve", "stdio"];
55 default:
56 return ["--permission-mode", "default", "agent", "stdio"];
57 }
58}
59
60function buildGrokAcpSpawnInput(
61 grokSettings: GrokAcpRuntimeGrokSettings | null | undefined,
62 cwd: string,
63 environment?: NodeJS.ProcessEnv,
64 runtimeMode?: RuntimeMode,
65): AcpSessionRuntime.AcpSpawnInput {
66 return {
67 command: grokSettings?.binaryPath || "grok",
68 args: [...grokAcpSpawnArgs(runtimeMode)],
69 cwd,
70 env: {
71 ...environment,
72 [GROK_OAUTH2_REFERRER_ENV]: T3_CODE_OAUTH_REFERRER,
73 },
74 };
75}
76
77function resolveGrokAuthMethodId(environment: NodeJS.ProcessEnv | undefined): string {
78 return environment?.[GROK_API_KEY_ENV]?.trim()
79 ? GROK_AUTH_METHOD_API_KEY
80 : GROK_AUTH_METHOD_CACHED_TOKEN;
81}
82
83function grokAcpRuntimeProcessOwnership(
84 processGroupPlatform: NodeJS.Platform,
85): Pick<
86 AcpSessionRuntime.AcpSessionRuntimeOptions,
87 "ownDescendantProcessGroups" | "ownDetachedProcessGroup" | "processGroupPlatform"
88> {
89 return {
90 // macOS lacks a stable libproc identity provider for Grok's nested detached tool groups.
91 ownDescendantProcessGroups: processGroupPlatform === "linux",
92 ownDetachedProcessGroup: true,
93 processGroupPlatform,
94 };
95}
96
97// Grok Ctrl+C blocks stale background wakes until the next genuine user turn.
98export const GROK_ACP_CANCEL_META = { cancelTrigger: "ctrl_c" } as const;
99
100// generic silently denies Auto refusals; extension prompts without persistent command grants.
101export const GROK_ACP_INITIALIZE_META = { clientType: "extension" } as const;
102
103// Only allow-edits-session is session-scoped; other allow-always choices persist project grants.
104const GROK_ALLOW_EDITS_SESSION_OPTION_ID = "allow-edits-session";
105
106export function grokApprovalOptions(
107 request: EffectAcpSchema.RequestPermissionRequest,
108): ReadonlyArray<ProviderApprovalOption> {
109 const has = (kind: EffectAcpSchema.PermissionOption["kind"], optionId?: string) =>
110 request.options.some(
111 (option) =>
112 option.kind === kind && (optionId === undefined || option.optionId.trim() === optionId),
113 );
114 return [
115 { decision: "cancel", label: "Cancel" },
116 ...(has("reject_once") ? [{ decision: "decline", label: "Decline" } as const] : []),
117 ...(has("allow_always", GROK_ALLOW_EDITS_SESSION_OPTION_ID)
118 ? [{ decision: "acceptForSession", label: "Allow all edits this session" } as const]
119 : []),
120 ...(has("allow_once") ? [{ decision: "accept", label: "Approve" } as const] : []),
121 ];
122}
123
124export const makeGrokAcpRuntime = Effect.fn("GrokAcpSupport.makeGrokAcpRuntime")(function* (
125 input: GrokAcpRuntimeInput,
126): Effect.fn.Return<
127 AcpSessionRuntime.AcpSessionRuntime["Service"],
128 EffectAcpErrors.AcpError,
129 ChildProcessSpawner.ChildProcessSpawner | SecureRandom.SecureRandom | Scope.Scope
130> {
131 const processGroupPlatform = yield* HostProcess.Platform.pipe(Effect.provide(NodeServices.layer));
132 const acpContext = yield* Layer.build(
133 AcpSessionRuntime.layer({
134 ...input,
135 spawn: buildGrokAcpSpawnInput(
136 input.grokSettings,
137 input.cwd,
138 input.environment,
139 input.runtimeMode,
140 ),
141 authMethodId: resolveGrokAuthMethodId(input.environment),
142 cancelMeta: { ...input.cancelMeta, ...GROK_ACP_CANCEL_META },
143 initializeMeta: GROK_ACP_INITIALIZE_META,
144 ...grokAcpRuntimeProcessOwnership(processGroupPlatform),
145 }),
146 );
147 const runtime = yield* Effect.service(AcpSessionRuntime.AcpSessionRuntime).pipe(
148 Effect.provide(acpContext),
149 );
150 return yield* makeXAiPromptCompletionRuntime(runtime);
151});
152
153// This product name is not an ACP model ID; it selects the current session model.
154export const GROK_DEFAULT_MODEL_SLUG = "grok-build";
155
156export function resolveGrokAcpBaseModelId(model: string | null | undefined): string {
157 const trimmed = model?.trim();
158 const base = trimmed && trimmed.length > 0 ? trimmed : GROK_DEFAULT_MODEL_SLUG;
159 return normalizeModelSlug(base, GROK_DRIVER_KIND) ?? GROK_DEFAULT_MODEL_SLUG;
160}
161
162const GROK_REASONING_EFFORT_TOKEN = /^[a-z0-9][a-z0-9._-]{0,31}$/i;
163
164export function isValidGrokReasoningEffortToken(value: string): boolean {
165 return GROK_REASONING_EFFORT_TOKEN.test(value);
166}
167
168function normalizeGrokReasoningEffort(value: string | undefined): string | undefined {
169 const effort = value?.trim();
170 return effort && isValidGrokReasoningEffortToken(effort) ? effort : undefined;
171}
172
173export function currentGrokModelIdFromSessionSetup(
174 sessionSetupResult:
175 | EffectAcpSchema.LoadSessionResponse
176 | EffectAcpSchema.NewSessionResponse
177 | EffectAcpSchema.ResumeSessionResponse,
178): string | undefined {
179 return sessionSetupResult.models?.currentModelId?.trim() || undefined;
180}
181
182export function currentGrokReasoningEffortFromSessionSetup(
183 sessionSetupResult:
184 | EffectAcpSchema.LoadSessionResponse
185 | EffectAcpSchema.NewSessionResponse
186 | EffectAcpSchema.ResumeSessionResponse,
187): string | undefined {
188 const modelState = sessionSetupResult.models;
189 if (!modelState) {
190 return undefined;
191 }
192 const currentModelId = modelState.currentModelId.trim();
193 if (currentModelId.length === 0) {
194 return undefined;
195 }
196 const currentModel = modelState.availableModels.find(
197 (model) => model.modelId.trim() === currentModelId,
198 );
199 const reasoningEffort = currentModel?._meta?.reasoningEffort;
200 return typeof reasoningEffort === "string"
201 ? normalizeGrokReasoningEffort(reasoningEffort)
202 : undefined;
203}
204
205export function applyGrokAcpModelSelection<E>(input: {
206 readonly runtime: Pick<AcpSessionRuntime.AcpSessionRuntime["Service"], "setSessionModel">;
207 readonly currentModelId: string | undefined;
208 readonly currentReasoningEffort?: string | undefined;
209 readonly requestedModelId: string | undefined;
210 readonly requestedReasoningEffort?: string | undefined;
211 readonly mapError: (cause: EffectAcpErrors.AcpError) => E;
212}): Effect.Effect<string | undefined, E> {
213 const requestedModelId =
214 input.requestedModelId === GROK_DEFAULT_MODEL_SLUG ? undefined : input.requestedModelId;
215 const modelChanged = requestedModelId !== undefined && requestedModelId !== input.currentModelId;
216 const reasoningProvided = input.requestedReasoningEffort !== undefined;
217 const reasoningEffort = reasoningProvided
218 ? normalizeGrokReasoningEffort(input.requestedReasoningEffort)
219 : undefined;
220 const reasoningEffortChanged =
221 reasoningProvided && reasoningEffort !== input.currentReasoningEffort;
222 const targetModelId = requestedModelId ?? input.currentModelId;
223 if ((!modelChanged && !reasoningEffortChanged) || targetModelId === undefined) {
224 return Effect.succeed(input.currentModelId);
225 }
226 const reasoningMeta =
227 reasoningProvided && reasoningEffort !== undefined ? { reasoningEffort } : undefined;
228 return input.runtime
229 .setSessionModel(targetModelId, reasoningMeta)
230 .pipe(Effect.mapError(input.mapError), Effect.as(targetModelId));
231}