Skip to content

Commit 3e64dcb

Browse files
committed
fix(shell): keep bootstrap auth and keys out of image layers
1 parent eb0c364 commit 3e64dcb

File tree

10 files changed

+168
-82
lines changed

10 files changed

+168
-82
lines changed

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,13 @@ if [[ "$CODEX_SHARE_AUTH" == "1" ]]; then
3030
if [[ "$CODEX_LABEL_NORM" != "default" ]]; then
3131
SHARED_AUTH_FILE="$CODEX_SHARED_HOME/$CODEX_LABEL_NORM/auth.json"; SHARED_AUTH_SEED="$DOCKER_GIT_CODEX_AUTH_ROOT/$CODEX_LABEL_NORM/auth.json"; mkdir -p "$(dirname "$SHARED_AUTH_FILE")"
3232
fi
33-
if [[ ! -f "$SHARED_AUTH_FILE" && -f "$SHARED_AUTH_SEED" ]]; then cp "$SHARED_AUTH_SEED" "$SHARED_AUTH_FILE"; chmod 600 "$SHARED_AUTH_FILE" || true; chown 1000:1000 "$SHARED_AUTH_FILE" || true; fi
33+
if [[ -f "$SHARED_AUTH_SEED" ]]; then
34+
cp "$SHARED_AUTH_SEED" "$SHARED_AUTH_FILE"
35+
chmod 600 "$SHARED_AUTH_FILE" || true
36+
chown 1000:1000 "$SHARED_AUTH_FILE" || true
37+
else
38+
rm -f "$SHARED_AUTH_FILE" || true
39+
fi
3440
# Guard against a bad bind mount creating a directory at auth.json.
3541
if [[ -d "$AUTH_FILE" ]]; then
3642
mv "$AUTH_FILE" "$AUTH_FILE.bak-$(date +%s)" || true

packages/lib/src/core/templates-entrypoint/nested-docker-git.ts

Lines changed: 60 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,27 +10,39 @@ DOCKER_GIT_ENV_GLOBAL="$DOCKER_GIT_ENV_DIR/global.env"
1010
DOCKER_GIT_ENV_PROJECT="$DOCKER_GIT_ENV_DIR/project.env"
1111
DOCKER_GIT_AUTH_KEYS="$DOCKER_GIT_HOME/authorized_keys"
1212
BOOTSTRAP_ROOT="/opt/docker-git/bootstrap"
13-
BOOTSTRAP_ORCH_ROOT="$BOOTSTRAP_ROOT/.orch"
14-
BOOTSTRAP_AUTH_KEYS="$BOOTSTRAP_ROOT/authorized_keys"
15-
BOOTSTRAP_CODEX_AUTH_DIR="$BOOTSTRAP_ORCH_ROOT/auth/codex"
16-
BOOTSTRAP_CLAUDE_AUTH_DIR="$BOOTSTRAP_ORCH_ROOT/auth/claude"
17-
BOOTSTRAP_ENV_GLOBAL="$BOOTSTRAP_ORCH_ROOT/env/global.env"
18-
BOOTSTRAP_ENV_PROJECT="$BOOTSTRAP_ORCH_ROOT/env/project.env"
13+
BOOTSTRAP_SOURCE_ROOT="$BOOTSTRAP_ROOT/source"
14+
BOOTSTRAP_AUTH_KEYS="$BOOTSTRAP_SOURCE_ROOT/authorized-keys/__AUTHORIZED_KEYS_BASENAME__"
15+
BOOTSTRAP_CODEX_AUTH_DIR="$BOOTSTRAP_SOURCE_ROOT/project-auth/codex"
16+
BOOTSTRAP_CODEX_SHARED_AUTH_DIR="$BOOTSTRAP_SOURCE_ROOT/shared-auth/codex"
17+
BOOTSTRAP_CLAUDE_AUTH_DIR="$BOOTSTRAP_SOURCE_ROOT/project-auth/claude"
18+
BOOTSTRAP_ENV_GLOBAL="$BOOTSTRAP_SOURCE_ROOT/env-global/__ENV_GLOBAL_BASENAME__"
19+
BOOTSTRAP_ENV_PROJECT="$BOOTSTRAP_SOURCE_ROOT/env-project/__ENV_PROJECT_BASENAME__"
1920
2021
mkdir -p "$DOCKER_GIT_AUTH_DIR" "$DOCKER_GIT_CLAUDE_AUTH_DIR" "$DOCKER_GIT_ENV_DIR" "$DOCKER_GIT_HOME/.orch/auth/gh"
2122
22-
copy_if_missing_file() {
23+
sync_file_if_present() {
2324
local source="$1"
2425
local target="$2"
25-
if [[ ! -f "$source" || -e "$target" ]]; then
26+
if [[ ! -f "$source" ]]; then
2627
return 1
2728
fi
2829
mkdir -p "$(dirname "$target")"
2930
cp "$source" "$target"
3031
return 0
3132
}
3233
33-
copy_dir_missing_entries() {
34+
sync_file_or_remove() {
35+
local source="$1"
36+
local target="$2"
37+
if [[ -f "$source" ]]; then
38+
sync_file_if_present "$source" "$target"
39+
return 0
40+
fi
41+
rm -f "$target" || true
42+
return 1
43+
}
44+
45+
sync_dir_entries() {
3446
local source="$1"
3547
local target="$2"
3648
if [[ ! -d "$source" ]]; then
@@ -45,29 +57,56 @@ copy_dir_missing_entries() {
4557
local target_entry="$target/$entry"
4658
if [[ -d "$source_entry" ]]; then
4759
mkdir -p "$target_entry"
48-
elif [[ -f "$source_entry" && ! -e "$target_entry" ]]; then
60+
elif [[ -f "$source_entry" ]]; then
4961
mkdir -p "$(dirname "$target_entry")"
5062
cp "$source_entry" "$target_entry"
5163
fi
5264
done
5365
}
5466
67+
sync_labeled_auth_files() {
68+
local source_root="$1"
69+
local target_root="$2"
70+
71+
sync_file_or_remove "$source_root/auth.json" "$target_root/auth.json" || true
72+
73+
if [[ -d "$source_root" ]]; then
74+
(
75+
cd "$source_root"
76+
find . -mindepth 1 -maxdepth 1 -type d -print
77+
) | while IFS= read -r entry; do
78+
sync_file_or_remove "$source_root/$entry/auth.json" "$target_root/$entry/auth.json" || true
79+
done
80+
fi
81+
82+
if [[ -d "$target_root" ]]; then
83+
(
84+
cd "$target_root"
85+
find . -mindepth 1 -maxdepth 1 -type d -print
86+
) | while IFS= read -r entry; do
87+
if [[ ! -d "$source_root/$entry" ]]; then
88+
rm -f "$target_root/$entry/auth.json" || true
89+
fi
90+
done
91+
fi
92+
}
93+
5594
if [[ ! -f "$DOCKER_GIT_AUTH_KEYS" && -f "/home/__SSH_USER__/.ssh/authorized_keys" ]]; then
5695
cp "/home/__SSH_USER__/.ssh/authorized_keys" "$DOCKER_GIT_AUTH_KEYS"
5796
fi
58-
copy_if_missing_file "$BOOTSTRAP_AUTH_KEYS" "$DOCKER_GIT_AUTH_KEYS" || true
97+
sync_file_if_present "$BOOTSTRAP_AUTH_KEYS" "$DOCKER_GIT_AUTH_KEYS" || true
5998
if [[ -f "$DOCKER_GIT_AUTH_KEYS" ]]; then
6099
chmod 600 "$DOCKER_GIT_AUTH_KEYS" || true
61100
fi
62101
63-
copy_if_missing_file "$BOOTSTRAP_ENV_GLOBAL" "$DOCKER_GIT_ENV_GLOBAL" || true
102+
sync_file_if_present "$BOOTSTRAP_ENV_GLOBAL" "$DOCKER_GIT_ENV_GLOBAL" || true
64103
if [[ ! -f "$DOCKER_GIT_ENV_GLOBAL" ]]; then
65104
cat <<'EOF' > "$DOCKER_GIT_ENV_GLOBAL"
66105
# docker-git env
67106
# KEY=value
68107
EOF
69108
fi
70-
copy_if_missing_file "$BOOTSTRAP_ENV_PROJECT" "$DOCKER_GIT_ENV_PROJECT" || true
109+
sync_file_if_present "$BOOTSTRAP_ENV_PROJECT" "$DOCKER_GIT_ENV_PROJECT" || true
71110
if [[ ! -f "$DOCKER_GIT_ENV_PROJECT" ]]; then
72111
cat <<'EOF' > "$DOCKER_GIT_ENV_PROJECT"
73112
# docker-git project env defaults
@@ -108,8 +147,9 @@ copy_if_distinct_file() {
108147
return 0
109148
}
110149
111-
copy_dir_missing_entries "$BOOTSTRAP_CODEX_AUTH_DIR" "$DOCKER_GIT_AUTH_DIR"
112-
copy_dir_missing_entries "$BOOTSTRAP_CLAUDE_AUTH_DIR" "$DOCKER_GIT_CLAUDE_AUTH_DIR"
150+
sync_dir_entries "$BOOTSTRAP_CODEX_AUTH_DIR" "$DOCKER_GIT_AUTH_DIR"
151+
sync_labeled_auth_files "$BOOTSTRAP_CODEX_SHARED_AUTH_DIR" "$DOCKER_GIT_AUTH_DIR"
152+
sync_dir_entries "$BOOTSTRAP_CLAUDE_AUTH_DIR" "$DOCKER_GIT_CLAUDE_AUTH_DIR"
113153
114154
if [[ -n "$GH_TOKEN" ]]; then
115155
upsert_env_var "$DOCKER_GIT_ENV_GLOBAL" "GH_TOKEN" "$GH_TOKEN"
@@ -129,6 +169,8 @@ if [[ -f "$SOURCE_SHARED_AUTH" ]]; then
129169
copy_if_distinct_file "$SOURCE_SHARED_AUTH" "$DOCKER_GIT_AUTH_DIR/auth.json" || true
130170
elif [[ -f "$SOURCE_LOCAL_AUTH" ]]; then
131171
copy_if_distinct_file "$SOURCE_LOCAL_AUTH" "$DOCKER_GIT_AUTH_DIR/auth.json" || true
172+
else
173+
rm -f "$DOCKER_GIT_AUTH_DIR/auth.json" || true
132174
fi
133175
if [[ -f "$DOCKER_GIT_AUTH_DIR/auth.json" ]]; then
134176
chmod 600 "$DOCKER_GIT_AUTH_DIR/auth.json" || true
@@ -139,4 +181,7 @@ chown -R 1000:1000 "$DOCKER_GIT_HOME" || true`
139181
export const renderEntrypointDockerGitBootstrap = (config: TemplateConfig): string =>
140182
entrypointDockerGitBootstrapTemplate
141183
.replaceAll("__SSH_USER__", config.sshUser)
184+
.replaceAll("__AUTHORIZED_KEYS_BASENAME__", config.authorizedKeysPath.replaceAll("\\", "/").split("/").at(-1) ?? "authorized_keys")
185+
.replaceAll("__ENV_GLOBAL_BASENAME__", config.envGlobalPath.replaceAll("\\", "/").split("/").at(-1) ?? "global.env")
186+
.replaceAll("__ENV_PROJECT_BASENAME__", config.envProjectPath.replaceAll("\\", "/").split("/").at(-1) ?? "project.env")
142187
.replaceAll("__CODEX_HOME__", config.codexHome)

packages/lib/src/core/templates.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ authorized_keys
2525

2626
const renderDockerignore = (): string =>
2727
`# docker-git build context
28+
authorized_keys
29+
.orch/env/
30+
.orch/auth/codex/
31+
.orch/auth/claude/
2832
.orch/auth/codex/log/
2933
.orch/auth/codex/tmp/
3034
.orch/auth/codex/sessions/

packages/lib/src/core/templates/docker-compose.ts

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,15 @@ type ComposeFragments = {
1717
readonly maybeDependsOn: string
1818
readonly maybePlaywrightEnv: string
1919
readonly maybeBrowserService: string
20+
readonly maybeBrowserVolume: string
21+
readonly maybeBootstrapMounts: string
2022
readonly forkRepoUrl: string
2123
}
2224

23-
type PlaywrightFragments = Pick<ComposeFragments, "maybeDependsOn" | "maybePlaywrightEnv" | "maybeBrowserService">
25+
type PlaywrightFragments = Pick<
26+
ComposeFragments,
27+
"maybeDependsOn" | "maybePlaywrightEnv" | "maybeBrowserService" | "maybeBrowserVolume"
28+
>
2429

2530
const sharedCodexVolumeKey = "docker_git_shared_codex"
2631
const sharedCacheVolumeKey = "docker_git_shared_cache"
@@ -54,6 +59,50 @@ const renderResourceLimits = (resourceLimits: ResolvedComposeResourceLimits | un
5459
resourceLimits === undefined
5560
? ""
5661
: ` cpus: ${resourceLimits.cpuLimit}\n mem_limit: "${resourceLimits.ramLimit}"\n memswap_limit: "${resourceLimits.ramLimit}"\n`
62+
63+
const renderProjectHostPath = (value: string): string => {
64+
if (value.startsWith("/")) {
65+
return value
66+
}
67+
68+
const normalized = value.startsWith("./") ? value.slice(2) : value
69+
return `\${DOCKER_GIT_PROJECT_DIR_HOST:-.}/${normalized}`
70+
}
71+
72+
const splitPath = (value: string): { readonly dir: string; readonly base: string } => {
73+
const normalized = value.replaceAll("\\", "/")
74+
const separatorIndex = normalized.lastIndexOf("/")
75+
if (separatorIndex === -1) {
76+
return { dir: ".", base: normalized }
77+
}
78+
return {
79+
dir: separatorIndex === 0 ? "/" : normalized.slice(0, separatorIndex),
80+
base: normalized.slice(separatorIndex + 1)
81+
}
82+
}
83+
84+
const renderClaudeBootstrapSourceDir = (codexAuthPath: string): string => {
85+
const normalized = codexAuthPath.replaceAll("\\", "/")
86+
const separatorIndex = normalized.lastIndexOf("/")
87+
const authRoot = separatorIndex === -1 ? ".orch/auth" : normalized.slice(0, separatorIndex)
88+
return `${authRoot}/claude`
89+
}
90+
91+
const renderBootstrapMounts = (config: TemplateConfig): string => {
92+
const authorizedKeys = splitPath(config.authorizedKeysPath)
93+
const envGlobal = splitPath(config.envGlobalPath)
94+
const envProject = splitPath(config.envProjectPath)
95+
96+
return [
97+
` - ${renderProjectHostPath(authorizedKeys.dir)}:/opt/docker-git/bootstrap/source/authorized-keys:ro`,
98+
` - ${renderProjectHostPath(envGlobal.dir)}:/opt/docker-git/bootstrap/source/env-global:ro`,
99+
` - ${renderProjectHostPath(envProject.dir)}:/opt/docker-git/bootstrap/source/env-project:ro`,
100+
` - ${renderProjectHostPath(config.codexAuthPath)}:/opt/docker-git/bootstrap/source/project-auth/codex:ro`,
101+
` - ${renderProjectHostPath(renderClaudeBootstrapSourceDir(config.codexAuthPath))}:/opt/docker-git/bootstrap/source/project-auth/claude:ro`,
102+
` - ${renderProjectHostPath(config.codexSharedAuthPath)}:/opt/docker-git/bootstrap/source/shared-auth/codex:ro`
103+
].join("\n")
104+
}
105+
57106
const buildPlaywrightFragments = (
58107
config: TemplateConfig,
59108
networkName: string,
@@ -63,7 +112,8 @@ const buildPlaywrightFragments = (
63112
return {
64113
maybeDependsOn: "",
65114
maybePlaywrightEnv: "",
66-
maybeBrowserService: ""
115+
maybeBrowserService: "",
116+
maybeBrowserVolume: ""
67117
}
68118
}
69119

@@ -80,7 +130,8 @@ const buildPlaywrightFragments = (
80130
maybeBrowserService:
81131
`\n ${browserServiceName}:\n build:\n context: .\n dockerfile: ${browserDockerfile}\n container_name: ${browserContainerName}\n restart: unless-stopped\n${
82132
renderResourceLimits(resourceLimits)
83-
} environment:\n VNC_NOPW: "1"\n shm_size: "2gb"\n expose:\n - "9223"\n volumes:\n - ${browserVolumeName}:/data\n networks:\n - ${networkName}\n`
133+
} environment:\n VNC_NOPW: "1"\n shm_size: "2gb"\n expose:\n - "9223"\n volumes:\n - ${browserVolumeName}:/data\n networks:\n - ${networkName}\n`,
134+
maybeBrowserVolume: ` ${browserVolumeName}:`
84135
}
85136
}
86137

@@ -112,6 +163,8 @@ const buildComposeFragments = (
112163
maybeDependsOn: playwright.maybeDependsOn,
113164
maybePlaywrightEnv: playwright.maybePlaywrightEnv,
114165
maybeBrowserService: playwright.maybeBrowserService,
166+
maybeBrowserVolume: playwright.maybeBrowserVolume,
167+
maybeBootstrapMounts: renderBootstrapMounts(config),
115168
forkRepoUrl
116169
}
117170
}
@@ -144,6 +197,7 @@ ${renderResourceLimits(resourceLimits)} volumes:
144197
- ${config.volumeName}:/home/${config.sshUser}
145198
- ${sharedCacheVolumeKey}:/home/${config.sshUser}/.docker-git/.cache
146199
- ${sharedCodexVolumeKey}:${config.codexHome}-shared
200+
${fragments.maybeBootstrapMounts}
147201
- /var/run/docker.sock:/var/run/docker.sock
148202
networks:
149203
- ${fragments.networkName}
@@ -161,7 +215,7 @@ const renderComposeNetworks = (
161215
${networkName}:
162216
driver: bridge`
163217

164-
const renderComposeVolumes = (config: TemplateConfig, enableMcpPlaywright: boolean): string =>
218+
const renderComposeVolumes = (config: TemplateConfig, maybeBrowserVolume: string): string =>
165219
[
166220
"volumes:",
167221
` ${config.volumeName}:`,
@@ -171,8 +225,8 @@ const renderComposeVolumes = (config: TemplateConfig, enableMcpPlaywright: boole
171225
` ${sharedCodexVolumeKey}:`,
172226
" external: true",
173227
` name: ${dockerGitSharedCodexVolumeName}`,
174-
...(enableMcpPlaywright ? [` ${config.volumeName}-browser:`] : [])
175-
].join("\n")
228+
maybeBrowserVolume
229+
].filter((entry) => entry.length > 0).join("\n")
176230

177231
export const renderDockerCompose = (
178232
config: TemplateConfig,
@@ -182,6 +236,6 @@ export const renderDockerCompose = (
182236
return [
183237
renderComposeServices(config, fragments, resourceLimits),
184238
renderComposeNetworks(fragments.networkMode, fragments.networkName),
185-
renderComposeVolumes(config, config.enableMcpPlaywright)
239+
renderComposeVolumes(config, fragments.maybeBrowserVolume)
186240
].join("\n\n")
187241
}

packages/lib/src/core/templates/dockerfile.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -222,9 +222,13 @@ RUN mkdir -p ${config.targetDir} \
222222
&& chown -R 1000:1000 /home/${config.sshUser} \
223223
&& if [ "${config.targetDir}" != "/" ]; then chown -R 1000:1000 "${config.targetDir}"; fi
224224
225-
RUN mkdir -p /opt/docker-git/bootstrap
226-
COPY authorized_keys /opt/docker-git/bootstrap/authorized_keys
227-
COPY .orch /opt/docker-git/bootstrap/.orch
225+
RUN mkdir -p /opt/docker-git/bootstrap/.orch/auth/codex \
226+
/opt/docker-git/bootstrap/.orch/auth/codex-shared \
227+
/opt/docker-git/bootstrap/.orch/auth/claude \
228+
/opt/docker-git/bootstrap/.orch/env \
229+
&& touch /opt/docker-git/bootstrap/authorized_keys \
230+
/opt/docker-git/bootstrap/.orch/env/global.env \
231+
/opt/docker-git/bootstrap/.orch/env/project.env
228232
229233
COPY entrypoint.sh /entrypoint.sh
230234
RUN sed -i 's/\r$//' /entrypoint.sh && chmod +x /entrypoint.sh

packages/lib/src/shell/docker-compose-env.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,18 @@ export const resolveDockerComposeEnv = (
2424
): Effect.Effect<Readonly<Record<string, string>>, never, CommandExecutor.CommandExecutor> =>
2525
Effect.gen(function*(_) {
2626
const projectsRoot = resolveProjectsRootCandidate()
27+
const remappedProjectDir = yield* _(resolveDockerVolumeHostPath(cwd, cwd))
2728
if (projectsRoot === null) {
28-
return {}
29+
return remappedProjectDir === cwd ? {} : { DOCKER_GIT_PROJECT_DIR_HOST: remappedProjectDir }
2930
}
3031

3132
const remappedProjectsRoot = yield* _(resolveDockerVolumeHostPath(cwd, projectsRoot))
32-
return remappedProjectsRoot === projectsRoot ? {} : { DOCKER_GIT_PROJECTS_ROOT_HOST: remappedProjectsRoot }
33+
const env: Record<string, string> = {}
34+
if (remappedProjectsRoot !== projectsRoot) {
35+
env["DOCKER_GIT_PROJECTS_ROOT_HOST"] = remappedProjectsRoot
36+
}
37+
if (remappedProjectDir !== cwd) {
38+
env["DOCKER_GIT_PROJECT_DIR_HOST"] = remappedProjectDir
39+
}
40+
return env
3341
})

packages/lib/src/shell/docker-volume.ts

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -13,36 +13,3 @@ export const runDockerVolumeCreate = (
1313
runCommandWithExitCodes({ cwd, command: "docker", args: ["volume", "create", volumeName] }, [Number(ExitCode(0))], (
1414
exitCode
1515
) => new DockerCommandError({ exitCode }))
16-
17-
const seedDockerVolumeScript = String.raw`set -eu
18-
mkdir -p /dest
19-
if [[ -d /src ]]; then
20-
cp -an /src/. /dest/ 2>/dev/null || true
21-
find /dest -type f -name auth.json -exec chmod 600 {} + >/dev/null 2>&1 || true
22-
fi`
23-
24-
export const runDockerVolumeSeedFromDir = (
25-
cwd: string,
26-
volumeName: string,
27-
sourceDir: string
28-
): Effect.Effect<void, DockerCommandError | PlatformError, CommandExecutor.CommandExecutor> =>
29-
runCommandWithExitCodes(
30-
{
31-
cwd,
32-
command: "docker",
33-
args: [
34-
"run",
35-
"--rm",
36-
"-v",
37-
`${volumeName}:/dest`,
38-
"-v",
39-
`${sourceDir}:/src:ro`,
40-
"ubuntu:24.04",
41-
"bash",
42-
"-lc",
43-
seedDockerVolumeScript
44-
]
45-
},
46-
[Number(ExitCode(0))],
47-
(exitCode) => new DockerCommandError({ exitCode })
48-
)

0 commit comments

Comments
 (0)