Skip to content
Merged
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
8 changes: 8 additions & 0 deletions packages/app/tests/docker-git/entrypoint-auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,14 @@ describe("renderEntrypoint auth bridge", () => {
expect(entrypoint).toContain(
"docker_git_link_claude_file \"$CLAUDE_CONFIG_DIR/.claude.json\" \"$CLAUDE_HOME_JSON\""
)
expect(entrypoint).toContain("su - dev -s /bin/bash -c \"bash -lc")
expect(entrypoint).toContain(". /etc/profile 2>/dev/null || true;")
expect(entrypoint).toContain(String.raw`. \"$AGENT_ENV_FILE\" 2>/dev/null || true;`)
expect(entrypoint).toContain(
String.raw`claude --dangerously-skip-permissions -p \"\$(cat \"$AGENT_PROMPT_FILE\")\"`
)
expect(entrypoint).toContain(String.raw`codex exec \"\$(cat \"$AGENT_PROMPT_FILE\")\"`)
expect(entrypoint).not.toContain("codex --approval-mode full-auto")
expect(entrypoint).toContain("CLAUDE_GLOBAL_PROMPT_FILE=\"/home/dev/.claude/CLAUDE.md\"")
expect(entrypoint).toContain("CLAUDE_AUTO_SYSTEM_PROMPT=\"${CLAUDE_AUTO_SYSTEM_PROMPT:-1}\"")
expect(entrypoint).toContain("docker-git-managed:claude-md")
Expand Down
16 changes: 12 additions & 4 deletions packages/lib/src/core/templates-entrypoint/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,17 @@ fi`

const renderAgentPromptCommand = (mode: "claude" | "codex"): string =>
mode === "claude"
? String.raw`claude --dangerously-skip-permissions -p \"\$(cat $AGENT_PROMPT_FILE)\"`
: String.raw`codex --approval-mode full-auto \"\$(cat $AGENT_PROMPT_FILE)\"`
? String.raw`claude --dangerously-skip-permissions -p \"\$(cat \"$AGENT_PROMPT_FILE\")\"`
: String.raw`codex exec \"\$(cat \"$AGENT_PROMPT_FILE\")\"`

const renderAgentAutoLaunchCommand = (
config: TemplateConfig,
mode: "claude" | "codex"
): string =>
String
.raw`su - ${config.sshUser} -s /bin/bash -c "bash -lc '. /etc/profile 2>/dev/null || true; . \"$AGENT_ENV_FILE\" 2>/dev/null || true; cd \"$TARGET_DIR\" && ${
renderAgentPromptCommand(mode)
}'"`

const renderAgentModeBlock = (
config: TemplateConfig,
Expand All @@ -60,8 +69,7 @@ const renderAgentModeBlock = (
return String.raw`"${mode}")
echo "${startMessage}"
if [[ -n "$AGENT_PROMPT" ]]; then
if su - ${config.sshUser} \
-c ". /run/docker-git/agent-env.sh 2>/dev/null; cd '$TARGET_DIR' && ${renderAgentPromptCommand(mode)}"; then
if ${renderAgentAutoLaunchCommand(config, mode)}; then
AGENT_OK=1
fi
else
Expand Down
10 changes: 8 additions & 2 deletions packages/lib/src/core/templates/docker-compose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ const renderAgentAutoEnv = (agentAuto: boolean | undefined): string =>
? ` AGENT_AUTO: "1"\n`
: ""

const renderProjectsRootHostMount = (projectsRoot: string): string =>
`\${DOCKER_GIT_PROJECTS_ROOT_HOST:-${projectsRoot}}`

const renderSharedCodexHostMount = (projectsRoot: string): string =>
`\${DOCKER_GIT_PROJECTS_ROOT_HOST:-${projectsRoot}}/.orch/auth/codex`

const buildPlaywrightFragments = (
config: TemplateConfig,
networkName: string
Expand Down Expand Up @@ -126,10 +132,10 @@ ${fragments.maybePlaywrightEnv}${fragments.maybeDependsOn} env_file:
- "127.0.0.1:${config.sshPort}:22"
volumes:
- ${config.volumeName}:/home/${config.sshUser}
- ${config.dockerGitPath}:/home/${config.sshUser}/.docker-git
- ${renderProjectsRootHostMount(config.dockerGitPath)}:/home/${config.sshUser}/.docker-git
- ${config.authorizedKeysPath}:/authorized_keys:ro
- ${config.codexAuthPath}:${config.codexHome}
- ${config.codexSharedAuthPath}:${config.codexHome}-shared
- ${renderSharedCodexHostMount(config.dockerGitPath)}:${config.codexHome}-shared
- /var/run/docker.sock:/var/run/docker.sock
networks:
- ${fragments.networkName}
Expand Down
14 changes: 7 additions & 7 deletions packages/lib/src/shell/docker-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ type DockerMountBinding = {
readonly destination: string
}

const resolveEnvValue = (key: string): string | null => {
export const resolveDockerEnvValue = (key: string): string | null => {
const value = process.env[key]?.trim()
return value && value.length > 0 ? value : null
}

const trimTrailingSlash = (value: string): string => {
export const trimDockerPathTrailingSlash = (value: string): string => {
let end = value.length
while (end > 0) {
const char = value[end - 1]
Expand All @@ -51,21 +51,21 @@ const translatePathPrefix = (candidate: string, sourcePrefix: string, targetPref
: null

const resolveContainerProjectsRoot = (): string | null => {
const explicit = resolveEnvValue("DOCKER_GIT_PROJECTS_ROOT")
const explicit = resolveDockerEnvValue("DOCKER_GIT_PROJECTS_ROOT")
if (explicit !== null) {
return explicit
}

const home = resolveEnvValue("HOME") ?? resolveEnvValue("USERPROFILE")
return home === null ? null : `${trimTrailingSlash(home)}/.docker-git`
const home = resolveDockerEnvValue("HOME") ?? resolveDockerEnvValue("USERPROFILE")
return home === null ? null : `${trimDockerPathTrailingSlash(home)}/.docker-git`
}

const resolveProjectsRootHostOverride = (): string | null => resolveEnvValue("DOCKER_GIT_PROJECTS_ROOT_HOST")
const resolveProjectsRootHostOverride = (): string | null => resolveDockerEnvValue("DOCKER_GIT_PROJECTS_ROOT_HOST")

const resolveCurrentContainerId = (
cwd: string
): Effect.Effect<string | null, never, CommandExecutor.CommandExecutor> => {
const fromEnv = resolveEnvValue("HOSTNAME")
const fromEnv = resolveDockerEnvValue("HOSTNAME")
if (fromEnv !== null) {
return Effect.succeed(fromEnv)
}
Expand Down
33 changes: 33 additions & 0 deletions packages/lib/src/shell/docker-compose-env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type * as CommandExecutor from "@effect/platform/CommandExecutor"
import { Effect } from "effect"

import { resolveDockerEnvValue, resolveDockerVolumeHostPath, trimDockerPathTrailingSlash } from "./docker-auth.js"

export const composeSpec = (cwd: string, args: ReadonlyArray<string>) => ({
cwd,
command: "docker",
args: ["compose", "--ansi", "never", "--progress", "plain", ...args]
})

const resolveProjectsRootCandidate = (): string | null => {
const explicit = resolveDockerEnvValue("DOCKER_GIT_PROJECTS_ROOT")
if (explicit !== null) {
return explicit
}

const home = resolveDockerEnvValue("HOME") ?? resolveDockerEnvValue("USERPROFILE")
return home === null ? null : `${trimDockerPathTrailingSlash(home)}/.docker-git`
}

export const resolveDockerComposeEnv = (
cwd: string
): Effect.Effect<Readonly<Record<string, string>>, never, CommandExecutor.CommandExecutor> =>
Effect.gen(function*(_) {
const projectsRoot = resolveProjectsRootCandidate()
if (projectsRoot === null) {
return {}
}

const remappedProjectsRoot = yield* _(resolveDockerVolumeHostPath(cwd, projectsRoot))
return remappedProjectsRoot === projectsRoot ? {} : { DOCKER_GIT_PROJECTS_ROOT_HOST: remappedProjectsRoot }
})
13 changes: 13 additions & 0 deletions packages/lib/src/shell/docker-inspect-parse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export const parseInspectNetworkEntry = (line: string): ReadonlyArray<readonly [string, string]> => {
const idx = line.indexOf("=")
if (idx <= 0) {
return []
}
const network = line.slice(0, idx).trim()
const ip = line.slice(idx + 1).trim()
if (network.length === 0 || ip.length === 0) {
return []
}
const entry: readonly [string, string] = [network, ip]
return [entry]
}
58 changes: 28 additions & 30 deletions packages/lib/src/shell/docker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,52 +5,50 @@ import type { PlatformError } from "@effect/platform/Error"
import { Duration, Effect, pipe, Schedule } from "effect"

import { runCommandCapture, runCommandExitCode, runCommandWithExitCodes } from "./command-runner.js"
import { composeSpec, resolveDockerComposeEnv } from "./docker-compose-env.js"
import { parseInspectNetworkEntry } from "./docker-inspect-parse.js"
import { CommandFailedError, DockerCommandError } from "./errors.js"

export { classifyDockerAccessIssue, ensureDockerDaemonAccess } from "./docker-daemon-access.js"
export { parseDockerPublishedHostPorts, runDockerPsPublishedHostPorts } from "./docker-published-ports.js"

const composeSpec = (cwd: string, args: ReadonlyArray<string>) => ({
cwd,
command: "docker",
args: ["compose", "--ansi", "never", "--progress", "plain", ...args]
})

const parseInspectNetworkEntry = (line: string): ReadonlyArray<readonly [string, string]> => {
const idx = line.indexOf("=")
if (idx <= 0) {
return []
}
const network = line.slice(0, idx).trim()
const ip = line.slice(idx + 1).trim()
if (network.length === 0 || ip.length === 0) {
return []
}
const entry: readonly [string, string] = [network, ip]
return [entry]
}

const runCompose = (
cwd: string,
args: ReadonlyArray<string>,
okExitCodes: ReadonlyArray<number>
): Effect.Effect<void, DockerCommandError | PlatformError, CommandExecutor.CommandExecutor> =>
runCommandWithExitCodes(
composeSpec(cwd, args),
okExitCodes,
(exitCode) => new DockerCommandError({ exitCode })
)
Effect.gen(function*(_) {
const env = yield* _(resolveDockerComposeEnv(cwd))
yield* _(
runCommandWithExitCodes(
{
...composeSpec(cwd, args),
...(Object.keys(env).length > 0 ? { env } : {})
},
okExitCodes,
(exitCode) => new DockerCommandError({ exitCode })
)
)
})

const runComposeCapture = (
cwd: string,
args: ReadonlyArray<string>,
okExitCodes: ReadonlyArray<number>
): Effect.Effect<string, DockerCommandError | PlatformError, CommandExecutor.CommandExecutor> =>
runCommandCapture(
composeSpec(cwd, args),
okExitCodes,
(exitCode) => new DockerCommandError({ exitCode })
)
Effect.gen(function*(_) {
const env = yield* _(resolveDockerComposeEnv(cwd))
return yield* _(
runCommandCapture(
{
...composeSpec(cwd, args),
...(Object.keys(env).length > 0 ? { env } : {})
},
okExitCodes,
(exitCode) => new DockerCommandError({ exitCode })
)
)
})

const dockerComposeUpRetrySchedule = Schedule.addDelay(
Schedule.recurs(2),
Expand Down
4 changes: 4 additions & 0 deletions packages/lib/tests/usecases/prepare-files.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@ describe("prepareProjectFiles", () => {
expect(entrypoint).toContain('OPENCODE_DATA_DIR="/home/dev/.local/share/opencode"')
expect(entrypoint).toContain('OPENCODE_SHARED_HOME="/home/dev/.codex-shared/opencode"')
expect(entrypoint).toContain('OPENCODE_CONFIG_DIR="/home/dev/.config/opencode"')
expect(entrypoint).toContain('su - dev -s /bin/bash -c "bash -lc')
expect(entrypoint).toContain('. /etc/profile 2>/dev/null || true;')
expect(entrypoint).toContain("codex exec")
expect(entrypoint).not.toContain("codex --approval-mode full-auto")
expect(entrypoint).toContain('"plugin": ["oh-my-opencode"]')
expect(entrypoint).toContain("branch '$REPO_REF' missing; retrying without --branch")
expect(entrypoint).not.toContain("git ls-remote --symref")
Expand Down
Loading