Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitkeep
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# .gitkeep file auto-generated at 2026-05-15T13:19:37.296Z for PR creation at branch issue-304-3b5ced26375c for issue https://github.com/ProverCoderAI/docker-git/issues/304
18 changes: 15 additions & 3 deletions packages/api/src/api/contracts.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export type ProjectStatus = "running" | "stopped" | "unknown"

export type AgentProvider = "codex" | "opencode" | "claude" | "custom"
export type AgentProvider = "codex" | "opencode" | "claude" | "grok" | "custom"

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

Expand Down Expand Up @@ -183,19 +183,23 @@ export type AuthMenuFlow =
| "ClaudeLogout"
| "GeminiApiKey"
| "GeminiLogout"
| "GrokApiKey"
| "GrokLogout"

export type AuthTerminalFlow = "ClaudeOauth" | "GeminiOauth"
export type AuthTerminalFlow = "ClaudeOauth" | "GeminiOauth" | "GrokOauth"

export type AuthSnapshot = {
readonly globalEnvPath: string
readonly claudeAuthPath: string
readonly geminiAuthPath: string
readonly grokAuthPath: string
readonly totalEntries: number
readonly githubTokenEntries: number
readonly gitTokenEntries: number
readonly gitUserEntries: number
readonly claudeAuthEntries: number
readonly geminiAuthEntries: number
readonly grokAuthEntries: number
}

export type AuthMenuRequest = {
Expand Down Expand Up @@ -249,6 +253,8 @@ export type ProjectAuthFlow =
| "ProjectClaudeDisconnect"
| "ProjectGeminiConnect"
| "ProjectGeminiDisconnect"
| "ProjectGrokConnect"
| "ProjectGrokDisconnect"

export type ProjectAuthSnapshot = {
readonly projectDir: string
Expand All @@ -257,22 +263,25 @@ export type ProjectAuthSnapshot = {
readonly envProjectPath: string
readonly claudeAuthPath: string
readonly geminiAuthPath: string
readonly grokAuthPath: string
readonly githubTokenEntries: number
readonly gitTokenEntries: number
readonly claudeAuthEntries: number
readonly geminiAuthEntries: number
readonly grokAuthEntries: number
readonly activeGithubLabel: string | null
readonly activeGitLabel: string | null
readonly activeClaudeLabel: string | null
readonly activeGeminiLabel: string | null
readonly activeGrokLabel: string | null
}

export type ProjectAuthRequest = {
readonly flow: ProjectAuthFlow
readonly label?: string | null | undefined
}

export type ProjectPromptKind = "claude" | "codex" | "gemini"
export type ProjectPromptKind = "claude" | "codex" | "gemini" | "grok"

export type ProjectPromptFile = {
readonly kind: ProjectPromptKind
Expand Down Expand Up @@ -302,6 +311,7 @@ export type ProjectSkillScope =
| "claude/skills"
| "codex/skills"
| "gemini/skills"
| "grok/skills"

export type ProjectSkillFile = {
readonly id: string
Expand Down Expand Up @@ -394,6 +404,8 @@ export type CreateProjectRequest = {
readonly skipGithubAuth?: boolean | undefined
readonly codexTokenLabel?: string | undefined
readonly claudeTokenLabel?: string | undefined
readonly geminiTokenLabel?: string | undefined
readonly grokTokenLabel?: string | undefined
readonly agentAutoMode?: string | undefined
readonly up?: boolean | undefined
readonly openSsh?: boolean | undefined
Expand Down
19 changes: 13 additions & 6 deletions packages/api/src/api/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ export const CreateProjectRequestSchema = Schema.Struct({
skipGithubAuth: OptionalBoolean,
codexTokenLabel: OptionalString,
claudeTokenLabel: OptionalString,
geminiTokenLabel: OptionalString,
grokTokenLabel: OptionalString,
agentAutoMode: OptionalString,
up: OptionalBoolean,
openSsh: OptionalBoolean,
Expand All @@ -58,10 +60,12 @@ export const AuthMenuFlowSchema = Schema.Literal(
"GitRemove",
"ClaudeLogout",
"GeminiApiKey",
"GeminiLogout"
"GeminiLogout",
"GrokApiKey",
"GrokLogout"
)

export const AuthTerminalFlowSchema = Schema.Literal("ClaudeOauth", "GeminiOauth")
export const AuthTerminalFlowSchema = Schema.Literal("ClaudeOauth", "GeminiOauth", "GrokOauth")

export const AuthMenuRequestSchema = Schema.Struct({
flow: AuthMenuFlowSchema,
Expand Down Expand Up @@ -105,15 +109,17 @@ export const ProjectAuthFlowSchema = Schema.Literal(
"ProjectClaudeConnect",
"ProjectClaudeDisconnect",
"ProjectGeminiConnect",
"ProjectGeminiDisconnect"
"ProjectGeminiDisconnect",
"ProjectGrokConnect",
"ProjectGrokDisconnect"
)

export const ProjectAuthRequestSchema = Schema.Struct({
flow: ProjectAuthFlowSchema,
label: OptionalNullableString
})

export const ProjectPromptKindSchema = Schema.Literal("claude", "codex", "gemini")
export const ProjectPromptKindSchema = Schema.Literal("claude", "codex", "gemini", "grok")

export const ProjectPromptUpdateRequestSchema = Schema.Struct({
content: Schema.String
Expand All @@ -125,7 +131,8 @@ export const ProjectSkillScopeSchema = Schema.Literal(
"agents/.skills",
"claude/skills",
"codex/skills",
"gemini/skills"
"gemini/skills",
"grok/skills"
)

export const ProjectSkillUpdateRequestSchema = Schema.Struct({
Expand Down Expand Up @@ -236,7 +243,7 @@ export const ProjectDatabaseForwardSchema = Schema.Struct({
targetPort: Schema.Number
})

export const AgentProviderSchema = Schema.Literal("codex", "opencode", "claude", "custom")
export const AgentProviderSchema = Schema.Literal("codex", "opencode", "claude", "grok", "custom")

export const AgentEnvVarSchema = Schema.Struct({
key: Schema.String,
Expand Down
13 changes: 10 additions & 3 deletions packages/api/src/auth-terminal-runner.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { NodeContext, NodeRuntime } from "@effect/platform-node"
import { authClaudeLogin, authGeminiLoginOauth } from "@effect-template/lib"
import { authClaudeLogin, authGeminiLoginOauth, authGrokLoginOauth } from "@effect-template/lib"
import { Effect, Match } from "effect"

type AuthTerminalRunnerFlow = "ClaudeOauth" | "GeminiOauth"
type AuthTerminalRunnerFlow = "ClaudeOauth" | "GeminiOauth" | "GrokOauth"

const parseFlow = (value: string | undefined): AuthTerminalRunnerFlow =>
value === "ClaudeOauth" || value === "GeminiOauth" ? value : "ClaudeOauth"
value === "ClaudeOauth" || value === "GeminiOauth" || value === "GrokOauth" ? value : "ClaudeOauth"
Comment on lines 7 to +8
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Не подменяйте невалидный flow значением ClaudeOauth по умолчанию.

Сейчас при ошибочном аргументе silently выбирается Claude-флоу, из-за чего можно авторизовать не того провайдера и получить ложный успех в CI/локально. Лучше fail-fast на невалидном значении.

💡 Предлагаемое исправление
-const parseFlow = (value: string | undefined): AuthTerminalRunnerFlow =>
-  value === "ClaudeOauth" || value === "GeminiOauth" || value === "GrokOauth" ? value : "ClaudeOauth"
+const parseFlow = (value: string | undefined): AuthTerminalRunnerFlow => {
+  if (value === "ClaudeOauth" || value === "GeminiOauth" || value === "GrokOauth") {
+    return value
+  }
+  throw new Error(`Unsupported auth flow: ${value ?? "<empty>"}`)
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/api/src/auth-terminal-runner.ts` around lines 7 - 8, parseFlow
currently maps any invalid or undefined value to "ClaudeOauth" silently; change
it to fail-fast instead: in the parseFlow function (returning
AuthTerminalRunnerFlow) validate that value is exactly one of "ClaudeOauth" |
"GeminiOauth" | "GrokOauth" and return it, otherwise throw a descriptive Error
(include the invalid value) so callers don't silently proceed with the wrong
provider; update any call sites expecting a default accordingly.


const parseLabel = (value: string | undefined): string | null => {
const trimmed = value?.trim() ?? ""
Expand All @@ -29,6 +29,13 @@ const program = Match.value(flow).pipe(
geminiAuthPath: ".docker-git/.orch/auth/gemini",
isWeb: false
})),
Match.when("GrokOauth", () =>
authGrokLoginOauth({
_tag: "AuthGrokLogin",
label,
grokAuthPath: ".docker-git/.orch/auth/grok",
isWeb: false
})),
Match.exhaustive
)

Expand Down
7 changes: 6 additions & 1 deletion packages/api/src/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ const ProjectDatabaseProfileParamsSchema = Schema.Struct({

const ProjectPromptParamsSchema = Schema.Struct({
projectId: Schema.String,
kind: Schema.Literal("claude", "codex", "gemini")
kind: Schema.Literal("claude", "codex", "gemini", "grok")
})

const ProjectSkillParamsSchema = Schema.Struct({
Expand Down Expand Up @@ -435,6 +435,8 @@ const skillScopeFromId = (scopeId: string): ProjectSkillScope | null => {
return "codex/skills"
case "gemini-skills":
return "gemini/skills"
case "grok-skills":
return "grok/skills"
default:
return null
}
Expand All @@ -454,6 +456,8 @@ export const skillScopeToId = (scope: ProjectSkillScope): string => {
return "codex-skills"
case "gemini/skills":
return "gemini-skills"
case "grok/skills":
return "grok-skills"
}
}

Expand All @@ -465,6 +469,7 @@ const skillScopeFromBody = (scope: string): ProjectSkillScope | null => {
case "claude/skills":
case "codex/skills":
case "gemini/skills":
case "grok/skills":
return scope as ProjectSkillScope
default:
return null
Expand Down
3 changes: 3 additions & 0 deletions packages/api/src/services/agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ const pickDefaultCommand = (provider: CreateAgentRequest["provider"]): string =>
if (provider === "claude") {
return "MCP_PLAYWRIGHT_ISOLATED=1 claude"
}
if (provider === "grok") {
return "MCP_PLAYWRIGHT_ISOLATED=1 grok --no-sandbox"
}
return ""
}

Expand Down
70 changes: 54 additions & 16 deletions packages/api/src/services/auth-menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as FileSystem from "@effect/platform/FileSystem"
import type * as CommandExecutor from "@effect/platform/CommandExecutor"
import type { PlatformError } from "@effect/platform/Error"
import * as Path from "@effect/platform/Path"
import { authClaudeLogout, authGeminiLogin, authGeminiLogout } from "@effect-template/lib/usecases/auth"
import { authClaudeLogout, authGeminiLogin, authGeminiLogout, authGrokLogin, authGrokLogout } from "@effect-template/lib/usecases/auth"
import { ensureEnvFile, parseEnvEntries, readEnvText, upsertEnvKey } from "@effect-template/lib/usecases/env-file"
import { renderError, type AppError } from "@effect-template/lib/usecases/errors"
import { defaultProjectsRoot } from "@effect-template/lib/usecases/menu-helpers"
Expand All @@ -16,6 +16,7 @@ type MenuAuthRuntime = FileSystem.FileSystem | Path.Path | CommandExecutor.Comma

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

const normalizeLabel = (value: string): string => {
Expand Down Expand Up @@ -102,18 +103,21 @@ export const readAuthMenuSnapshot = (): Effect.Effect<AuthSnapshot, PlatformErro
Effect.flatMap(({ envText, fs, path }) =>
Effect.all({
claudeAuthEntries: countAuthAccountDirectories(fs, path, claudeAuthRoot),
geminiAuthEntries: countAuthAccountDirectories(fs, path, geminiAuthRoot)
geminiAuthEntries: countAuthAccountDirectories(fs, path, geminiAuthRoot),
grokAuthEntries: countAuthAccountDirectories(fs, path, grokAuthRoot)
}).pipe(
Effect.map(({ claudeAuthEntries, geminiAuthEntries }) => ({
Effect.map(({ claudeAuthEntries, geminiAuthEntries, grokAuthEntries }) => ({
globalEnvPath,
claudeAuthPath: claudeAuthRoot,
geminiAuthPath: geminiAuthRoot,
grokAuthPath: grokAuthRoot,
totalEntries: parseEnvEntries(envText).filter((entry) => entry.value.trim().length > 0).length,
githubTokenEntries: countKeyEntries(envText, "GITHUB_TOKEN"),
gitTokenEntries: countKeyEntries(envText, "GIT_AUTH_TOKEN"),
gitUserEntries: countKeyEntries(envText, "GIT_AUTH_USER"),
claudeAuthEntries,
geminiAuthEntries
geminiAuthEntries,
grokAuthEntries
}))
)
)
Expand All @@ -135,7 +139,11 @@ const syncMessage = (request: AuthMenuRequest): string =>
? `chore(state): auth claude logout ${canonicalLabel(request.label)}`
: request.flow === "GeminiApiKey"
? `chore(state): auth gemini ${canonicalLabel(request.label)}`
: `chore(state): auth gemini logout ${canonicalLabel(request.label)}`
: request.flow === "GeminiLogout"
? `chore(state): auth gemini logout ${canonicalLabel(request.label)}`
: request.flow === "GrokApiKey"
? `chore(state): auth grok ${canonicalLabel(request.label)}`
: `chore(state): auth grok logout ${canonicalLabel(request.label)}`

const writeEnvBackedAuthFlow = (
request: AuthMenuRequest
Expand Down Expand Up @@ -213,15 +221,45 @@ export const runAuthMenuFlow = (
error instanceof ApiBadRequestError ? error : new ApiBadRequestError({ message: String(error) })
)
)
: pipe(
authGeminiLogout({
_tag: "AuthGeminiLogout",
label: request.label ?? null,
geminiAuthPath: geminiAuthRoot
}),
Effect.mapError(mapMenuAuthError),
Effect.zipRight(readAuthMenuSnapshot()),
Effect.mapError((error) =>
error instanceof ApiBadRequestError ? error : new ApiBadRequestError({ message: String(error) })
: request.flow === "GeminiLogout"
? pipe(
authGeminiLogout({
_tag: "AuthGeminiLogout",
label: request.label ?? null,
geminiAuthPath: geminiAuthRoot
}),
Effect.mapError(mapMenuAuthError),
Effect.zipRight(readAuthMenuSnapshot()),
Effect.mapError((error) =>
error instanceof ApiBadRequestError ? error : new ApiBadRequestError({ message: String(error) })
)
)
)
: request.flow === "GrokApiKey"
? pipe(
authGrokLogin(
{
_tag: "AuthGrokLogin",
label: request.label ?? null,
grokAuthPath: grokAuthRoot,
isWeb: true
},
request.apiKey ?? ""
),
Effect.mapError(mapMenuAuthError),
Effect.zipRight(readAuthMenuSnapshot()),
Effect.mapError((error) =>
error instanceof ApiBadRequestError ? error : new ApiBadRequestError({ message: String(error) })
)
)
: pipe(
authGrokLogout({
_tag: "AuthGrokLogout",
label: request.label ?? null,
grokAuthPath: grokAuthRoot
}),
Effect.mapError(mapMenuAuthError),
Effect.zipRight(readAuthMenuSnapshot()),
Effect.mapError((error) =>
error instanceof ApiBadRequestError ? error : new ApiBadRequestError({ message: String(error) })
)
)
4 changes: 3 additions & 1 deletion packages/api/src/services/auth-terminal-sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ const resolveCommandLabel = (request: AuthTerminalSessionRequest): string => {
const suffix = label === undefined || label.length === 0 ? "" : ` [${label}]`
return request.flow === "ClaudeOauth"
? `docker-git menu auth claude oauth${suffix}`
: `docker-git menu auth gemini oauth${suffix}`
: request.flow === "GeminiOauth"
? `docker-git menu auth gemini oauth${suffix}`
: `docker-git menu auth grok oauth${suffix}`
Comment on lines 72 to +76
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== AuthTerminalFlow declaration(s) =="
rg -n -C3 '\bAuthTerminalFlow\b|ClaudeOauth|GeminiOauth|GrokOauth' packages/api/src/api/contracts.ts packages/api/src/api/schema.ts

echo
echo "== resolveCommandLabel implementation =="
rg -n -C6 'const resolveCommandLabel' packages/api/src/services/auth-terminal-sessions.ts

Repository: ProverCoderAI/docker-git

Length of output: 1907


Используйте исчерпывающий разбор request.flow с Match.exhaustive()

Сейчас последняя ветка ternary неявно маппит любой новый flow в grok oauth, что нарушает требование к исчерпывающему анализу union-типов (coding guidelines). При расширении AuthTerminalFlow значение будет тихо отображено в неправильный label.

Замените вложенные ternary на Match.exhaustive() для проверки на уровне компилятора:

return Match.value(request.flow).pipe(
  Match.when("ClaudeOauth", () => `docker-git menu auth claude oauth${suffix}`),
  Match.when("GeminiOauth", () => `docker-git menu auth gemini oauth${suffix}`),
  Match.when("GrokOauth", () => `docker-git menu auth grok oauth${suffix}`),
  Match.exhaustive()
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/api/src/services/auth-terminal-sessions.ts` around lines 72 - 76,
The nested ternary that maps request.flow to a command string should be replaced
with an exhaustive Match-based switch to ensure compile-time coverage of all
AuthTerminalFlow variants: use Match.value(request.flow).pipe(...) with
Match.when handlers for "ClaudeOauth", "GeminiOauth", and "GrokOauth" that each
return the appropriate backtick string including the existing suffix, and finish
with Match.exhaustive() so new flows cannot silently fall through to the wrong
label; update the return expression accordingly where request.flow is used.

}

const resolveRunnerArgs = (flow: AuthTerminalFlow, label: string | null | undefined): ReadonlyArray<string> => {
Expand Down
2 changes: 1 addition & 1 deletion packages/api/src/services/container-tasks-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export type ManagedAgentPid = {
readonly pid: number
}

const interactiveAgentPattern = /\b(codex|claude|opencode|gemini)\b/u
const interactiveAgentPattern = /\b(codex|claude|opencode|gemini|grok)\b/u

const hasInteractiveTty = (process: RawContainerProcess): boolean =>
process.tty !== "?" && process.tty.trim().length > 0
Expand Down
5 changes: 4 additions & 1 deletion packages/api/src/services/federation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1695,7 +1695,7 @@ const resolveAgentProvider = (
subscription: FollowSubscription | undefined
): AgentProvider => {
const raw = subscription?.agentProvider ?? process.env["DOCKER_GIT_EXCHANGE_AGENT_PROVIDER"]
return raw === "claude" || raw === "opencode" || raw === "custom" ? raw : "codex"
return raw === "claude" || raw === "opencode" || raw === "grok" || raw === "custom" ? raw : "codex"
}

const buildTaskPrompt = (issue: FederationIssueRecord): string => {
Expand Down Expand Up @@ -1730,6 +1730,9 @@ const buildAgentCommand = (
if (provider === "opencode") {
return `opencode run ${shellEscape(prompt)}`
}
if (provider === "grok") {
return `MCP_PLAYWRIGHT_ISOLATED=1 grok --no-sandbox -p ${shellEscape(prompt)}`
}
if (provider === "custom") {
return `sh -lc ${shellEscape(`printf '%s\n' ${shellEscape(prompt)}`)}`
}
Expand Down
Loading
Loading