Skip to content

Commit b6cb6bf

Browse files
committed
feat(auth): add grok authentication support
1 parent da15456 commit b6cb6bf

106 files changed

Lines changed: 2982 additions & 481 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/api/src/api/contracts.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export type ProjectStatus = "running" | "stopped" | "unknown"
22

3-
export type AgentProvider = "codex" | "opencode" | "claude" | "custom"
3+
export type AgentProvider = "codex" | "opencode" | "claude" | "grok" | "custom"
44

55
export type AgentStatus = "starting" | "running" | "stopping" | "stopped" | "exited" | "failed"
66

@@ -183,19 +183,23 @@ export type AuthMenuFlow =
183183
| "ClaudeLogout"
184184
| "GeminiApiKey"
185185
| "GeminiLogout"
186+
| "GrokApiKey"
187+
| "GrokLogout"
186188

187-
export type AuthTerminalFlow = "ClaudeOauth" | "GeminiOauth"
189+
export type AuthTerminalFlow = "ClaudeOauth" | "GeminiOauth" | "GrokOauth"
188190

189191
export type AuthSnapshot = {
190192
readonly globalEnvPath: string
191193
readonly claudeAuthPath: string
192194
readonly geminiAuthPath: string
195+
readonly grokAuthPath: string
193196
readonly totalEntries: number
194197
readonly githubTokenEntries: number
195198
readonly gitTokenEntries: number
196199
readonly gitUserEntries: number
197200
readonly claudeAuthEntries: number
198201
readonly geminiAuthEntries: number
202+
readonly grokAuthEntries: number
199203
}
200204

201205
export type AuthMenuRequest = {
@@ -249,6 +253,8 @@ export type ProjectAuthFlow =
249253
| "ProjectClaudeDisconnect"
250254
| "ProjectGeminiConnect"
251255
| "ProjectGeminiDisconnect"
256+
| "ProjectGrokConnect"
257+
| "ProjectGrokDisconnect"
252258

253259
export type ProjectAuthSnapshot = {
254260
readonly projectDir: string
@@ -257,22 +263,25 @@ export type ProjectAuthSnapshot = {
257263
readonly envProjectPath: string
258264
readonly claudeAuthPath: string
259265
readonly geminiAuthPath: string
266+
readonly grokAuthPath: string
260267
readonly githubTokenEntries: number
261268
readonly gitTokenEntries: number
262269
readonly claudeAuthEntries: number
263270
readonly geminiAuthEntries: number
271+
readonly grokAuthEntries: number
264272
readonly activeGithubLabel: string | null
265273
readonly activeGitLabel: string | null
266274
readonly activeClaudeLabel: string | null
267275
readonly activeGeminiLabel: string | null
276+
readonly activeGrokLabel: string | null
268277
}
269278

270279
export type ProjectAuthRequest = {
271280
readonly flow: ProjectAuthFlow
272281
readonly label?: string | null | undefined
273282
}
274283

275-
export type ProjectPromptKind = "claude" | "codex" | "gemini"
284+
export type ProjectPromptKind = "claude" | "codex" | "gemini" | "grok"
276285

277286
export type ProjectPromptFile = {
278287
readonly kind: ProjectPromptKind
@@ -302,6 +311,7 @@ export type ProjectSkillScope =
302311
| "claude/skills"
303312
| "codex/skills"
304313
| "gemini/skills"
314+
| "grok/skills"
305315

306316
export type ProjectSkillFile = {
307317
readonly id: string
@@ -394,6 +404,8 @@ export type CreateProjectRequest = {
394404
readonly skipGithubAuth?: boolean | undefined
395405
readonly codexTokenLabel?: string | undefined
396406
readonly claudeTokenLabel?: string | undefined
407+
readonly geminiTokenLabel?: string | undefined
408+
readonly grokTokenLabel?: string | undefined
397409
readonly agentAutoMode?: string | undefined
398410
readonly up?: boolean | undefined
399411
readonly openSsh?: boolean | undefined

packages/api/src/api/schema.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ export const CreateProjectRequestSchema = Schema.Struct({
3232
skipGithubAuth: OptionalBoolean,
3333
codexTokenLabel: OptionalString,
3434
claudeTokenLabel: OptionalString,
35+
geminiTokenLabel: OptionalString,
36+
grokTokenLabel: OptionalString,
3537
agentAutoMode: OptionalString,
3638
up: OptionalBoolean,
3739
openSsh: OptionalBoolean,
@@ -58,10 +60,12 @@ export const AuthMenuFlowSchema = Schema.Literal(
5860
"GitRemove",
5961
"ClaudeLogout",
6062
"GeminiApiKey",
61-
"GeminiLogout"
63+
"GeminiLogout",
64+
"GrokApiKey",
65+
"GrokLogout"
6266
)
6367

64-
export const AuthTerminalFlowSchema = Schema.Literal("ClaudeOauth", "GeminiOauth")
68+
export const AuthTerminalFlowSchema = Schema.Literal("ClaudeOauth", "GeminiOauth", "GrokOauth")
6569

6670
export const AuthMenuRequestSchema = Schema.Struct({
6771
flow: AuthMenuFlowSchema,
@@ -105,15 +109,17 @@ export const ProjectAuthFlowSchema = Schema.Literal(
105109
"ProjectClaudeConnect",
106110
"ProjectClaudeDisconnect",
107111
"ProjectGeminiConnect",
108-
"ProjectGeminiDisconnect"
112+
"ProjectGeminiDisconnect",
113+
"ProjectGrokConnect",
114+
"ProjectGrokDisconnect"
109115
)
110116

111117
export const ProjectAuthRequestSchema = Schema.Struct({
112118
flow: ProjectAuthFlowSchema,
113119
label: OptionalNullableString
114120
})
115121

116-
export const ProjectPromptKindSchema = Schema.Literal("claude", "codex", "gemini")
122+
export const ProjectPromptKindSchema = Schema.Literal("claude", "codex", "gemini", "grok")
117123

118124
export const ProjectPromptUpdateRequestSchema = Schema.Struct({
119125
content: Schema.String
@@ -125,7 +131,8 @@ export const ProjectSkillScopeSchema = Schema.Literal(
125131
"agents/.skills",
126132
"claude/skills",
127133
"codex/skills",
128-
"gemini/skills"
134+
"gemini/skills",
135+
"grok/skills"
129136
)
130137

131138
export const ProjectSkillUpdateRequestSchema = Schema.Struct({
@@ -236,7 +243,7 @@ export const ProjectDatabaseForwardSchema = Schema.Struct({
236243
targetPort: Schema.Number
237244
})
238245

239-
export const AgentProviderSchema = Schema.Literal("codex", "opencode", "claude", "custom")
246+
export const AgentProviderSchema = Schema.Literal("codex", "opencode", "claude", "grok", "custom")
240247

241248
export const AgentEnvVarSchema = Schema.Struct({
242249
key: Schema.String,

packages/api/src/auth-terminal-runner.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { NodeContext, NodeRuntime } from "@effect/platform-node"
2-
import { authClaudeLogin, authGeminiLoginOauth } from "@effect-template/lib"
2+
import { authClaudeLogin, authGeminiLoginOauth, authGrokLoginOauth } from "@effect-template/lib"
33
import { Effect, Match } from "effect"
44

5-
type AuthTerminalRunnerFlow = "ClaudeOauth" | "GeminiOauth"
5+
type AuthTerminalRunnerFlow = "ClaudeOauth" | "GeminiOauth" | "GrokOauth"
66

77
const parseFlow = (value: string | undefined): AuthTerminalRunnerFlow =>
8-
value === "ClaudeOauth" || value === "GeminiOauth" ? value : "ClaudeOauth"
8+
value === "ClaudeOauth" || value === "GeminiOauth" || value === "GrokOauth" ? value : "ClaudeOauth"
99

1010
const parseLabel = (value: string | undefined): string | null => {
1111
const trimmed = value?.trim() ?? ""
@@ -29,6 +29,13 @@ const program = Match.value(flow).pipe(
2929
geminiAuthPath: ".docker-git/.orch/auth/gemini",
3030
isWeb: false
3131
})),
32+
Match.when("GrokOauth", () =>
33+
authGrokLoginOauth({
34+
_tag: "AuthGrokLogin",
35+
label,
36+
grokAuthPath: ".docker-git/.orch/auth/grok",
37+
isWeb: false
38+
})),
3239
Match.exhaustive
3340
)
3441

packages/api/src/http.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ const ProjectDatabaseProfileParamsSchema = Schema.Struct({
178178

179179
const ProjectPromptParamsSchema = Schema.Struct({
180180
projectId: Schema.String,
181-
kind: Schema.Literal("claude", "codex", "gemini")
181+
kind: Schema.Literal("claude", "codex", "gemini", "grok")
182182
})
183183

184184
const ProjectSkillParamsSchema = Schema.Struct({
@@ -435,6 +435,8 @@ const skillScopeFromId = (scopeId: string): ProjectSkillScope | null => {
435435
return "codex/skills"
436436
case "gemini-skills":
437437
return "gemini/skills"
438+
case "grok-skills":
439+
return "grok/skills"
438440
default:
439441
return null
440442
}
@@ -454,6 +456,8 @@ export const skillScopeToId = (scope: ProjectSkillScope): string => {
454456
return "codex-skills"
455457
case "gemini/skills":
456458
return "gemini-skills"
459+
case "grok/skills":
460+
return "grok-skills"
457461
}
458462
}
459463

@@ -465,6 +469,7 @@ const skillScopeFromBody = (scope: string): ProjectSkillScope | null => {
465469
case "claude/skills":
466470
case "codex/skills":
467471
case "gemini/skills":
472+
case "grok/skills":
468473
return scope as ProjectSkillScope
469474
default:
470475
return null

packages/api/src/services/agents.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ const pickDefaultCommand = (provider: CreateAgentRequest["provider"]): string =>
6767
if (provider === "claude") {
6868
return "MCP_PLAYWRIGHT_ISOLATED=1 claude"
6969
}
70+
if (provider === "grok") {
71+
return "MCP_PLAYWRIGHT_ISOLATED=1 grok --no-sandbox"
72+
}
7073
return ""
7174
}
7275

packages/api/src/services/auth-menu.ts

Lines changed: 54 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as FileSystem from "@effect/platform/FileSystem"
22
import type * as CommandExecutor from "@effect/platform/CommandExecutor"
33
import type { PlatformError } from "@effect/platform/Error"
44
import * as Path from "@effect/platform/Path"
5-
import { authClaudeLogout, authGeminiLogin, authGeminiLogout } from "@effect-template/lib/usecases/auth"
5+
import { authClaudeLogout, authGeminiLogin, authGeminiLogout, authGrokLogin, authGrokLogout } from "@effect-template/lib/usecases/auth"
66
import { ensureEnvFile, parseEnvEntries, readEnvText, upsertEnvKey } from "@effect-template/lib/usecases/env-file"
77
import { renderError, type AppError } from "@effect-template/lib/usecases/errors"
88
import { defaultProjectsRoot } from "@effect-template/lib/usecases/menu-helpers"
@@ -16,6 +16,7 @@ type MenuAuthRuntime = FileSystem.FileSystem | Path.Path | CommandExecutor.Comma
1616

1717
const claudeAuthRoot = `${defaultProjectsRoot(process.cwd())}/.orch/auth/claude`
1818
const geminiAuthRoot = `${defaultProjectsRoot(process.cwd())}/.orch/auth/gemini`
19+
const grokAuthRoot = `${defaultProjectsRoot(process.cwd())}/.orch/auth/grok`
1920
const globalEnvPath = `${defaultProjectsRoot(process.cwd())}/.orch/env/global.env`
2021

2122
const normalizeLabel = (value: string): string => {
@@ -102,18 +103,21 @@ export const readAuthMenuSnapshot = (): Effect.Effect<AuthSnapshot, PlatformErro
102103
Effect.flatMap(({ envText, fs, path }) =>
103104
Effect.all({
104105
claudeAuthEntries: countAuthAccountDirectories(fs, path, claudeAuthRoot),
105-
geminiAuthEntries: countAuthAccountDirectories(fs, path, geminiAuthRoot)
106+
geminiAuthEntries: countAuthAccountDirectories(fs, path, geminiAuthRoot),
107+
grokAuthEntries: countAuthAccountDirectories(fs, path, grokAuthRoot)
106108
}).pipe(
107-
Effect.map(({ claudeAuthEntries, geminiAuthEntries }) => ({
109+
Effect.map(({ claudeAuthEntries, geminiAuthEntries, grokAuthEntries }) => ({
108110
globalEnvPath,
109111
claudeAuthPath: claudeAuthRoot,
110112
geminiAuthPath: geminiAuthRoot,
113+
grokAuthPath: grokAuthRoot,
111114
totalEntries: parseEnvEntries(envText).filter((entry) => entry.value.trim().length > 0).length,
112115
githubTokenEntries: countKeyEntries(envText, "GITHUB_TOKEN"),
113116
gitTokenEntries: countKeyEntries(envText, "GIT_AUTH_TOKEN"),
114117
gitUserEntries: countKeyEntries(envText, "GIT_AUTH_USER"),
115118
claudeAuthEntries,
116-
geminiAuthEntries
119+
geminiAuthEntries,
120+
grokAuthEntries
117121
}))
118122
)
119123
)
@@ -135,7 +139,11 @@ const syncMessage = (request: AuthMenuRequest): string =>
135139
? `chore(state): auth claude logout ${canonicalLabel(request.label)}`
136140
: request.flow === "GeminiApiKey"
137141
? `chore(state): auth gemini ${canonicalLabel(request.label)}`
138-
: `chore(state): auth gemini logout ${canonicalLabel(request.label)}`
142+
: request.flow === "GeminiLogout"
143+
? `chore(state): auth gemini logout ${canonicalLabel(request.label)}`
144+
: request.flow === "GrokApiKey"
145+
? `chore(state): auth grok ${canonicalLabel(request.label)}`
146+
: `chore(state): auth grok logout ${canonicalLabel(request.label)}`
139147

140148
const writeEnvBackedAuthFlow = (
141149
request: AuthMenuRequest
@@ -213,15 +221,45 @@ export const runAuthMenuFlow = (
213221
error instanceof ApiBadRequestError ? error : new ApiBadRequestError({ message: String(error) })
214222
)
215223
)
216-
: pipe(
217-
authGeminiLogout({
218-
_tag: "AuthGeminiLogout",
219-
label: request.label ?? null,
220-
geminiAuthPath: geminiAuthRoot
221-
}),
222-
Effect.mapError(mapMenuAuthError),
223-
Effect.zipRight(readAuthMenuSnapshot()),
224-
Effect.mapError((error) =>
225-
error instanceof ApiBadRequestError ? error : new ApiBadRequestError({ message: String(error) })
224+
: request.flow === "GeminiLogout"
225+
? pipe(
226+
authGeminiLogout({
227+
_tag: "AuthGeminiLogout",
228+
label: request.label ?? null,
229+
geminiAuthPath: geminiAuthRoot
230+
}),
231+
Effect.mapError(mapMenuAuthError),
232+
Effect.zipRight(readAuthMenuSnapshot()),
233+
Effect.mapError((error) =>
234+
error instanceof ApiBadRequestError ? error : new ApiBadRequestError({ message: String(error) })
235+
)
226236
)
227-
)
237+
: request.flow === "GrokApiKey"
238+
? pipe(
239+
authGrokLogin(
240+
{
241+
_tag: "AuthGrokLogin",
242+
label: request.label ?? null,
243+
grokAuthPath: grokAuthRoot,
244+
isWeb: true
245+
},
246+
request.apiKey ?? ""
247+
),
248+
Effect.mapError(mapMenuAuthError),
249+
Effect.zipRight(readAuthMenuSnapshot()),
250+
Effect.mapError((error) =>
251+
error instanceof ApiBadRequestError ? error : new ApiBadRequestError({ message: String(error) })
252+
)
253+
)
254+
: pipe(
255+
authGrokLogout({
256+
_tag: "AuthGrokLogout",
257+
label: request.label ?? null,
258+
grokAuthPath: grokAuthRoot
259+
}),
260+
Effect.mapError(mapMenuAuthError),
261+
Effect.zipRight(readAuthMenuSnapshot()),
262+
Effect.mapError((error) =>
263+
error instanceof ApiBadRequestError ? error : new ApiBadRequestError({ message: String(error) })
264+
)
265+
)

packages/api/src/services/auth-terminal-sessions.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,9 @@ const resolveCommandLabel = (request: AuthTerminalSessionRequest): string => {
7171
const suffix = label === undefined || label.length === 0 ? "" : ` [${label}]`
7272
return request.flow === "ClaudeOauth"
7373
? `docker-git menu auth claude oauth${suffix}`
74-
: `docker-git menu auth gemini oauth${suffix}`
74+
: request.flow === "GeminiOauth"
75+
? `docker-git menu auth gemini oauth${suffix}`
76+
: `docker-git menu auth grok oauth${suffix}`
7577
}
7678

7779
const resolveRunnerArgs = (flow: AuthTerminalFlow, label: string | null | undefined): ReadonlyArray<string> => {

packages/api/src/services/container-tasks-core.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export type ManagedAgentPid = {
1717
readonly pid: number
1818
}
1919

20-
const interactiveAgentPattern = /\b(codex|claude|opencode|gemini)\b/u
20+
const interactiveAgentPattern = /\b(codex|claude|opencode|gemini|grok)\b/u
2121

2222
const hasInteractiveTty = (process: RawContainerProcess): boolean =>
2323
process.tty !== "?" && process.tty.trim().length > 0

packages/api/src/services/federation.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1695,7 +1695,7 @@ const resolveAgentProvider = (
16951695
subscription: FollowSubscription | undefined
16961696
): AgentProvider => {
16971697
const raw = subscription?.agentProvider ?? process.env["DOCKER_GIT_EXCHANGE_AGENT_PROVIDER"]
1698-
return raw === "claude" || raw === "opencode" || raw === "custom" ? raw : "codex"
1698+
return raw === "claude" || raw === "opencode" || raw === "grok" || raw === "custom" ? raw : "codex"
16991699
}
17001700

17011701
const buildTaskPrompt = (issue: FederationIssueRecord): string => {
@@ -1730,6 +1730,9 @@ const buildAgentCommand = (
17301730
if (provider === "opencode") {
17311731
return `opencode run ${shellEscape(prompt)}`
17321732
}
1733+
if (provider === "grok") {
1734+
return `MCP_PLAYWRIGHT_ISOLATED=1 grok --no-sandbox -p ${shellEscape(prompt)}`
1735+
}
17331736
if (provider === "custom") {
17341737
return `sh -lc ${shellEscape(`printf '%s\n' ${shellEscape(prompt)}`)}`
17351738
}

0 commit comments

Comments
 (0)