From fd104d841e798d4c88640b0b7883e4ab1201271c Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Thu, 12 Mar 2026 20:07:31 +0000 Subject: [PATCH 1/2] fix(auto): align agent launch with container shell --- .../tests/docker-git/entrypoint-auth.test.ts | 8 ++ .../src/core/templates-entrypoint/agent.ts | 13 +++- .../lib/src/core/templates/docker-compose.ts | 10 ++- packages/lib/src/shell/docker.ts | 77 ++++++++++++++++--- .../lib/tests/usecases/prepare-files.test.ts | 4 + 5 files changed, 96 insertions(+), 16 deletions(-) diff --git a/packages/app/tests/docker-git/entrypoint-auth.test.ts b/packages/app/tests/docker-git/entrypoint-auth.test.ts index 3b3d5222..d863bcb7 100644 --- a/packages/app/tests/docker-git/entrypoint-auth.test.ts +++ b/packages/app/tests/docker-git/entrypoint-auth.test.ts @@ -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") diff --git a/packages/lib/src/core/templates-entrypoint/agent.ts b/packages/lib/src/core/templates-entrypoint/agent.ts index 2e6267d3..29ced006 100644 --- a/packages/lib/src/core/templates-entrypoint/agent.ts +++ b/packages/lib/src/core/templates-entrypoint/agent.ts @@ -47,8 +47,14 @@ 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, @@ -60,8 +66,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 diff --git a/packages/lib/src/core/templates/docker-compose.ts b/packages/lib/src/core/templates/docker-compose.ts index 000cbd02..a17b02bb 100644 --- a/packages/lib/src/core/templates/docker-compose.ts +++ b/packages/lib/src/core/templates/docker-compose.ts @@ -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 @@ -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} diff --git a/packages/lib/src/shell/docker.ts b/packages/lib/src/shell/docker.ts index fd87cf67..e5c4a63f 100644 --- a/packages/lib/src/shell/docker.ts +++ b/packages/lib/src/shell/docker.ts @@ -5,6 +5,7 @@ import type { PlatformError } from "@effect/platform/Error" import { Duration, Effect, pipe, Schedule } from "effect" import { runCommandCapture, runCommandExitCode, runCommandWithExitCodes } from "./command-runner.js" +import { resolveDockerVolumeHostPath } from "./docker-auth.js" import { CommandFailedError, DockerCommandError } from "./errors.js" export { classifyDockerAccessIssue, ensureDockerDaemonAccess } from "./docker-daemon-access.js" @@ -16,6 +17,46 @@ const composeSpec = (cwd: string, args: ReadonlyArray) => ({ args: ["compose", "--ansi", "never", "--progress", "plain", ...args] }) +const resolveEnvValue = (key: string): string | null => { + const value = process.env[key]?.trim() + return value && value.length > 0 ? value : null +} + +const trimTrailingSlash = (value: string): string => { + let end = value.length + while (end > 0) { + const char = value[end - 1] + if (char !== "/" && char !== "\\") { + break + } + end -= 1 + } + return value.slice(0, end) +} + +const resolveProjectsRootCandidate = (): string | null => { + const explicit = resolveEnvValue("DOCKER_GIT_PROJECTS_ROOT") + if (explicit !== null) { + return explicit + } + + const home = resolveEnvValue("HOME") ?? resolveEnvValue("USERPROFILE") + return home === null ? null : `${trimTrailingSlash(home)}/.docker-git` +} + +const resolveComposeEnv = ( + cwd: string +): Effect.Effect>, 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 } + }) + const parseInspectNetworkEntry = (line: string): ReadonlyArray => { const idx = line.indexOf("=") if (idx <= 0) { @@ -35,22 +76,38 @@ const runCompose = ( args: ReadonlyArray, okExitCodes: ReadonlyArray ): Effect.Effect => - runCommandWithExitCodes( - composeSpec(cwd, args), - okExitCodes, - (exitCode) => new DockerCommandError({ exitCode }) - ) + Effect.gen(function*(_) { + const env = yield* _(resolveComposeEnv(cwd)) + yield* _( + runCommandWithExitCodes( + { + ...composeSpec(cwd, args), + ...(Object.keys(env).length > 0 ? { env } : {}) + }, + okExitCodes, + (exitCode) => new DockerCommandError({ exitCode }) + ) + ) + }) const runComposeCapture = ( cwd: string, args: ReadonlyArray, okExitCodes: ReadonlyArray ): Effect.Effect => - runCommandCapture( - composeSpec(cwd, args), - okExitCodes, - (exitCode) => new DockerCommandError({ exitCode }) - ) + Effect.gen(function*(_) { + const env = yield* _(resolveComposeEnv(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), diff --git a/packages/lib/tests/usecases/prepare-files.test.ts b/packages/lib/tests/usecases/prepare-files.test.ts index 9d9a1420..99e13c96 100644 --- a/packages/lib/tests/usecases/prepare-files.test.ts +++ b/packages/lib/tests/usecases/prepare-files.test.ts @@ -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") From 57fc680c67af59e75402301241b4a075f6969c8a Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Thu, 12 Mar 2026 20:55:31 +0000 Subject: [PATCH 2/2] fix(auto): remap shared docker-git root for compose --- .../src/core/templates-entrypoint/agent.ts | 5 +- packages/lib/src/shell/docker-auth.ts | 14 ++-- packages/lib/src/shell/docker-compose-env.ts | 33 +++++++++ .../lib/src/shell/docker-inspect-parse.ts | 13 ++++ packages/lib/src/shell/docker.ts | 67 ++----------------- 5 files changed, 61 insertions(+), 71 deletions(-) create mode 100644 packages/lib/src/shell/docker-compose-env.ts create mode 100644 packages/lib/src/shell/docker-inspect-parse.ts diff --git a/packages/lib/src/core/templates-entrypoint/agent.ts b/packages/lib/src/core/templates-entrypoint/agent.ts index 29ced006..a6b10ce6 100644 --- a/packages/lib/src/core/templates-entrypoint/agent.ts +++ b/packages/lib/src/core/templates-entrypoint/agent.ts @@ -54,7 +54,10 @@ 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)}'"` + 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, diff --git a/packages/lib/src/shell/docker-auth.ts b/packages/lib/src/shell/docker-auth.ts index 1c1a6f1b..5e739a63 100644 --- a/packages/lib/src/shell/docker-auth.ts +++ b/packages/lib/src/shell/docker-auth.ts @@ -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] @@ -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 => { - const fromEnv = resolveEnvValue("HOSTNAME") + const fromEnv = resolveDockerEnvValue("HOSTNAME") if (fromEnv !== null) { return Effect.succeed(fromEnv) } diff --git a/packages/lib/src/shell/docker-compose-env.ts b/packages/lib/src/shell/docker-compose-env.ts new file mode 100644 index 00000000..a2743ad4 --- /dev/null +++ b/packages/lib/src/shell/docker-compose-env.ts @@ -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) => ({ + 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>, 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 } + }) diff --git a/packages/lib/src/shell/docker-inspect-parse.ts b/packages/lib/src/shell/docker-inspect-parse.ts new file mode 100644 index 00000000..7bd9641e --- /dev/null +++ b/packages/lib/src/shell/docker-inspect-parse.ts @@ -0,0 +1,13 @@ +export const parseInspectNetworkEntry = (line: string): ReadonlyArray => { + 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] +} diff --git a/packages/lib/src/shell/docker.ts b/packages/lib/src/shell/docker.ts index e5c4a63f..8d78b116 100644 --- a/packages/lib/src/shell/docker.ts +++ b/packages/lib/src/shell/docker.ts @@ -5,79 +5,20 @@ import type { PlatformError } from "@effect/platform/Error" import { Duration, Effect, pipe, Schedule } from "effect" import { runCommandCapture, runCommandExitCode, runCommandWithExitCodes } from "./command-runner.js" -import { resolveDockerVolumeHostPath } from "./docker-auth.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) => ({ - cwd, - command: "docker", - args: ["compose", "--ansi", "never", "--progress", "plain", ...args] -}) - -const resolveEnvValue = (key: string): string | null => { - const value = process.env[key]?.trim() - return value && value.length > 0 ? value : null -} - -const trimTrailingSlash = (value: string): string => { - let end = value.length - while (end > 0) { - const char = value[end - 1] - if (char !== "/" && char !== "\\") { - break - } - end -= 1 - } - return value.slice(0, end) -} - -const resolveProjectsRootCandidate = (): string | null => { - const explicit = resolveEnvValue("DOCKER_GIT_PROJECTS_ROOT") - if (explicit !== null) { - return explicit - } - - const home = resolveEnvValue("HOME") ?? resolveEnvValue("USERPROFILE") - return home === null ? null : `${trimTrailingSlash(home)}/.docker-git` -} - -const resolveComposeEnv = ( - cwd: string -): Effect.Effect>, 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 } - }) - -const parseInspectNetworkEntry = (line: string): ReadonlyArray => { - 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, okExitCodes: ReadonlyArray ): Effect.Effect => Effect.gen(function*(_) { - const env = yield* _(resolveComposeEnv(cwd)) + const env = yield* _(resolveDockerComposeEnv(cwd)) yield* _( runCommandWithExitCodes( { @@ -96,7 +37,7 @@ const runComposeCapture = ( okExitCodes: ReadonlyArray ): Effect.Effect => Effect.gen(function*(_) { - const env = yield* _(resolveComposeEnv(cwd)) + const env = yield* _(resolveDockerComposeEnv(cwd)) return yield* _( runCommandCapture( {