Skip to content

Commit b783fa6

Browse files
authored
Merge pull request #130 from ProverCoderAI/fix-auto-agent
fix(auto): align agent launch with container shell
2 parents b8f05aa + 57fc680 commit b783fa6

8 files changed

Lines changed: 113 additions & 43 deletions

File tree

packages/app/tests/docker-git/entrypoint-auth.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,14 @@ describe("renderEntrypoint auth bridge", () => {
6868
expect(entrypoint).toContain(
6969
"docker_git_link_claude_file \"$CLAUDE_CONFIG_DIR/.claude.json\" \"$CLAUDE_HOME_JSON\""
7070
)
71+
expect(entrypoint).toContain("su - dev -s /bin/bash -c \"bash -lc")
72+
expect(entrypoint).toContain(". /etc/profile 2>/dev/null || true;")
73+
expect(entrypoint).toContain(String.raw`. \"$AGENT_ENV_FILE\" 2>/dev/null || true;`)
74+
expect(entrypoint).toContain(
75+
String.raw`claude --dangerously-skip-permissions -p \"\$(cat \"$AGENT_PROMPT_FILE\")\"`
76+
)
77+
expect(entrypoint).toContain(String.raw`codex exec \"\$(cat \"$AGENT_PROMPT_FILE\")\"`)
78+
expect(entrypoint).not.toContain("codex --approval-mode full-auto")
7179
expect(entrypoint).toContain("CLAUDE_GLOBAL_PROMPT_FILE=\"/home/dev/.claude/CLAUDE.md\"")
7280
expect(entrypoint).toContain("CLAUDE_AUTO_SYSTEM_PROMPT=\"${CLAUDE_AUTO_SYSTEM_PROMPT:-1}\"")
7381
expect(entrypoint).toContain("docker-git-managed:claude-md")

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

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,17 @@ fi`
4747

4848
const renderAgentPromptCommand = (mode: "claude" | "codex"): string =>
4949
mode === "claude"
50-
? String.raw`claude --dangerously-skip-permissions -p \"\$(cat $AGENT_PROMPT_FILE)\"`
51-
: String.raw`codex --approval-mode full-auto \"\$(cat $AGENT_PROMPT_FILE)\"`
50+
? String.raw`claude --dangerously-skip-permissions -p \"\$(cat \"$AGENT_PROMPT_FILE\")\"`
51+
: String.raw`codex exec \"\$(cat \"$AGENT_PROMPT_FILE\")\"`
52+
53+
const renderAgentAutoLaunchCommand = (
54+
config: TemplateConfig,
55+
mode: "claude" | "codex"
56+
): string =>
57+
String
58+
.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\" && ${
59+
renderAgentPromptCommand(mode)
60+
}'"`
5261

5362
const renderAgentModeBlock = (
5463
config: TemplateConfig,
@@ -60,8 +69,7 @@ const renderAgentModeBlock = (
6069
return String.raw`"${mode}")
6170
echo "${startMessage}"
6271
if [[ -n "$AGENT_PROMPT" ]]; then
63-
if su - ${config.sshUser} \
64-
-c ". /run/docker-git/agent-env.sh 2>/dev/null; cd '$TARGET_DIR' && ${renderAgentPromptCommand(mode)}"; then
72+
if ${renderAgentAutoLaunchCommand(config, mode)}; then
6573
AGENT_OK=1
6674
fi
6775
else

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ const renderAgentAutoEnv = (agentAuto: boolean | undefined): string =>
4545
? ` AGENT_AUTO: "1"\n`
4646
: ""
4747

48+
const renderProjectsRootHostMount = (projectsRoot: string): string =>
49+
`\${DOCKER_GIT_PROJECTS_ROOT_HOST:-${projectsRoot}}`
50+
51+
const renderSharedCodexHostMount = (projectsRoot: string): string =>
52+
`\${DOCKER_GIT_PROJECTS_ROOT_HOST:-${projectsRoot}}/.orch/auth/codex`
53+
4854
const buildPlaywrightFragments = (
4955
config: TemplateConfig,
5056
networkName: string
@@ -126,10 +132,10 @@ ${fragments.maybePlaywrightEnv}${fragments.maybeDependsOn} env_file:
126132
- "127.0.0.1:${config.sshPort}:22"
127133
volumes:
128134
- ${config.volumeName}:/home/${config.sshUser}
129-
- ${config.dockerGitPath}:/home/${config.sshUser}/.docker-git
135+
- ${renderProjectsRootHostMount(config.dockerGitPath)}:/home/${config.sshUser}/.docker-git
130136
- ${config.authorizedKeysPath}:/authorized_keys:ro
131137
- ${config.codexAuthPath}:${config.codexHome}
132-
- ${config.codexSharedAuthPath}:${config.codexHome}-shared
138+
- ${renderSharedCodexHostMount(config.dockerGitPath)}:${config.codexHome}-shared
133139
- /var/run/docker.sock:/var/run/docker.sock
134140
networks:
135141
- ${fragments.networkName}

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,12 @@ type DockerMountBinding = {
2525
readonly destination: string
2626
}
2727

28-
const resolveEnvValue = (key: string): string | null => {
28+
export const resolveDockerEnvValue = (key: string): string | null => {
2929
const value = process.env[key]?.trim()
3030
return value && value.length > 0 ? value : null
3131
}
3232

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

5353
const resolveContainerProjectsRoot = (): string | null => {
54-
const explicit = resolveEnvValue("DOCKER_GIT_PROJECTS_ROOT")
54+
const explicit = resolveDockerEnvValue("DOCKER_GIT_PROJECTS_ROOT")
5555
if (explicit !== null) {
5656
return explicit
5757
}
5858

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

63-
const resolveProjectsRootHostOverride = (): string | null => resolveEnvValue("DOCKER_GIT_PROJECTS_ROOT_HOST")
63+
const resolveProjectsRootHostOverride = (): string | null => resolveDockerEnvValue("DOCKER_GIT_PROJECTS_ROOT_HOST")
6464

6565
const resolveCurrentContainerId = (
6666
cwd: string
6767
): Effect.Effect<string | null, never, CommandExecutor.CommandExecutor> => {
68-
const fromEnv = resolveEnvValue("HOSTNAME")
68+
const fromEnv = resolveDockerEnvValue("HOSTNAME")
6969
if (fromEnv !== null) {
7070
return Effect.succeed(fromEnv)
7171
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import type * as CommandExecutor from "@effect/platform/CommandExecutor"
2+
import { Effect } from "effect"
3+
4+
import { resolveDockerEnvValue, resolveDockerVolumeHostPath, trimDockerPathTrailingSlash } from "./docker-auth.js"
5+
6+
export const composeSpec = (cwd: string, args: ReadonlyArray<string>) => ({
7+
cwd,
8+
command: "docker",
9+
args: ["compose", "--ansi", "never", "--progress", "plain", ...args]
10+
})
11+
12+
const resolveProjectsRootCandidate = (): string | null => {
13+
const explicit = resolveDockerEnvValue("DOCKER_GIT_PROJECTS_ROOT")
14+
if (explicit !== null) {
15+
return explicit
16+
}
17+
18+
const home = resolveDockerEnvValue("HOME") ?? resolveDockerEnvValue("USERPROFILE")
19+
return home === null ? null : `${trimDockerPathTrailingSlash(home)}/.docker-git`
20+
}
21+
22+
export const resolveDockerComposeEnv = (
23+
cwd: string
24+
): Effect.Effect<Readonly<Record<string, string>>, never, CommandExecutor.CommandExecutor> =>
25+
Effect.gen(function*(_) {
26+
const projectsRoot = resolveProjectsRootCandidate()
27+
if (projectsRoot === null) {
28+
return {}
29+
}
30+
31+
const remappedProjectsRoot = yield* _(resolveDockerVolumeHostPath(cwd, projectsRoot))
32+
return remappedProjectsRoot === projectsRoot ? {} : { DOCKER_GIT_PROJECTS_ROOT_HOST: remappedProjectsRoot }
33+
})
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export const parseInspectNetworkEntry = (line: string): ReadonlyArray<readonly [string, string]> => {
2+
const idx = line.indexOf("=")
3+
if (idx <= 0) {
4+
return []
5+
}
6+
const network = line.slice(0, idx).trim()
7+
const ip = line.slice(idx + 1).trim()
8+
if (network.length === 0 || ip.length === 0) {
9+
return []
10+
}
11+
const entry: readonly [string, string] = [network, ip]
12+
return [entry]
13+
}

packages/lib/src/shell/docker.ts

Lines changed: 28 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,52 +5,50 @@ import type { PlatformError } from "@effect/platform/Error"
55
import { Duration, Effect, pipe, Schedule } from "effect"
66

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

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

13-
const composeSpec = (cwd: string, args: ReadonlyArray<string>) => ({
14-
cwd,
15-
command: "docker",
16-
args: ["compose", "--ansi", "never", "--progress", "plain", ...args]
17-
})
18-
19-
const parseInspectNetworkEntry = (line: string): ReadonlyArray<readonly [string, string]> => {
20-
const idx = line.indexOf("=")
21-
if (idx <= 0) {
22-
return []
23-
}
24-
const network = line.slice(0, idx).trim()
25-
const ip = line.slice(idx + 1).trim()
26-
if (network.length === 0 || ip.length === 0) {
27-
return []
28-
}
29-
const entry: readonly [string, string] = [network, ip]
30-
return [entry]
31-
}
32-
3315
const runCompose = (
3416
cwd: string,
3517
args: ReadonlyArray<string>,
3618
okExitCodes: ReadonlyArray<number>
3719
): Effect.Effect<void, DockerCommandError | PlatformError, CommandExecutor.CommandExecutor> =>
38-
runCommandWithExitCodes(
39-
composeSpec(cwd, args),
40-
okExitCodes,
41-
(exitCode) => new DockerCommandError({ exitCode })
42-
)
20+
Effect.gen(function*(_) {
21+
const env = yield* _(resolveDockerComposeEnv(cwd))
22+
yield* _(
23+
runCommandWithExitCodes(
24+
{
25+
...composeSpec(cwd, args),
26+
...(Object.keys(env).length > 0 ? { env } : {})
27+
},
28+
okExitCodes,
29+
(exitCode) => new DockerCommandError({ exitCode })
30+
)
31+
)
32+
})
4333

4434
const runComposeCapture = (
4535
cwd: string,
4636
args: ReadonlyArray<string>,
4737
okExitCodes: ReadonlyArray<number>
4838
): Effect.Effect<string, DockerCommandError | PlatformError, CommandExecutor.CommandExecutor> =>
49-
runCommandCapture(
50-
composeSpec(cwd, args),
51-
okExitCodes,
52-
(exitCode) => new DockerCommandError({ exitCode })
53-
)
39+
Effect.gen(function*(_) {
40+
const env = yield* _(resolveDockerComposeEnv(cwd))
41+
return yield* _(
42+
runCommandCapture(
43+
{
44+
...composeSpec(cwd, args),
45+
...(Object.keys(env).length > 0 ? { env } : {})
46+
},
47+
okExitCodes,
48+
(exitCode) => new DockerCommandError({ exitCode })
49+
)
50+
)
51+
})
5452

5553
const dockerComposeUpRetrySchedule = Schedule.addDelay(
5654
Schedule.recurs(2),

packages/lib/tests/usecases/prepare-files.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,10 @@ describe("prepareProjectFiles", () => {
137137
expect(entrypoint).toContain('OPENCODE_DATA_DIR="/home/dev/.local/share/opencode"')
138138
expect(entrypoint).toContain('OPENCODE_SHARED_HOME="/home/dev/.codex-shared/opencode"')
139139
expect(entrypoint).toContain('OPENCODE_CONFIG_DIR="/home/dev/.config/opencode"')
140+
expect(entrypoint).toContain('su - dev -s /bin/bash -c "bash -lc')
141+
expect(entrypoint).toContain('. /etc/profile 2>/dev/null || true;')
142+
expect(entrypoint).toContain("codex exec")
143+
expect(entrypoint).not.toContain("codex --approval-mode full-auto")
140144
expect(entrypoint).toContain('"plugin": ["oh-my-opencode"]')
141145
expect(entrypoint).toContain("branch '$REPO_REF' missing; retrying without --branch")
142146
expect(entrypoint).not.toContain("git ls-remote --symref")

0 commit comments

Comments
 (0)