Skip to content

Commit bc1da97

Browse files
committed
fix(shell): isolate claude oauth env boundary
1 parent 06a4e2a commit bc1da97

4 files changed

Lines changed: 87 additions & 12 deletions

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import {
2+
dockerGitClaudeOauthTokenEnvKey,
3+
readClaudeOauthTokenFromEnv
4+
} from "@prover-coder-ai/docker-git-auth-oauth/claude-oauth-token"
5+
6+
// CHANGE: read the Docker Git Claude OAuth token only at the shell boundary
7+
// WHY: usecases and shared runners should receive decoded boundary values explicitly
8+
// QUOTE(ТЗ): "Исправь CI/CD и все правки от Rabbit Coder."
9+
// REF: PR-440-CodeRabbit-env-boundary
10+
// SOURCE: n/a
11+
// FORMAT THEOREM: forall env: token(env) = Some(t) -> process_token() = Some(t)
12+
// PURITY: SHELL
13+
// EFFECT: reads process.env
14+
// INVARIANT: only a normalized non-empty DOCKER_GIT_CLAUDE_OAUTH_TOKEN crosses into the login flow
15+
// COMPLEXITY: O(1)
16+
export const readDockerGitClaudeOauthTokenFromProcessEnv = (): string | null =>
17+
readClaudeOauthTokenFromEnv(process.env, [dockerGitClaudeOauthTokenEnvKey])

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

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,10 @@ import {
99
runClaudeDockerOauth
1010
} from "@prover-coder-ai/docker-git-auth-oauth/claude-docker-oauth"
1111
import {
12-
dockerGitClaudeOauthTokenEnvKey,
1312
extractClaudeOauthToken,
1413
flushClaudeOauthTokenRedactionState,
1514
initialClaudeOauthTokenRedactionState,
16-
redactClaudeOauthTokenChunk,
17-
readClaudeOauthTokenFromEnv
15+
redactClaudeOauthTokenChunk
1816
} from "@prover-coder-ai/docker-git-auth-oauth/claude-oauth-token"
1917
import { Effect, pipe } from "effect"
2018
import * as Fiber from "effect/Fiber"
@@ -205,13 +203,13 @@ export const runClaudeOauthLoginWithPrompt = (
205203
cwd: string,
206204
accountPath: string,
207205
options: {
206+
readonly envToken: string | null
208207
readonly image: string
209208
readonly containerPath: string
210209
}
211210
): Effect.Effect<string, AuthError | CommandFailedError | PlatformError, CommandExecutor.CommandExecutor> => {
212-
const envToken = readClaudeOauthTokenFromEnv(process.env, [dockerGitClaudeOauthTokenEnvKey])
213-
if (envToken !== null) {
214-
return Effect.succeed(envToken)
211+
if (options.envToken !== null) {
212+
return Effect.succeed(options.envToken)
215213
}
216214

217215
return Effect.scoped(

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { Effect } from "effect"
1212

1313
import type { AuthClaudeLoginCommand, AuthClaudeLogoutCommand, AuthClaudeStatusCommand } from "../core/domain.js"
1414
import { defaultTemplateConfig } from "../core/domain.js"
15+
import { readDockerGitClaudeOauthTokenFromProcessEnv } from "../shell/claude-oauth-env.js"
1516
import { runDockerAuth, runDockerAuthExitCode } from "../shell/docker-auth.js"
1617
import type { AuthError } from "../shell/errors.js"
1718
import { CommandFailedError } from "../shell/errors.js"
@@ -283,6 +284,7 @@ export const authClaudeLogin = (
283284
runClaudeLoginFlow({
284285
accountLabel,
285286
captureToken: runClaudeOauthLoginWithPrompt(cwd, accountPath, {
287+
envToken: readDockerGitClaudeOauthTokenFromProcessEnv(),
286288
image: claudeImageName,
287289
containerPath: claudeContainerHomeDir
288290
}),

packages/lib/tests/usecases/auth-claude-login.test.ts

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ import * as CommandExecutor from "@effect/platform/CommandExecutor"
33
import * as FileSystem from "@effect/platform/FileSystem"
44
import * as Path from "@effect/platform/Path"
55
import { NodeContext } from "@effect/platform-node"
6-
import { claudeOauthTokenFileMode } from "@prover-coder-ai/docker-git-auth-oauth/claude-oauth-token"
6+
import {
7+
claudeOauthTokenFileMode,
8+
dockerGitClaudeOauthTokenEnvKey
9+
} from "@prover-coder-ai/docker-git-auth-oauth/claude-oauth-token"
710
import { describe, expect, it } from "@effect/vitest"
811
import { Effect, Logger } from "effect"
912
import * as Inspectable from "effect/Inspectable"
@@ -46,13 +49,15 @@ const isPingProbe = (args: ReadonlyArray<string>): boolean => args.includes("-p"
4649
// REF: issue-439
4750
const makeFakeExecutor = (
4851
token: string | null,
49-
pingExitCode: number
52+
pingExitCode: number,
53+
invocations: Array<{ readonly command: string; readonly args: ReadonlyArray<string> }> = []
5054
): CommandExecutor.CommandExecutor => {
5155
const start = (command: Command.Command): Effect.Effect<CommandExecutor.Process, never> =>
5256
Effect.sync(() => {
5357
const flattened = Command.flatten(command)
5458
const invocation = flattened[flattened.length - 1]!
5559
const args = invocation.args
60+
invocations.push({ command: invocation.command, args })
5661

5762
const stdoutText = isSetupToken(args)
5863
? token === null
@@ -192,7 +197,12 @@ describe("authClaudeLogin", () => {
192197
it.effect("persists the OAuth token even when the post-login API probe fails", () =>
193198
withTempDir((root) =>
194199
withPatchedEnv(
195-
{ HOME: root, DOCKER_GIT_STATE_AUTO_SYNC: "0", DOCKER_GIT_PROJECTS_ROOT: undefined },
200+
{
201+
HOME: root,
202+
DOCKER_GIT_STATE_AUTO_SYNC: "0",
203+
DOCKER_GIT_PROJECTS_ROOT: undefined,
204+
[dockerGitClaudeOauthTokenEnvKey]: undefined
205+
},
196206
Effect.gen(function*(_) {
197207
const { logs, tokenText } = yield* _(runLoginAndReadToken(root, 7))
198208
expect(tokenText.trim()).toBe(oauthToken)
@@ -204,18 +214,61 @@ describe("authClaudeLogin", () => {
204214
it.effect("persists the OAuth token when the post-login API probe succeeds", () =>
205215
withTempDir((root) =>
206216
withPatchedEnv(
207-
{ HOME: root, DOCKER_GIT_STATE_AUTO_SYNC: "0", DOCKER_GIT_PROJECTS_ROOT: undefined },
217+
{
218+
HOME: root,
219+
DOCKER_GIT_STATE_AUTO_SYNC: "0",
220+
DOCKER_GIT_PROJECTS_ROOT: undefined,
221+
[dockerGitClaudeOauthTokenEnvKey]: undefined
222+
},
208223
Effect.gen(function*(_) {
209224
const { tokenText } = yield* _(runLoginAndReadToken(root, 0))
210225
expect(tokenText.trim()).toBe(oauthToken)
211226
})
212227
)
213228
).pipe(Effect.provide(NodeContext.layer)))
214229

230+
it.effect("uses a decoded docker-git OAuth env token without running setup-token", () =>
231+
withTempDir((root) =>
232+
withPatchedEnv(
233+
{
234+
HOME: root,
235+
DOCKER_GIT_STATE_AUTO_SYNC: "0",
236+
DOCKER_GIT_PROJECTS_ROOT: undefined,
237+
[dockerGitClaudeOauthTokenEnvKey]: ` ${oauthToken} `
238+
},
239+
Effect.gen(function*(_) {
240+
const fs = yield* _(FileSystem.FileSystem)
241+
const path = yield* _(Path.Path)
242+
const invocations: Array<{ readonly command: string; readonly args: ReadonlyArray<string> }> = []
243+
const claudeAuthPath = path.join(root, ".docker-git/.orch/auth/claude")
244+
245+
yield* _(
246+
authClaudeLogin({
247+
_tag: "AuthClaudeLogin",
248+
label: null,
249+
claudeAuthPath
250+
}).pipe(
251+
Effect.provideService(CommandExecutor.CommandExecutor, makeFakeExecutor(null, 0, invocations))
252+
)
253+
)
254+
255+
const tokenText = yield* _(fs.readFileString(path.join(claudeAuthPath, "default", ".oauth-token")))
256+
expect(tokenText.trim()).toBe(oauthToken)
257+
expect(invocations.some((invocation) => isSetupToken(invocation.args))).toBe(false)
258+
expect(invocations.some((invocation) => isPingProbe(invocation.args))).toBe(true)
259+
})
260+
)
261+
).pipe(Effect.provide(NodeContext.layer)))
262+
215263
it.effect("replaces an existing token symlink without writing the secret to the symlink target", () =>
216264
withTempDir((root) =>
217265
withPatchedEnv(
218-
{ HOME: root, DOCKER_GIT_STATE_AUTO_SYNC: "0", DOCKER_GIT_PROJECTS_ROOT: undefined },
266+
{
267+
HOME: root,
268+
DOCKER_GIT_STATE_AUTO_SYNC: "0",
269+
DOCKER_GIT_PROJECTS_ROOT: undefined,
270+
[dockerGitClaudeOauthTokenEnvKey]: undefined
271+
},
219272
Effect.gen(function*(_) {
220273
const fs = yield* _(FileSystem.FileSystem)
221274
const path = yield* _(Path.Path)
@@ -266,7 +319,12 @@ describe("authClaudeLogin", () => {
266319
it.effect("fails when setup-token completes without a captured OAuth token", () =>
267320
withTempDir((root) =>
268321
withPatchedEnv(
269-
{ HOME: root, DOCKER_GIT_STATE_AUTO_SYNC: "0", DOCKER_GIT_PROJECTS_ROOT: undefined },
322+
{
323+
HOME: root,
324+
DOCKER_GIT_STATE_AUTO_SYNC: "0",
325+
DOCKER_GIT_PROJECTS_ROOT: undefined,
326+
[dockerGitClaudeOauthTokenEnvKey]: undefined
327+
},
270328
Effect.gen(function*(_) {
271329
yield* _(runLoginWithoutCapturedToken(root))
272330
})

0 commit comments

Comments
 (0)