Skip to content

Commit a3f9900

Browse files
committed
fix(auth): harden claude global auth fallback and compose retries
1 parent 9c1cb11 commit a3f9900

File tree

12 files changed

+118
-21
lines changed

12 files changed

+118
-21
lines changed

packages/app/src/docker-git/menu-project-auth-data.ts

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -195,21 +195,31 @@ const updateProjectGitDisconnect = (spec: ProjectEnvUpdateSpec): Effect.Effect<s
195195
return Effect.succeed(clearProjectGitLabels(withoutUser))
196196
}
197197

198+
const resolveClaudeAccountCandidates = (
199+
claudeAuthPath: string,
200+
accountLabel: string
201+
): ReadonlyArray<string> =>
202+
accountLabel === "default"
203+
? [`${claudeAuthPath}/default`, claudeAuthPath]
204+
: [`${claudeAuthPath}/${accountLabel}`]
205+
198206
const updateProjectClaudeConnect = (spec: ProjectEnvUpdateSpec): Effect.Effect<string, AppError> => {
199207
const accountLabel = normalizeAccountLabel(spec.rawLabel, "default")
200-
const accountPath = `${spec.claudeAuthPath}/${accountLabel}`
208+
const accountCandidates = resolveClaudeAccountCandidates(spec.claudeAuthPath, accountLabel)
201209
return Effect.gen(function*(_) {
202-
const exists = yield* _(spec.fs.exists(accountPath))
203-
if (!exists) {
204-
return yield* _(Effect.fail(missingSecret("Claude Code login", spec.canonicalLabel, spec.claudeAuthPath)))
205-
}
210+
for (const accountPath of accountCandidates) {
211+
const exists = yield* _(spec.fs.exists(accountPath))
212+
if (!exists) {
213+
continue
214+
}
206215

207-
const hasCredentials = yield* _(
208-
hasClaudeAccountCredentials(spec.fs, accountPath),
209-
Effect.orElseSucceed(() => false)
210-
)
211-
if (hasCredentials) {
212-
return upsertEnvKey(spec.projectEnvText, projectClaudeLabelKey, spec.canonicalLabel)
216+
const hasCredentials = yield* _(
217+
hasClaudeAccountCredentials(spec.fs, accountPath),
218+
Effect.orElseSucceed(() => false)
219+
)
220+
if (hasCredentials) {
221+
return upsertEnvKey(spec.projectEnvText, projectClaudeLabelKey, spec.canonicalLabel)
222+
}
213223
}
214224

215225
return yield* _(Effect.fail(missingSecret("Claude Code login", spec.canonicalLabel, spec.claudeAuthPath)))

packages/app/tests/docker-git/entrypoint-auth.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ describe("renderEntrypoint auth bridge", () => {
4040
expect(entrypoint).toContain("CLAUDE_SETTINGS_FILE=\"$CLAUDE_CONFIG_DIR/.claude.json\"")
4141
expect(entrypoint).toContain("nextServers.playwright = {")
4242
expect(entrypoint).toContain("command: \"docker-git-playwright-mcp\"")
43+
expect(entrypoint).toContain("CLAUDE_ROOT_TOKEN_FILE=\"$CLAUDE_AUTH_ROOT/.oauth-token\"")
44+
expect(entrypoint).toContain("CLAUDE_ROOT_CONFIG_FILE=\"$CLAUDE_AUTH_ROOT/.config.json\"")
4345
expect(entrypoint).toContain("CLAUDE_GLOBAL_PROMPT_FILE=\"/home/dev/.claude/CLAUDE.md\"")
4446
expect(entrypoint).toContain("CLAUDE_AUTO_SYSTEM_PROMPT=\"${CLAUDE_AUTO_SYSTEM_PROMPT:-1}\"")
4547
expect(entrypoint).toContain("docker-git-managed:claude-md")

packages/lib/src/core/templates-entrypoint/claude.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,16 @@ fi
1919
2020
CLAUDE_AUTH_ROOT="${claudeAuthRootContainerPath(config.sshUser)}"
2121
CLAUDE_CONFIG_DIR="$CLAUDE_AUTH_ROOT/$CLAUDE_LABEL_NORM"
22+
23+
# Backward compatibility: if default auth is stored directly under claude root, reuse it.
24+
if [[ "$CLAUDE_LABEL_NORM" == "default" ]]; then
25+
CLAUDE_ROOT_TOKEN_FILE="$CLAUDE_AUTH_ROOT/.oauth-token"
26+
CLAUDE_ROOT_CONFIG_FILE="$CLAUDE_AUTH_ROOT/.config.json"
27+
if [[ -f "$CLAUDE_ROOT_TOKEN_FILE" ]] || [[ -f "$CLAUDE_ROOT_CONFIG_FILE" ]]; then
28+
CLAUDE_CONFIG_DIR="$CLAUDE_AUTH_ROOT"
29+
fi
30+
fi
31+
2232
export CLAUDE_CONFIG_DIR
2333
2434
mkdir -p "$CLAUDE_CONFIG_DIR" || true

packages/lib/src/shell/docker.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as Command from "@effect/platform/Command"
22
import type * as CommandExecutor from "@effect/platform/CommandExecutor"
33
import { ExitCode } from "@effect/platform/CommandExecutor"
44
import type { PlatformError } from "@effect/platform/Error"
5-
import { Effect, pipe } from "effect"
5+
import { Duration, Effect, Schedule, pipe } from "effect"
66

77
import { runCommandCapture, runCommandExitCode, runCommandWithExitCodes } from "./command-runner.js"
88
import { CommandFailedError, DockerCommandError } from "./errors.js"
@@ -51,6 +51,24 @@ const runComposeCapture = (
5151
(exitCode) => new DockerCommandError({ exitCode })
5252
)
5353

54+
const dockerComposeUpRetrySchedule = Schedule.addDelay(
55+
Schedule.recurs(2),
56+
() => Duration.seconds(2)
57+
)
58+
59+
const retryDockerComposeUp = (
60+
cwd: string,
61+
effect: Effect.Effect<void, DockerCommandError | PlatformError, CommandExecutor.CommandExecutor>
62+
): Effect.Effect<void, DockerCommandError | PlatformError, CommandExecutor.CommandExecutor> =>
63+
effect.pipe(
64+
Effect.tapError(() =>
65+
Effect.logWarning(
66+
`docker compose up failed in ${cwd}; retrying (possible transient Docker Hub/DNS issue)...`
67+
)
68+
),
69+
Effect.retry(dockerComposeUpRetrySchedule)
70+
)
71+
5472
// CHANGE: run docker compose up -d --build in the target directory
5573
// WHY: provide a controlled shell effect for image creation
5674
// QUOTE(ТЗ): "создавать докер образы"
@@ -64,7 +82,10 @@ const runComposeCapture = (
6482
export const runDockerComposeUp = (
6583
cwd: string
6684
): Effect.Effect<void, DockerCommandError | PlatformError, CommandExecutor.CommandExecutor> =>
67-
runCompose(cwd, ["up", "-d", "--build"], [Number(ExitCode(0))])
85+
retryDockerComposeUp(
86+
cwd,
87+
runCompose(cwd, ["up", "-d", "--build"], [Number(ExitCode(0))])
88+
)
6889

6990
export const dockerComposeUpRecreateArgs: ReadonlyArray<string> = [
7091
"up",
@@ -86,7 +107,10 @@ export const dockerComposeUpRecreateArgs: ReadonlyArray<string> = [
86107
export const runDockerComposeUpRecreate = (
87108
cwd: string
88109
): Effect.Effect<void, DockerCommandError | PlatformError, CommandExecutor.CommandExecutor> =>
89-
runCompose(cwd, dockerComposeUpRecreateArgs, [Number(ExitCode(0))])
110+
retryDockerComposeUp(
111+
cwd,
112+
runCompose(cwd, dockerComposeUpRecreateArgs, [Number(ExitCode(0))])
113+
)
90114

91115
// CHANGE: run docker compose down in the target directory
92116
// WHY: allow stopping managed containers from the CLI/menu

packages/lib/src/usecases/actions/prepare-files.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,5 +169,6 @@ export const migrateProjectOrchLayout = (
169169
globalConfig.envGlobalPath,
170170
globalConfig.envProjectPath,
171171
globalConfig.codexAuthPath,
172-
resolveRootPath(".docker-git/.orch/auth/gh")
172+
resolveRootPath(".docker-git/.orch/auth/gh"),
173+
resolveRootPath(".docker-git/.orch/auth/claude")
173174
)

packages/lib/src/usecases/auth-claude.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ const ensureClaudeOrchLayout = (
4545
defaultTemplateConfig.envGlobalPath,
4646
defaultTemplateConfig.envProjectPath,
4747
defaultTemplateConfig.codexAuthPath,
48-
".docker-git/.orch/auth/gh"
48+
".docker-git/.orch/auth/gh",
49+
".docker-git/.orch/auth/claude"
4950
)
5051

5152
const renderClaudeDockerfile = (): string =>

packages/lib/src/usecases/auth-codex.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ const ensureCodexOrchLayout = (
3636
defaultTemplateConfig.envGlobalPath,
3737
defaultTemplateConfig.envProjectPath,
3838
codexAuthPath,
39-
".docker-git/.orch/auth/gh"
39+
".docker-git/.orch/auth/gh",
40+
".docker-git/.orch/auth/claude"
4041
)
4142

4243
const renderCodexDockerfile = (): string =>

packages/lib/src/usecases/auth-github.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ const ensureGithubOrchLayout = (
4343
envGlobalPath,
4444
defaultTemplateConfig.envProjectPath,
4545
defaultTemplateConfig.codexAuthPath,
46-
ghAuthRoot
46+
ghAuthRoot,
47+
".docker-git/.orch/auth/claude"
4748
)
4849

4950
const normalizeGithubLabel = (value: string | null): string => {

packages/lib/src/usecases/auth-sync.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,8 @@ export const migrateLegacyOrchLayout = (
277277
envGlobalPath: string,
278278
envProjectPath: string,
279279
codexAuthPath: string,
280-
ghAuthPath: string
280+
ghAuthPath: string,
281+
claudeAuthPath: string
281282
): Effect.Effect<void, PlatformError, FileSystem.FileSystem | Path.Path> =>
282283
withFsPathContext(({ fs, path }) =>
283284
Effect.gen(function*(_) {
@@ -295,15 +296,18 @@ export const migrateLegacyOrchLayout = (
295296
const legacyEnvProject = path.join(legacyRoot, "env", "project.env")
296297
const legacyCodex = path.join(legacyRoot, "auth", "codex")
297298
const legacyGh = path.join(legacyRoot, "auth", "gh")
299+
const legacyClaude = path.join(legacyRoot, "auth", "claude")
298300

299301
const resolvedEnvGlobal = resolvePathFromBase(path, baseDir, envGlobalPath)
300302
const resolvedEnvProject = resolvePathFromBase(path, baseDir, envProjectPath)
301303
const resolvedCodex = resolvePathFromBase(path, baseDir, codexAuthPath)
302304
const resolvedGh = resolvePathFromBase(path, baseDir, ghAuthPath)
305+
const resolvedClaude = resolvePathFromBase(path, baseDir, claudeAuthPath)
303306

304307
yield* _(copyFileIfNeeded(legacyEnvGlobal, resolvedEnvGlobal))
305308
yield* _(copyFileIfNeeded(legacyEnvProject, resolvedEnvProject))
306309
yield* _(copyDirIfEmpty(fs, path, legacyCodex, resolvedCodex, "Codex auth"))
307310
yield* _(copyDirIfEmpty(fs, path, legacyGh, resolvedGh, "GH auth"))
311+
yield* _(copyDirIfEmpty(fs, path, legacyClaude, resolvedClaude, "Claude auth"))
308312
})
309313
)

packages/lib/src/usecases/errors.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@ const renderPrimaryError = (error: NonParseError): string | null =>
8282
`docker compose failed with exit code ${exitCode}`,
8383
"Hint: ensure Docker daemon is running and current user can access /var/run/docker.sock (for example via the docker group).",
8484
"Hint: if output above contains 'port is already allocated', retry with a free SSH port via --ssh-port <port> (for example --ssh-port 2235), or stop the conflicting project/container.",
85-
"Hint: if output above contains 'all predefined address pools have been fully subnetted', run `docker network prune -f`, configure Docker `default-address-pools`, or use shared network mode (`--network-mode shared`)."
85+
"Hint: if output above contains 'all predefined address pools have been fully subnetted', run `docker network prune -f`, configure Docker `default-address-pools`, or use shared network mode (`--network-mode shared`).",
86+
"Hint: if output above contains 'lookup auth.docker.io' or 'read udp ... [::1]:53 ... connection refused', fix Docker DNS resolver (set working DNS in host/daemon config) and retry."
8687
].join("\n")),
8788
Match.when({ _tag: "DockerAccessError" }, ({ details, issue }) =>
8889
[

0 commit comments

Comments
 (0)