Skip to content

Commit a2c7c0a

Browse files
committed
fix(shell): harden codex auth bootstrap seeding
1 parent c73eed1 commit a2c7c0a

File tree

4 files changed

+245
-40
lines changed

4 files changed

+245
-40
lines changed

packages/app/src/lib/usecases/shared-volume-seed.ts

Lines changed: 65 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,25 +16,35 @@ import type { DockerCommandError } from "../shell/errors.js"
1616

1717
type SharedVolumeSeedEnvironment = CommandExecutor | FileSystem.FileSystem | Path.Path
1818

19+
const isNotFoundSystemError = (error: PlatformError): boolean =>
20+
error._tag === "SystemError" && error.reason === "NotFound"
21+
1922
const resolvePathFromBase = (
2023
path: Path.Path,
2124
baseDir: string,
2225
targetPath: string
2326
): string => (path.isAbsolute(targetPath) ? targetPath : path.resolve(baseDir, targetPath))
2427

28+
const statIfPresent = (
29+
fs: FileSystem.FileSystem,
30+
targetPath: string
31+
): Effect.Effect<FileSystem.File.Info | null, PlatformError> =>
32+
fs.stat(targetPath).pipe(
33+
Effect.catchTag("SystemError", (error) =>
34+
isNotFoundSystemError(error)
35+
? Effect.succeed(null)
36+
: Effect.fail(error))
37+
)
38+
2539
const copyDirRecursive = (
2640
fs: FileSystem.FileSystem,
2741
path: Path.Path,
2842
sourceDir: string,
2943
targetDir: string
3044
): Effect.Effect<void, PlatformError, FileSystem.FileSystem | Path.Path> =>
3145
Effect.gen(function*(_) {
32-
const exists = yield* _(fs.exists(sourceDir))
33-
if (!exists) {
34-
return
35-
}
36-
const info = yield* _(fs.stat(sourceDir))
37-
if (info.type !== "Directory") {
46+
const info = yield* _(statIfPresent(fs, sourceDir))
47+
if (info === null || info.type !== "Directory") {
3848
return
3949
}
4050

@@ -43,7 +53,10 @@ const copyDirRecursive = (
4353
for (const entry of entries) {
4454
const sourceEntry = path.join(sourceDir, entry)
4555
const targetEntry = path.join(targetDir, entry)
46-
const entryInfo = yield* _(fs.stat(sourceEntry))
56+
const entryInfo = yield* _(statIfPresent(fs, sourceEntry))
57+
if (entryInfo === null) {
58+
continue
59+
}
4760
if (entryInfo.type === "Directory") {
4861
yield* _(copyDirRecursive(fs, path, sourceEntry, targetEntry))
4962
} else if (entryInfo.type === "File") {
@@ -60,18 +73,48 @@ const copyFileIfPresent = (
6073
targetPath: string
6174
): Effect.Effect<void, PlatformError, FileSystem.FileSystem | Path.Path> =>
6275
Effect.gen(function*(_) {
63-
const exists = yield* _(fs.exists(sourcePath))
64-
if (!exists) {
65-
return
66-
}
67-
const info = yield* _(fs.stat(sourcePath))
68-
if (info.type !== "File") {
76+
const info = yield* _(statIfPresent(fs, sourcePath))
77+
if (info === null || info.type !== "File") {
6978
return
7079
}
7180
yield* _(fs.makeDirectory(path.dirname(targetPath), { recursive: true }))
7281
yield* _(fs.copyFile(sourcePath, targetPath))
7382
})
7483

84+
const copyCodexAuthFileIfPresent = (
85+
fs: FileSystem.FileSystem,
86+
path: Path.Path,
87+
sourceDir: string,
88+
targetDir: string,
89+
fileName: "auth.json" | "config.toml"
90+
): Effect.Effect<void, PlatformError, FileSystem.FileSystem | Path.Path> =>
91+
copyFileIfPresent(fs, path, path.join(sourceDir, fileName), path.join(targetDir, fileName))
92+
93+
const copyLabeledCodexFiles = (
94+
fs: FileSystem.FileSystem,
95+
path: Path.Path,
96+
sourceRoot: string,
97+
targetRoot: string,
98+
fileName: "auth.json" | "config.toml"
99+
): Effect.Effect<void, PlatformError, FileSystem.FileSystem | Path.Path> =>
100+
Effect.gen(function*(_) {
101+
const sourceInfo = yield* _(statIfPresent(fs, sourceRoot))
102+
if (sourceInfo === null || sourceInfo.type !== "Directory") {
103+
return
104+
}
105+
106+
const entries = yield* _(fs.readDirectory(sourceRoot))
107+
for (const entry of entries) {
108+
const sourceEntry = path.join(sourceRoot, entry)
109+
const entryInfo = yield* _(statIfPresent(fs, sourceEntry))
110+
if (entryInfo === null || entryInfo.type !== "Directory") {
111+
continue
112+
}
113+
114+
yield* _(copyCodexAuthFileIfPresent(fs, path, sourceEntry, path.join(targetRoot, entry), fileName))
115+
}
116+
})
117+
75118
type BootstrapSeedConfig = Pick<
76119
TemplateConfig,
77120
"authorizedKeysPath" | "envGlobalPath" | "envProjectPath" | "codexAuthPath" | "codexSharedAuthPath"
@@ -163,12 +206,18 @@ const copyBootstrapSnapshotAuthDirs = (
163206
targets: BootstrapSnapshotTargets
164207
): Effect.Effect<void, PlatformError, FileSystem.FileSystem | Path.Path> =>
165208
Effect.gen(function*(_) {
166-
yield* _(copyDirRecursive(fs, path, sources.codexAuthSource, targets.projectCodexTarget))
209+
yield* _(copyCodexAuthFileIfPresent(fs, path, sources.codexAuthSource, targets.projectCodexTarget, "auth.json"))
210+
yield* _(copyCodexAuthFileIfPresent(fs, path, sources.codexAuthSource, targets.projectCodexTarget, "config.toml"))
211+
yield* _(copyLabeledCodexFiles(fs, path, sources.codexAuthSource, targets.projectCodexTarget, "auth.json"))
212+
yield* _(copyLabeledCodexFiles(fs, path, sources.codexAuthSource, targets.projectCodexTarget, "config.toml"))
167213
yield* _(copyDirRecursive(fs, path, sources.claudeAuthSource, targets.projectClaudeTarget))
168-
yield* _(copyDirRecursive(fs, path, sources.codexSharedAuthSource, targets.sharedCodexTarget))
214+
yield* _(
215+
copyCodexAuthFileIfPresent(fs, path, sources.codexSharedAuthSource, targets.sharedCodexTarget, "auth.json")
216+
)
217+
yield* _(copyLabeledCodexFiles(fs, path, sources.codexSharedAuthSource, targets.sharedCodexTarget, "auth.json"))
169218
})
170219

171-
const stageBootstrapSnapshot = (
220+
export const stageBootstrapSnapshot = (
172221
stagingDir: string,
173222
projectDir: string,
174223
config: BootstrapSeedConfig

packages/lib/src/usecases/shared-volume-seed.ts

Lines changed: 63 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,25 +15,35 @@ import type { DockerCommandError } from "../shell/errors.js"
1515

1616
type SharedVolumeSeedEnvironment = CommandExecutor | FileSystem.FileSystem | Path.Path
1717

18+
const isNotFoundSystemError = (error: PlatformError): boolean =>
19+
error._tag === "SystemError" && error.reason === "NotFound"
20+
1821
const resolvePathFromBase = (
1922
path: Path.Path,
2023
baseDir: string,
2124
targetPath: string
2225
): string => (path.isAbsolute(targetPath) ? targetPath : path.resolve(baseDir, targetPath))
2326

27+
const statIfPresent = (
28+
fs: FileSystem.FileSystem,
29+
targetPath: string
30+
): Effect.Effect<FileSystem.File.Info | null, PlatformError> =>
31+
fs.stat(targetPath).pipe(
32+
Effect.catchTag("SystemError", (error) =>
33+
isNotFoundSystemError(error)
34+
? Effect.succeed(null)
35+
: Effect.fail(error))
36+
)
37+
2438
const copyDirRecursive = (
2539
fs: FileSystem.FileSystem,
2640
path: Path.Path,
2741
sourceDir: string,
2842
targetDir: string
2943
): Effect.Effect<void, PlatformError, FileSystem.FileSystem | Path.Path> =>
3044
Effect.gen(function*(_) {
31-
const exists = yield* _(fs.exists(sourceDir))
32-
if (!exists) {
33-
return
34-
}
35-
const info = yield* _(fs.stat(sourceDir))
36-
if (info.type !== "Directory") {
45+
const info = yield* _(statIfPresent(fs, sourceDir))
46+
if (info === null || info.type !== "Directory") {
3747
return
3848
}
3949

@@ -42,7 +52,10 @@ const copyDirRecursive = (
4252
for (const entry of entries) {
4353
const sourceEntry = path.join(sourceDir, entry)
4454
const targetEntry = path.join(targetDir, entry)
45-
const entryInfo = yield* _(fs.stat(sourceEntry))
55+
const entryInfo = yield* _(statIfPresent(fs, sourceEntry))
56+
if (entryInfo === null) {
57+
continue
58+
}
4659
if (entryInfo.type === "Directory") {
4760
yield* _(copyDirRecursive(fs, path, sourceEntry, targetEntry))
4861
} else if (entryInfo.type === "File") {
@@ -59,18 +72,48 @@ const copyFileIfPresent = (
5972
targetPath: string
6073
): Effect.Effect<void, PlatformError, FileSystem.FileSystem | Path.Path> =>
6174
Effect.gen(function*(_) {
62-
const exists = yield* _(fs.exists(sourcePath))
63-
if (!exists) {
64-
return
65-
}
66-
const info = yield* _(fs.stat(sourcePath))
67-
if (info.type !== "File") {
75+
const info = yield* _(statIfPresent(fs, sourcePath))
76+
if (info === null || info.type !== "File") {
6877
return
6978
}
7079
yield* _(fs.makeDirectory(path.dirname(targetPath), { recursive: true }))
7180
yield* _(fs.copyFile(sourcePath, targetPath))
7281
})
7382

83+
const copyCodexAuthFileIfPresent = (
84+
fs: FileSystem.FileSystem,
85+
path: Path.Path,
86+
sourceDir: string,
87+
targetDir: string,
88+
fileName: "auth.json" | "config.toml"
89+
): Effect.Effect<void, PlatformError, FileSystem.FileSystem | Path.Path> =>
90+
copyFileIfPresent(fs, path, path.join(sourceDir, fileName), path.join(targetDir, fileName))
91+
92+
const copyLabeledCodexFiles = (
93+
fs: FileSystem.FileSystem,
94+
path: Path.Path,
95+
sourceRoot: string,
96+
targetRoot: string,
97+
fileName: "auth.json" | "config.toml"
98+
): Effect.Effect<void, PlatformError, FileSystem.FileSystem | Path.Path> =>
99+
Effect.gen(function*(_) {
100+
const sourceInfo = yield* _(statIfPresent(fs, sourceRoot))
101+
if (sourceInfo === null || sourceInfo.type !== "Directory") {
102+
return
103+
}
104+
105+
const entries = yield* _(fs.readDirectory(sourceRoot))
106+
for (const entry of entries) {
107+
const sourceEntry = path.join(sourceRoot, entry)
108+
const entryInfo = yield* _(statIfPresent(fs, sourceEntry))
109+
if (entryInfo === null || entryInfo.type !== "Directory") {
110+
continue
111+
}
112+
113+
yield* _(copyCodexAuthFileIfPresent(fs, path, sourceEntry, path.join(targetRoot, entry), fileName))
114+
}
115+
})
116+
74117
type BootstrapSeedConfig = Pick<
75118
TemplateConfig,
76119
"authorizedKeysPath" | "envGlobalPath" | "envProjectPath" | "codexAuthPath" | "codexSharedAuthPath"
@@ -162,12 +205,16 @@ const copyBootstrapSnapshotAuthDirs = (
162205
targets: BootstrapSnapshotTargets
163206
): Effect.Effect<void, PlatformError, FileSystem.FileSystem | Path.Path> =>
164207
Effect.gen(function*(_) {
165-
yield* _(copyDirRecursive(fs, path, sources.codexAuthSource, targets.projectCodexTarget))
208+
yield* _(copyCodexAuthFileIfPresent(fs, path, sources.codexAuthSource, targets.projectCodexTarget, "auth.json"))
209+
yield* _(copyCodexAuthFileIfPresent(fs, path, sources.codexAuthSource, targets.projectCodexTarget, "config.toml"))
210+
yield* _(copyLabeledCodexFiles(fs, path, sources.codexAuthSource, targets.projectCodexTarget, "auth.json"))
211+
yield* _(copyLabeledCodexFiles(fs, path, sources.codexAuthSource, targets.projectCodexTarget, "config.toml"))
166212
yield* _(copyDirRecursive(fs, path, sources.claudeAuthSource, targets.projectClaudeTarget))
167-
yield* _(copyDirRecursive(fs, path, sources.codexSharedAuthSource, targets.sharedCodexTarget))
213+
yield* _(copyCodexAuthFileIfPresent(fs, path, sources.codexSharedAuthSource, targets.sharedCodexTarget, "auth.json"))
214+
yield* _(copyLabeledCodexFiles(fs, path, sources.codexSharedAuthSource, targets.sharedCodexTarget, "auth.json"))
168215
})
169216

170-
const stageBootstrapSnapshot = (
217+
export const stageBootstrapSnapshot = (
171218
stagingDir: string,
172219
projectDir: string,
173220
config: BootstrapSeedConfig
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import * as fs from "node:fs"
2+
3+
import * as FileSystem from "@effect/platform/FileSystem"
4+
import * as Path from "@effect/platform/Path"
5+
import { NodeContext } from "@effect/platform-node"
6+
import { describe, expect, it } from "@effect/vitest"
7+
import { Effect } from "effect"
8+
9+
import { stageBootstrapSnapshot } from "../../src/usecases/shared-volume-seed.js"
10+
11+
const withTempDir = <A, E, R>(
12+
use: (tempDir: string) => Effect.Effect<A, E, R>
13+
): Effect.Effect<A, E, R | FileSystem.FileSystem> =>
14+
Effect.scoped(
15+
Effect.gen(function*(_) {
16+
const fileSystem = yield* _(FileSystem.FileSystem)
17+
const tempDir = yield* _(
18+
fileSystem.makeTempDirectoryScoped({
19+
prefix: "docker-git-shared-volume-seed-"
20+
})
21+
)
22+
return yield* _(use(tempDir))
23+
})
24+
)
25+
26+
describe("stageBootstrapSnapshot", () => {
27+
it.effect("copies stable Codex auth files and skips transient broken tmp entries", () =>
28+
withTempDir((root) =>
29+
Effect.gen(function*(_) {
30+
const fileSystem = yield* _(FileSystem.FileSystem)
31+
const path = yield* _(Path.Path)
32+
33+
const projectDir = path.join(root, "project")
34+
const stagingDir = path.join(root, "staging")
35+
const projectCodexDir = path.join(projectDir, ".orch", "auth", "codex")
36+
const projectCodexLabelDir = path.join(projectCodexDir, "team-a")
37+
const projectClaudeDir = path.join(projectDir, ".orch", "auth", "claude", "default")
38+
const sharedCodexDir = path.join(root, ".docker-git", ".orch", "auth", "codex")
39+
const sharedCodexLabelDir = path.join(sharedCodexDir, "team-a")
40+
const envDir = path.join(projectDir, ".orch", "env")
41+
42+
yield* _(fileSystem.makeDirectory(projectCodexDir, { recursive: true }))
43+
yield* _(fileSystem.makeDirectory(projectCodexLabelDir, { recursive: true }))
44+
yield* _(fileSystem.makeDirectory(projectClaudeDir, { recursive: true }))
45+
yield* _(fileSystem.makeDirectory(sharedCodexDir, { recursive: true }))
46+
yield* _(fileSystem.makeDirectory(sharedCodexLabelDir, { recursive: true }))
47+
yield* _(fileSystem.makeDirectory(envDir, { recursive: true }))
48+
yield* _(fileSystem.writeFileString(path.join(projectDir, "authorized_keys"), "ssh-ed25519 test\n"))
49+
yield* _(fileSystem.writeFileString(path.join(envDir, "global.env"), "GITHUB_TOKEN=test\n"))
50+
yield* _(fileSystem.writeFileString(path.join(envDir, "project.env"), "CODEX_SHARE_AUTH=1\n"))
51+
yield* _(fileSystem.writeFileString(path.join(projectCodexDir, "auth.json"), "{\"project\":true}\n"))
52+
yield* _(fileSystem.writeFileString(path.join(projectCodexDir, "config.toml"), "model = \"gpt-5.4\"\n"))
53+
yield* _(fileSystem.writeFileString(path.join(projectCodexLabelDir, "auth.json"), "{\"label\":\"team-a\"}\n"))
54+
yield* _(fileSystem.writeFileString(path.join(projectCodexLabelDir, "config.toml"), "model = \"gpt-5.4\"\n"))
55+
yield* _(fileSystem.writeFileString(path.join(projectClaudeDir, ".oauth-token"), "claude-token\n"))
56+
yield* _(fileSystem.writeFileString(path.join(sharedCodexDir, "auth.json"), "{\"shared\":true}\n"))
57+
yield* _(fileSystem.writeFileString(path.join(sharedCodexLabelDir, "auth.json"), "{\"shared\":\"team-a\"}\n"))
58+
59+
yield* _(
60+
Effect.sync(() => {
61+
const brokenShimDir = path.join(sharedCodexDir, "tmp", "arg0", "codex-arg0broken")
62+
fs.mkdirSync(brokenShimDir, { recursive: true })
63+
fs.symlinkSync(
64+
"/usr/local/bun/install/global/node_modules/@openai/codex-linux-x64/vendor/x86_64-unknown-linux-musl/codex/codex",
65+
path.join(brokenShimDir, "apply_patch")
66+
)
67+
fs.writeFileSync(path.join(brokenShimDir, ".lock"), "")
68+
fs.mkdirSync(path.join(sharedCodexDir, "log"), { recursive: true })
69+
fs.writeFileSync(path.join(sharedCodexDir, "log", "codex-login.log"), "transient log\n")
70+
fs.mkdirSync(path.join(sharedCodexDir, ".image"), { recursive: true })
71+
fs.writeFileSync(path.join(sharedCodexDir, ".image", "Dockerfile"), "FROM scratch\n")
72+
})
73+
)
74+
75+
yield* _(
76+
stageBootstrapSnapshot(stagingDir, projectDir, {
77+
volumeName: "dg-test-home",
78+
authorizedKeysPath: path.join(projectDir, "authorized_keys"),
79+
envGlobalPath: path.join(projectDir, ".orch", "env", "global.env"),
80+
envProjectPath: path.join(projectDir, ".orch", "env", "project.env"),
81+
codexAuthPath: path.join(projectDir, ".orch", "auth", "codex"),
82+
codexSharedAuthPath: sharedCodexDir
83+
})
84+
)
85+
86+
expect(yield* _(fileSystem.readFileString(path.join(stagingDir, "project-auth", "codex", "auth.json")))).toBe(
87+
"{\"project\":true}\n"
88+
)
89+
expect(
90+
yield* _(fileSystem.readFileString(path.join(stagingDir, "project-auth", "codex", "config.toml")))
91+
).toBe("model = \"gpt-5.4\"\n")
92+
expect(
93+
yield* _(fileSystem.readFileString(path.join(stagingDir, "project-auth", "codex", "team-a", "auth.json")))
94+
).toBe("{\"label\":\"team-a\"}\n")
95+
expect(
96+
yield* _(fileSystem.readFileString(path.join(stagingDir, "project-auth", "codex", "team-a", "config.toml")))
97+
).toBe("model = \"gpt-5.4\"\n")
98+
expect(yield* _(fileSystem.readFileString(path.join(stagingDir, "shared-auth", "codex", "auth.json")))).toBe(
99+
"{\"shared\":true}\n"
100+
)
101+
expect(
102+
yield* _(fileSystem.readFileString(path.join(stagingDir, "shared-auth", "codex", "team-a", "auth.json")))
103+
).toBe("{\"shared\":\"team-a\"}\n")
104+
expect(
105+
yield* _(fileSystem.readFileString(path.join(stagingDir, "project-auth", "claude", "default", ".oauth-token")))
106+
).toBe("claude-token\n")
107+
108+
expect(yield* _(fileSystem.exists(path.join(stagingDir, "shared-auth", "codex", "tmp")))).toBe(false)
109+
expect(yield* _(fileSystem.exists(path.join(stagingDir, "shared-auth", "codex", "log")))).toBe(false)
110+
expect(yield* _(fileSystem.exists(path.join(stagingDir, "shared-auth", "codex", ".image")))).toBe(false)
111+
expect(yield* _(fileSystem.exists(path.join(stagingDir, "project-auth", "codex", "tmp")))).toBe(false)
112+
})
113+
).pipe(Effect.provide(NodeContext.layer)))
114+
})

scripts/e2e/opencode-autoconnect.sh

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -119,14 +119,9 @@ AUTH_LOG="$ROOT/codex-auth.log"
119119
pnpm run docker-git auth codex status --codex-auth "$ROOT/.orch/auth/codex"
120120
) >"$AUTH_LOG" 2>&1
121121

122-
grep -Fq -- "Codex auth imported into controller state." "$AUTH_LOG" \
123-
|| fail "expected controller-owned Codex auth import confirmation"
124-
125-
grep -Fq -- '"present": true' "$AUTH_LOG" \
126-
|| fail "expected controller-owned Codex auth status confirmation"
127-
128-
grep -Fq -- '"authPath":' "$AUTH_LOG" \
129-
|| fail "expected controller-owned Codex auth status payload"
122+
auth_confirmation_count="$(grep -Fc -- "Codex auth imported into controller state (account: ci@example.com)." "$AUTH_LOG" || true)"
123+
[[ "$auth_confirmation_count" -ge 2 ]] \
124+
|| fail "expected controller-owned Codex auth import + status confirmations"
130125

131126
clone_attempts=3
132127
clone_attempt=1

0 commit comments

Comments
 (0)