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
15 changes: 15 additions & 0 deletions .changeset/fix-claude-auth-login-probe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"@prover-coder-ai/docker-git": patch
---

Fix `docker-git auth claude login` failing after a successful OAuth login.

After `claude setup-token` created and persisted the OAuth token, the login
command ran a verification probe (`claude -p ping`) and treated any non-zero
exit as a hard failure, exiting with code 1 even though the token was already
saved. A transient probe failure (network hiccup, rate limit, or token
propagation delay) would therefore discard an otherwise successful login.

The probe failure is now reported as a warning instead of an error, mirroring
`docker-git auth claude status`. The token is kept, and the user is advised to
re-check connectivity later with `docker-git auth claude status`.
9 changes: 9 additions & 0 deletions packages/app/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# @prover-coder-ai/docker-git

## 1.3.14

### Patch Changes

- chore: automated version bump

- Updated dependencies []:
- @prover-coder-ai/docker-git-session-sync@1.0.70

## 1.3.13

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion packages/app/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@prover-coder-ai/docker-git",
"version": "1.3.13",
"version": "1.3.14",
"description": "docker-git Bun and Gridland CLI plus browser frontend",
"main": "dist/src/docker-git/main.js",
"bin": {
Expand Down
6 changes: 6 additions & 0 deletions packages/docker-git-session-sync/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# @prover-coder-ai/docker-git-session-sync

## 1.0.70

### Patch Changes

- chore: automated version bump

## 1.0.69

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion packages/docker-git-session-sync/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@prover-coder-ai/docker-git-session-sync",
"version": "1.0.69",
"version": "1.0.70",
"description": "Standalone docker-git AI agent session synchronization tool",
"main": "dist/docker-git-session-sync.js",
"bin": {
Expand Down
15 changes: 10 additions & 5 deletions packages/lib/src/usecases/auth-claude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,14 +268,19 @@ export const authClaudeLogin = (
yield* _(fs.writeFileString(claudeOauthTokenPath(accountPath), `${token}\n`))
yield* _(fs.chmod(claudeOauthTokenPath(accountPath), 0o600), Effect.orElseSucceed(() => void 0))
yield* _(resolveClaudeAuthMethod(fs, path, accountPath))
// CHANGE: treat a failing post-login API probe as a warning instead of a hard error
// WHY: the OAuth token is already created and persisted; a transient probe failure
// (network hiccup, rate limit, token propagation delay) must not discard a
// successful login. Mirrors authClaudeStatus, which only warns on probe failure.
// REF: issue-439
// SOURCE: n/a
const probeExitCode = yield* _(runClaudePingProbeExitCode(cwd, accountPath, token))
if (probeExitCode !== 0) {
yield* _(
Effect.fail(
new CommandFailedError({
command: "claude setup-token",
exitCode: probeExitCode
})
Effect.logWarning(
`Claude OAuth token saved (${accountLabel}), but the API probe failed (exit=${probeExitCode}). ` +
`The token may need a moment to activate, or there was a transient network issue. ` +
`Verify later with 'docker-git auth claude status'.`
)
)
}
Expand Down
162 changes: 162 additions & 0 deletions packages/lib/tests/usecases/auth-claude-login.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import * as Command from "@effect/platform/Command"
import * as CommandExecutor from "@effect/platform/CommandExecutor"
import * as FileSystem from "@effect/platform/FileSystem"
import * as Path from "@effect/platform/Path"
import { NodeContext } from "@effect/platform-node"
import { describe, expect, it } from "@effect/vitest"
import { Effect } from "effect"
import * as Inspectable from "effect/Inspectable"
import * as Sink from "effect/Sink"
import * as Stream from "effect/Stream"

import { authClaudeLogin } from "../../src/usecases/auth-claude.js"

const encode = (value: string): Uint8Array => new TextEncoder().encode(value)

const oauthToken = "sk-ant-oat01-EXAMPLE0123456789abcdef"

// Mirrors the real `claude setup-token` output that the OAuth parser scans for.
const setupTokenOutput = (token: string): string =>
[
"Welcome to Claude Code",
"",
" ✓ Long-lived authentication token created successfully!",
"",
" Your OAuth token (valid for 1 year):",
"",
` ${token}`,
"",
" Store this token securely. You won't be able to see it again."
].join("\n")

const isSetupToken = (args: ReadonlyArray<string>): boolean => args.includes("setup-token")
const isPingProbe = (args: ReadonlyArray<string>): boolean => args.includes("-p") && args.includes("ping")

// CHANGE: fake docker executor that captures a setup-token and lets the ping probe fail
// WHY: reproduce issue-439 where a successful OAuth login was discarded by a failing probe
// REF: issue-439
const makeFakeExecutor = (
token: string,
pingExitCode: number
): CommandExecutor.CommandExecutor => {
const start = (command: Command.Command): Effect.Effect<CommandExecutor.Process, never> =>
Effect.sync(() => {
const flattened = Command.flatten(command)
const invocation = flattened[flattened.length - 1]!
const args = invocation.args

const stdoutText = isSetupToken(args) ? setupTokenOutput(token) : ""
const exitCode = isPingProbe(args) ? pingExitCode : 0
const stdout = stdoutText.length === 0 ? Stream.empty : Stream.succeed(encode(stdoutText))

const process: CommandExecutor.Process = {
[CommandExecutor.ProcessTypeId]: CommandExecutor.ProcessTypeId,
pid: CommandExecutor.ProcessId(1),
exitCode: Effect.succeed(CommandExecutor.ExitCode(exitCode)),
isRunning: Effect.succeed(false),
kill: (_signal) => Effect.void,
stderr: Stream.empty,
stdin: Sink.drain,
stdout,
toJSON: () => ({ _tag: "ClaudeLoginTestProcess", command: invocation.command, args }),
[Inspectable.NodeInspectSymbol]: () => ({
_tag: "ClaudeLoginTestProcess",
command: invocation.command,
args
}),
toString: () => `[ClaudeLoginTestProcess ${invocation.command}]`
}

return process
})

return CommandExecutor.makeExecutor(start)
}

const withTempDir = <A, E, R>(
use: (tempDir: string) => Effect.Effect<A, E, R>
): Effect.Effect<A, E, R | FileSystem.FileSystem> =>
Effect.scoped(
Effect.gen(function*(_) {
const fs = yield* _(FileSystem.FileSystem)
const tempDir = yield* _(fs.makeTempDirectoryScoped({ prefix: "docker-git-auth-claude-" }))
return yield* _(use(tempDir))
})
)

const withPatchedEnv = <A, E, R>(
patch: Readonly<Record<string, string | undefined>>,
effect: Effect.Effect<A, E, R>
): Effect.Effect<A, E, R> =>
Effect.acquireUseRelease(
Effect.sync(() => {
const previous = new Map<string, string | undefined>()
for (const [key, value] of Object.entries(patch)) {
previous.set(key, process.env[key])
if (value === undefined) {
delete process.env[key]
} else {
process.env[key] = value
}
}
return previous
}),
() => effect,
(previous) =>
Effect.sync(() => {
for (const [key, value] of previous.entries()) {
if (value === undefined) {
delete process.env[key]
} else {
process.env[key] = value
}
}
})
)

const runLoginAndReadToken = (
root: string,
pingExitCode: number
): Effect.Effect<string, unknown, FileSystem.FileSystem | Path.Path> =>
Effect.gen(function*(_) {
const fs = yield* _(FileSystem.FileSystem)
const path = yield* _(Path.Path)
const claudeAuthPath = path.join(root, ".docker-git/.orch/auth/claude")

yield* _(
authClaudeLogin({
_tag: "AuthClaudeLogin",
label: null,
claudeAuthPath
}).pipe(
Effect.provideService(CommandExecutor.CommandExecutor, makeFakeExecutor(oauthToken, pingExitCode))
)
)

return yield* _(fs.readFileString(path.join(claudeAuthPath, "default", ".oauth-token")))
})

describe("authClaudeLogin", () => {
// Regression for issue-439: a non-zero probe exit must not discard a created token.
it.effect("persists the OAuth token even when the post-login API probe fails", () =>
withTempDir((root) =>
withPatchedEnv(
{ HOME: root, DOCKER_GIT_STATE_AUTO_SYNC: "0", DOCKER_GIT_PROJECTS_ROOT: undefined },
Effect.gen(function*(_) {
const persisted = yield* _(runLoginAndReadToken(root, 7))
expect(persisted.trim()).toBe(oauthToken)
})
)
).pipe(Effect.provide(NodeContext.layer)))

it.effect("persists the OAuth token when the post-login API probe succeeds", () =>
withTempDir((root) =>
withPatchedEnv(
{ HOME: root, DOCKER_GIT_STATE_AUTO_SYNC: "0", DOCKER_GIT_PROJECTS_ROOT: undefined },
Effect.gen(function*(_) {
const persisted = yield* _(runLoginAndReadToken(root, 0))
expect(persisted.trim()).toBe(oauthToken)
})
)
).pipe(Effect.provide(NodeContext.layer)))
})
Loading