Skip to content

Commit c06a9e9

Browse files
committed
fix(shell): preserve remote and isolated controller bootstrap
1 parent 3dee2bd commit c06a9e9

19 files changed

Lines changed: 458 additions & 60 deletions

ctl

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ set -euo pipefail
1313

1414
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
1515
COMPOSE_FILE="$ROOT/docker-compose.yml"
16+
COMPOSE_ISOLATED_FILE="$ROOT/docker-compose.isolated.yml"
1617
CONTAINER_NAME="docker-git-api"
1718
API_PORT="${DOCKER_GIT_API_PORT:-3334}"
1819
API_HOST="${DOCKER_GIT_API_BIND_HOST:-127.0.0.1}"
@@ -54,7 +55,11 @@ USAGE
5455
}
5556

5657
compose() {
57-
"${DOCKER_CMD[@]}" compose -f "$COMPOSE_FILE" "$@"
58+
local compose_args=(-f "$COMPOSE_FILE")
59+
if [[ "${DOCKER_GIT_DOCKER_RUNTIME:-host}" == "isolated" ]]; then
60+
compose_args+=(-f "$COMPOSE_ISOLATED_FILE")
61+
fi
62+
"${DOCKER_CMD[@]}" compose "${compose_args[@]}" "$@"
5863
}
5964

6065
compute_controller_revision() {

docker-compose.api.isolated.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
services:
2+
api:
3+
environment:
4+
DOCKER_GIT_PROJECT_DOCKER_HOST: ${DOCKER_GIT_PROJECT_DOCKER_HOST:-tcp://host.docker.internal:2375}
5+
volumes: !override
6+
- docker_git_projects:${DOCKER_GIT_PROJECTS_ROOT:-/home/dev/.docker-git}
7+
- docker_git_docker_data:/var/lib/docker

docker-compose.isolated.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
services:
2+
api:
3+
environment:
4+
DOCKER_GIT_PROJECT_DOCKER_HOST: ${DOCKER_GIT_PROJECT_DOCKER_HOST:-tcp://host.docker.internal:2375}
5+
volumes: !override
6+
- docker_git_projects:${DOCKER_GIT_PROJECTS_ROOT:-/home/dev/.docker-git}
7+
- docker_git_docker_data:/var/lib/docker

packages/api/README.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ container created from this package binds the host socket
1616
(`/var/run/docker.sock:/var/run/docker.sock`, see `docker-compose.yml`) and
1717
uses it to spawn per-project containers. `DOCKER_GIT_DOCKER_RUNTIME=isolated`
1818
is an opt-in fallback for environments that explicitly require an embedded
19-
controller daemon.
19+
controller daemon. In isolated mode, start the controller through the host CLI
20+
or include `docker-compose.isolated.yml`; that overlay removes the host socket
21+
bind and defaults project containers to the embedded daemon endpoint
22+
`tcp://host.docker.internal:2375`.
2023

2124
Security note: binding `/var/run/docker.sock` gives the controller container
2225
root-equivalent control over the host Docker daemon, including the ability to
@@ -59,6 +62,14 @@ docker compose up -d --build
5962
./ctl health
6063
```
6164

65+
Isolated fallback:
66+
67+
```bash
68+
DOCKER_GIT_DOCKER_RUNTIME=isolated \
69+
docker compose -f docker-compose.yml -f docker-compose.isolated.yml up -d --build
70+
./ctl health
71+
```
72+
6273
Default port mapping:
6374

6475
- host: `127.0.0.1:3334`
@@ -73,7 +84,7 @@ Optional env:
7384
- `DOCKER_GIT_CONTROLLER_PRIVILEGED` (default: `false`; set to `true` only when using `DOCKER_GIT_DOCKER_RUNTIME=isolated`)
7485
- `DOCKER_GIT_DOCKERD_TCP_HOST` (default: `tcp://0.0.0.0:2375`; reachable only inside Docker networks unless explicitly published)
7586
- `DOCKER_GIT_DOCKERD_DEFAULT_CGROUPNS_MODE` (default: `host`; keeps nested project containers compatible with cgroup v2 DinD)
76-
- `DOCKER_GIT_PROJECT_DOCKER_HOST` (default: empty; unset uses host socket in project containers when mounted)
87+
- `DOCKER_GIT_PROJECT_DOCKER_HOST` (default: empty in host mode; isolated mode defaults to `tcp://host.docker.internal:2375`)
7788
- `DOCKER_GIT_PROJECT_SSH_BIND_HOST` (default: `0.0.0.0`)
7889
- `DOCKER_GIT_PROJECTS_ROOT` (container path, default: `/home/dev/.docker-git`)
7990
- `DOCKER_GIT_PROJECTS_ROOT_VOLUME` (Docker volume name for controller state, default: `docker-git-projects`)

packages/api/scripts/start-controller.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ cleanup() {
1515
trap cleanup EXIT INT TERM
1616

1717
if [[ "$runtime" == "isolated" ]]; then
18+
if [[ -z "${DOCKER_GIT_PROJECT_DOCKER_HOST:-}" ]]; then
19+
export DOCKER_GIT_PROJECT_DOCKER_HOST="tcp://host.docker.internal:2375"
20+
fi
21+
1822
if [[ "$docker_host" != unix://* ]]; then
1923
echo "DOCKER_GIT_DOCKER_RUNTIME=isolated requires a unix:// DOCKER_HOST for the managed controller daemon." >&2
2024
exit 1

packages/app/scripts/print-controller-revision.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@
33
import { NodeContext, NodeRuntime } from "@effect/platform-node"
44
import { Effect, pipe } from "effect"
55

6+
import {
7+
controllerRevisionForMode,
8+
parseControllerBuildSkillerMode,
9+
parseControllerGpuMode
10+
} from "../src/docker-git/controller-compose.ts"
611
import { computeLocalControllerRevision } from "../src/docker-git/controller-revision.ts"
12+
import { parseControllerDockerRuntime } from "../src/docker-git/controller-runtime.ts"
713

814
// CHANGE: expose controller revision computation as a reusable Bun script for shell tooling
915
// WHY: ctl must inject the same deterministic controller revision into docker compose as the host CLI bootstrap path
@@ -25,9 +31,32 @@ const readComposePath = (): Effect.Effect<string, Error> => {
2531
: Effect.fail(new Error(usage))
2632
}
2733

34+
const readControllerRevisionModes = (): Effect.Effect<{
35+
readonly buildSkillerMode: "0" | "1"
36+
readonly dockerRuntime: "host" | "isolated"
37+
readonly gpuMode: "none" | "all"
38+
}, Error> => {
39+
const gpuMode = parseControllerGpuMode(process.env["DOCKER_GIT_CONTROLLER_GPU"])
40+
const buildSkillerMode = parseControllerBuildSkillerMode(process.env["DOCKER_GIT_CONTROLLER_BUILD_SKILLER"])
41+
const dockerRuntime = parseControllerDockerRuntime(process.env["DOCKER_GIT_DOCKER_RUNTIME"])
42+
if (gpuMode === null || buildSkillerMode === null || dockerRuntime === null) {
43+
return Effect.fail(new Error("Invalid controller revision mode environment."))
44+
}
45+
return Effect.succeed({ buildSkillerMode, dockerRuntime, gpuMode })
46+
}
47+
2848
const main = pipe(
29-
readComposePath(),
30-
Effect.flatMap((composePath) => computeLocalControllerRevision(composePath)),
49+
Effect.all({
50+
composePath: readComposePath(),
51+
modes: readControllerRevisionModes()
52+
}),
53+
Effect.flatMap(({ composePath, modes }) =>
54+
computeLocalControllerRevision(composePath).pipe(
55+
Effect.map((revision) =>
56+
controllerRevisionForMode(revision, modes.gpuMode, modes.buildSkillerMode, modes.dockerRuntime)
57+
)
58+
)
59+
),
3160
Effect.tap((revision) => Effect.sync(() => process.stdout.write(`${revision}\n`))),
3261
Effect.asVoid,
3362
Effect.provide(NodeContext.layer)
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import type { PlatformError } from "@effect/platform/Error"
2+
import * as FileSystem from "@effect/platform/FileSystem"
3+
import * as Path from "@effect/platform/Path"
4+
import { Effect } from "effect"
5+
6+
import {
7+
type ControllerDockerRuntime,
8+
controllerDockerRuntimeEnvKey,
9+
parseControllerDockerRuntime
10+
} from "./controller-runtime.js"
11+
import { type ControllerBootstrapError, controllerBootstrapError } from "./host-errors.js"
12+
13+
const mapComposePathError = (error: PlatformError): ControllerBootstrapError =>
14+
controllerBootstrapError(`Failed to resolve docker-compose.yml path.\nDetails: ${String(error)}`)
15+
16+
export const loadControllerDockerRuntime = (): Effect.Effect<ControllerDockerRuntime, ControllerBootstrapError> => {
17+
const raw = process.env[controllerDockerRuntimeEnvKey]
18+
const parsed = parseControllerDockerRuntime(raw)
19+
if (parsed !== null) {
20+
return Effect.succeed(parsed)
21+
}
22+
return Effect.fail(
23+
controllerBootstrapError(
24+
`${controllerDockerRuntimeEnvKey} must be unset or one of: host, isolated. Received: ${raw ?? ""}`
25+
)
26+
)
27+
}
28+
29+
const isolatedOverlayFileName = (composeFileName: string): string =>
30+
composeFileName.endsWith(".yaml")
31+
? `${composeFileName.slice(0, -".yaml".length)}.isolated.yaml`
32+
: `${composeFileName.slice(0, -".yml".length)}.isolated.yml`
33+
34+
export const resolveControllerRuntimeOverlayPath = (
35+
composePath: string,
36+
dockerRuntime: ControllerDockerRuntime
37+
): Effect.Effect<string | null, ControllerBootstrapError, FileSystem.FileSystem | Path.Path> =>
38+
dockerRuntime === "host"
39+
? Effect.succeed(null)
40+
: Effect.gen(function*(_) {
41+
const fs = yield* _(FileSystem.FileSystem)
42+
const path = yield* _(Path.Path)
43+
const runtimeOverlayPath = path.join(
44+
path.dirname(composePath),
45+
isolatedOverlayFileName(path.basename(composePath))
46+
)
47+
const exists = yield* _(fs.exists(runtimeOverlayPath).pipe(Effect.mapError(mapComposePathError)))
48+
return exists
49+
? runtimeOverlayPath
50+
: yield* _(
51+
Effect.fail(
52+
controllerBootstrapError(
53+
`${controllerDockerRuntimeEnvKey}=isolated requires ${runtimeOverlayPath}, but it was not found.`
54+
)
55+
)
56+
)
57+
})

packages/app/src/docker-git/controller-compose.ts

Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import * as FileSystem from "@effect/platform/FileSystem"
44
import * as Path from "@effect/platform/Path"
55
import { Duration, Effect } from "effect"
66

7+
import { loadControllerDockerRuntime, resolveControllerRuntimeOverlayPath } from "./controller-compose-runtime.js"
78
import { computeLocalControllerRevision, controllerRevisionEnvKey } from "./controller-revision.js"
9+
import type { ControllerDockerRuntime } from "./controller-runtime.js"
810
import { runCommandWithCapturedOutput } from "./frontend-lib/shell/command-runner.js"
911
import { findExistingUpwards } from "./frontend-lib/usecases/path-helpers.js"
1012
import type { ControllerBootstrapError } from "./host-errors.js"
@@ -18,6 +20,7 @@ export type ControllerBuildSkillerMode = "0" | "1"
1820
export type ControllerComposeFiles = {
1921
readonly composePath: string
2022
readonly gpuOverlayPath: string | null
23+
readonly runtimeOverlayPath: string | null
2124
}
2225

2326
const skillerSubmodulePath = "third_party/skiller-desktop-skills-manager"
@@ -47,8 +50,9 @@ export const parseControllerBuildSkillerMode = (raw?: string): ControllerBuildSk
4750
export const controllerRevisionForMode = (
4851
sourceRevision: string,
4952
gpuMode: ControllerGpuMode,
50-
buildSkillerMode: ControllerBuildSkillerMode = "1"
51-
): string => `${sourceRevision}-${gpuMode}-skiller${buildSkillerMode}`
53+
buildSkillerMode: ControllerBuildSkillerMode = "1",
54+
dockerRuntime: ControllerDockerRuntime = "host"
55+
): string => `${sourceRevision}-${dockerRuntime}-${gpuMode}-skiller${buildSkillerMode}`
5256

5357
const loadControllerGpuMode = (): Effect.Effect<ControllerGpuMode, ControllerBootstrapError> => {
5458
const raw = process.env[controllerGpuModeEnvKey]
@@ -187,11 +191,21 @@ export const ensureSkillerSubmoduleInitialized = (
187191

188192
export const composeFilesForMode = (
189193
composePath: string,
190-
gpuOverlayPath: string | null
191-
): ReadonlyArray<string> =>
192-
gpuOverlayPath === null
193-
? ["-f", composePath]
194-
: ["-f", composePath, "-f", gpuOverlayPath]
194+
gpuOverlayPath: string | null,
195+
runtimeOverlayPath: string | null = null
196+
): ReadonlyArray<string> => [
197+
"-f",
198+
composePath,
199+
...(runtimeOverlayPath === null ? [] : ["-f", runtimeOverlayPath]),
200+
...(gpuOverlayPath === null ? [] : ["-f", gpuOverlayPath])
201+
]
202+
203+
export const composeFilesToArgs = (composeFiles: ControllerComposeFiles): ReadonlyArray<string> =>
204+
composeFilesForMode(
205+
composeFiles.composePath,
206+
composeFiles.gpuOverlayPath,
207+
composeFiles.runtimeOverlayPath
208+
)
195209

196210
const requireGpuOverlayPath = (
197211
composePath: string
@@ -215,13 +229,14 @@ const composeFilesForGpuMode = (
215229
gpuMode: ControllerGpuMode
216230
): Effect.Effect<ControllerComposeFiles, ControllerBootstrapError, FileSystem.FileSystem | Path.Path> =>
217231
gpuMode === "none"
218-
? Effect.succeed({ composePath, gpuOverlayPath: null })
232+
? Effect.succeed({ composePath, gpuOverlayPath: null, runtimeOverlayPath: null })
219233
: requireGpuOverlayPath(composePath).pipe(
220-
Effect.map((gpuOverlayPath) => ({ composePath, gpuOverlayPath }))
234+
Effect.map((gpuOverlayPath) => ({ composePath, gpuOverlayPath, runtimeOverlayPath: null }))
221235
)
222236

223237
type ComposePathAndGpuMode = {
224238
readonly composePath: string
239+
readonly dockerRuntime: ControllerDockerRuntime
225240
readonly gpuMode: ControllerGpuMode
226241
readonly buildSkillerMode: ControllerBuildSkillerMode
227242
}
@@ -236,12 +251,12 @@ const withComposePathAndGpuMode = <A, R>(
236251
composeFilePath().pipe(
237252
Effect.mapError(mapComposePathError),
238253
Effect.flatMap((composePath) =>
239-
loadControllerGpuMode().pipe(
240-
Effect.flatMap((gpuMode) =>
241-
loadControllerBuildSkillerMode().pipe(
242-
Effect.flatMap((buildSkillerMode) => effect({ buildSkillerMode, composePath, gpuMode }))
243-
)
244-
)
254+
Effect.all({
255+
buildSkillerMode: loadControllerBuildSkillerMode(),
256+
dockerRuntime: loadControllerDockerRuntime(),
257+
gpuMode: loadControllerGpuMode()
258+
}).pipe(
259+
Effect.flatMap((modes) => effect({ composePath, ...modes }))
245260
)
246261
)
247262
)
@@ -250,16 +265,24 @@ export const resolveControllerComposeFiles = (): Effect.Effect<
250265
ControllerComposeFiles,
251266
ControllerBootstrapError,
252267
FileSystem.FileSystem | Path.Path
253-
> => withComposePathAndGpuMode(({ composePath, gpuMode }) => composeFilesForGpuMode(composePath, gpuMode))
268+
> =>
269+
withComposePathAndGpuMode(({ composePath, dockerRuntime, gpuMode }) =>
270+
Effect.gen(function*(_) {
271+
const composeFiles = yield* _(composeFilesForGpuMode(composePath, gpuMode))
272+
const runtimeOverlayPath = yield* _(resolveControllerRuntimeOverlayPath(composePath, dockerRuntime))
273+
return { ...composeFiles, runtimeOverlayPath }
274+
})
275+
)
254276

255277
const computeControllerRevision = (
256278
composePath: string,
257279
gpuMode: ControllerGpuMode,
258-
buildSkillerMode: ControllerBuildSkillerMode
280+
buildSkillerMode: ControllerBuildSkillerMode,
281+
dockerRuntime: ControllerDockerRuntime
259282
): Effect.Effect<string, ControllerBootstrapError, FileSystem.FileSystem | Path.Path> =>
260283
computeLocalControllerRevision(composePath).pipe(
261284
Effect.mapError(mapControllerRevisionError),
262-
Effect.map((revision) => controllerRevisionForMode(revision, gpuMode, buildSkillerMode))
285+
Effect.map((revision) => controllerRevisionForMode(revision, gpuMode, buildSkillerMode, dockerRuntime))
263286
)
264287

265288
const persistControllerRevision = (revision: string): Effect.Effect<void> =>
@@ -272,13 +295,13 @@ export const prepareControllerRevision = (): Effect.Effect<
272295
ControllerBootstrapError,
273296
FileSystem.FileSystem | Path.Path | CommandExecutor.CommandExecutor
274297
> =>
275-
withComposePathAndGpuMode(({ buildSkillerMode, composePath, gpuMode }) =>
298+
withComposePathAndGpuMode(({ buildSkillerMode, composePath, dockerRuntime, gpuMode }) =>
276299
Effect.gen(function*(_) {
277300
const path = yield* _(Path.Path)
278301
if (buildSkillerMode === "1") {
279302
yield* _(ensureSkillerSubmoduleInitialized(path.dirname(composePath)))
280303
}
281-
return yield* _(computeControllerRevision(composePath, gpuMode, buildSkillerMode))
304+
return yield* _(computeControllerRevision(composePath, gpuMode, buildSkillerMode, dockerRuntime))
282305
})
283306
).pipe(
284307
Effect.tap((revision) => persistControllerRevision(revision))

packages/app/src/docker-git/controller-docker.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type * as FileSystem from "@effect/platform/FileSystem"
44
import type * as Path from "@effect/platform/Path"
55
import { Effect } from "effect"
66

7-
import { composeFilesForMode, prepareControllerRevision, resolveControllerComposeFiles } from "./controller-compose.js"
7+
import { composeFilesToArgs, prepareControllerRevision, resolveControllerComposeFiles } from "./controller-compose.js"
88
import { readCurrentContainerName } from "./controller-hostname.js"
99
import {
1010
runCommandCaptureWithFailureOutput,
@@ -30,6 +30,7 @@ export {
3030
parseControllerBuildSkillerMode,
3131
parseControllerGpuMode
3232
} from "./controller-compose.js"
33+
export { parseControllerDockerRuntime } from "./controller-runtime.js"
3334

3435
export type ControllerRuntime =
3536
| CommandExecutor.CommandExecutor
@@ -385,7 +386,7 @@ export const runCompose = (
385386
const composeFiles = yield* _(resolveControllerComposeFiles())
386387
const invocation = buildDockerInvocation(dockerCommand, [
387388
"compose",
388-
...composeFilesForMode(composeFiles.composePath, composeFiles.gpuOverlayPath),
389+
...composeFilesToArgs(composeFiles),
389390
...args
390391
])
391392
const exitCode = yield* _(

packages/app/src/docker-git/controller-image-revision.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Effect } from "effect"
22

3-
import { composeFilesForMode, resolveControllerComposeFiles } from "./controller-compose.js"
3+
import { composeFilesToArgs, resolveControllerComposeFiles } from "./controller-compose.js"
44
import { type ControllerRuntime, runDockerCapture, runDockerCaptureWithFailureOutput } from "./controller-docker.js"
55
import { parseControllerRevisionLabelOutput } from "./controller-revision.js"
66
import type { ControllerBootstrapError } from "./host-errors.js"
@@ -131,7 +131,7 @@ const inspectControllerComposeImageName = (): Effect.Effect<
131131
runDockerCapture(
132132
[
133133
"compose",
134-
...composeFilesForMode(composeFiles.composePath, composeFiles.gpuOverlayPath),
134+
...composeFilesToArgs(composeFiles),
135135
"config",
136136
"--images"
137137
],

0 commit comments

Comments
 (0)